Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .changeset/selfish-toes-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@hyperdx/common-utils": patch
"@hyperdx/api": patch
"@hyperdx/app": patch
---

feat: Allow displaying group-by columns on LHS of table
6 changes: 6 additions & 0 deletions packages/api/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1348,6 +1348,12 @@
"numberFormat": {
"$ref": "#/components/schemas/NumberFormat",
"description": "Number formatting options for displayed values."
},
"groupByColumnsOnLeft": {
"type": "boolean",
"description": "When true, render Group By columns to the left of series columns in the table. Defaults to false (Group By columns on the right).\n",
"default": false,
"example": false
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2255,6 +2255,7 @@ describe('External API v2 Dashboards - new format', () => {
thousandSeparated: true,
average: true,
},
groupByColumnsOnLeft: true,
},
};

Expand Down Expand Up @@ -3091,6 +3092,7 @@ describe('External API v2 Dashboards - new format', () => {
thousandSeparated: true,
average: true,
},
groupByColumnsOnLeft: true,
},
};

Expand Down
7 changes: 7 additions & 0 deletions packages/api/src/routers/external-api/v2/dashboards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,13 @@ async function getSourceConnectionMismatches(
* numberFormat:
* $ref: '#/components/schemas/NumberFormat'
* description: Number formatting options for displayed values.
* groupByColumnsOnLeft:
* type: boolean
* description: >
* When true, render Group By columns to the left of series columns
* in the table. Defaults to false (Group By columns on the right).
* default: false
* example: false
*
* NumberBuilderChartConfig:
* type: object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ const convertToExternalTileChartConfig = (
};
case DisplayType.Table:
return {
...pick(config, ['having', 'numberFormat']),
...pick(config, ['having', 'numberFormat', 'groupByColumnsOnLeft']),
displayType: config.displayType,
sourceId,
asRatio:
Expand Down Expand Up @@ -423,6 +423,7 @@ export function convertToInternalTileConfig(
'numberFormat',
'having',
'orderBy',
'groupByColumnsOnLeft',
]),
displayType: DisplayType.Table,
select: externalConfig.select.map(convertToInternalSelectItem),
Expand Down
1 change: 1 addition & 0 deletions packages/api/src/utils/zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ const externalDashboardTableChartConfigSchema = z.object({
orderBy: z.string().max(10000).optional(),
asRatio: z.boolean().optional(),
numberFormat: NumberFormatSchema.optional(),
groupByColumnsOnLeft: z.boolean().optional(),
});

const externalDashboardTableRawSqlChartConfigSchema =
Expand Down
38 changes: 32 additions & 6 deletions packages/app/src/components/ChartDisplaySettingsDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import { shouldFillNullsWithZero } from '@/ChartUtils';
import { FormatTime } from '@/useFormatTime';

import { CheckBoxControlled } from './InputControlled';
import { DEFAULT_NUMBER_FORMAT, NumberFormatForm } from './NumberFormat';

export type ChartConfigDisplaySettings = Pick<
Expand All @@ -26,7 +27,9 @@ export type ChartConfigDisplaySettings = Pick<
| 'alignDateRangeToGranularity'
| 'fillNulls'
| 'compareToPreviousPeriod'
>;
> & {
groupByColumnsOnLeft?: boolean;
};

interface ChartDisplaySettingsDrawerProps {
opened: boolean;
Expand All @@ -35,6 +38,8 @@ interface ChartDisplaySettingsDrawerProps {
* Used as the default when no explicit numberFormat is set. */
defaultNumberFormat?: NumberFormat;
displayType: DisplayType;
/** 'sql' for raw SQL chart configs; anything else is treated as a builder config. */
configType?: 'sql' | 'builder';
previousDateRange?: [Date, Date];
onChange: (settings: ChartConfigDisplaySettings) => void;
onClose: () => void;
Expand All @@ -53,13 +58,15 @@ function applyDefaultSettings(
: settings.alignDateRangeToGranularity,
fillNulls: settings.fillNulls ?? 0,
compareToPreviousPeriod: settings.compareToPreviousPeriod ?? false,
groupByColumnsOnLeft: settings.groupByColumnsOnLeft ?? false,
};
}

export default function ChartDisplaySettingsDrawer({
settings,
opened,
displayType,
configType,
defaultNumberFormat,
onChange,
onClose,
Expand All @@ -70,7 +77,7 @@ export default function ChartDisplaySettingsDrawer({
[settings, defaultNumberFormat],
);

const { control, handleSubmit, register, reset, setValue } =
const { control, handleSubmit, reset, setValue } =
useForm<ChartConfigDisplaySettings>({
defaultValues: appliedDefaults,
});
Expand Down Expand Up @@ -99,6 +106,11 @@ export default function ChartDisplaySettingsDrawer({
const isTimeChart =
displayType === DisplayType.Line || displayType === DisplayType.StackedBar;

// Group By column ordering only applies to builder table charts; raw SQL
// configs let the user author whatever column order they want directly.
const showGroupByColumnsOnLeft =
displayType === DisplayType.Table && configType !== 'sql';

return (
<Drawer
title="Display Settings"
Expand All @@ -109,10 +121,11 @@ export default function ChartDisplaySettingsDrawer({
<Stack>
{isTimeChart && (
<>
<Checkbox
<CheckBoxControlled

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fix - these checkboxes were not being reset when the reset to defaults button is clicked, since they accept a checked prop instead of the value prop provided by {...register(...)}

control={control}
name="alignDateRangeToGranularity"
size="xs"
label="Show Complete Intervals"
{...register('alignDateRangeToGranularity')}
/>
<Box>
<Checkbox
Expand All @@ -124,7 +137,9 @@ export default function ChartDisplaySettingsDrawer({
}}
/>
</Box>
<Checkbox
<CheckBoxControlled
control={control}
name="compareToPreviousPeriod"
size="xs"
label="Compare to Previous Period"
description={
Expand All @@ -137,7 +152,18 @@ export default function ChartDisplaySettingsDrawer({
</>
)
}
{...register('compareToPreviousPeriod')}
/>
<Divider />
</>
)}

{showGroupByColumnsOnLeft && (
<>
<CheckBoxControlled
control={control}
name="groupByColumnsOnLeft"
size="xs"
label="Display Group By Columns on Left"
/>
<Divider />
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,13 +184,15 @@ export default function EditTimeChartForm({
fillNulls,
compareToPreviousPeriod,
numberFormat,
groupByColumnsOnLeft,
] = useWatch({
control,
name: [
'alignDateRangeToGranularity',
'fillNulls',
'compareToPreviousPeriod',
'numberFormat',
'groupByColumnsOnLeft',
],
});

Expand All @@ -209,12 +211,14 @@ export default function EditTimeChartForm({
fillNulls,
compareToPreviousPeriod,
numberFormat,
groupByColumnsOnLeft,
}),
[
alignDateRangeToGranularity,
fillNulls,
compareToPreviousPeriod,
numberFormat,
groupByColumnsOnLeft,
],
);

Expand Down Expand Up @@ -457,11 +461,13 @@ export default function EditTimeChartForm({
alignDateRangeToGranularity,
fillNulls,
compareToPreviousPeriod,
groupByColumnsOnLeft,
}: ChartConfigDisplaySettings) => {
setValue('numberFormat', numberFormat);
setValue('alignDateRangeToGranularity', alignDateRangeToGranularity);
setValue('fillNulls', fillNulls);
setValue('compareToPreviousPeriod', compareToPreviousPeriod);
setValue('groupByColumnsOnLeft', groupByColumnsOnLeft);
onSubmit();
},
[setValue, onSubmit],
Expand Down Expand Up @@ -661,6 +667,7 @@ export default function EditTimeChartForm({
defaultNumberFormat={autoDetectedNumberFormat}
previousDateRange={!dashboardId ? previousDateRange : undefined}
displayType={displayType}
configType={configType}
onChange={handleUpdateDisplaySettings}
onClose={closeDisplaySettings}
/>
Expand Down
29 changes: 25 additions & 4 deletions packages/app/src/components/DBTableChart.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useMemo, useState } from 'react';
import { ClickHouseQueryError } from '@hyperdx/common-utils/dist/clickhouse';
import { isRatioChartConfig } from '@hyperdx/common-utils/dist/core/renderChartConfig';
import {
isBuilderChartConfig,
isRawSqlChartConfig,
Expand Down Expand Up @@ -127,16 +128,36 @@ export default function DBTableChart({
return [];
}

const firstRow = rows.at(0);
const allKeys = firstRow ? Object.keys(firstRow) : [];

// We extract groupBy keys by counting the series columns to avoid parsing
// the groupBy string, which may have complex expressions and aliases, making
// it difficult to reliably parse out the individual group by keys.
let groupByKeys: string[] = [];
if (
isBuilderChartConfig(queriedConfig) &&
queriedConfig.groupBy &&
typeof queriedConfig.groupBy === 'string'
Array.isArray(queriedConfig.select)
) {
const isRatio = isRatioChartConfig(queriedConfig.select, queriedConfig);
const seriesCount = isRatio ? 1 : queriedConfig.select.length;
const groupByCount = allKeys.length - seriesCount;
groupByKeys = groupByCount > 0 ? allKeys.slice(-groupByCount) : [];
}

// Builder table configs may opt to render Group By columns
// to the left of series columns.
let orderedKeys = [...allKeys];
if (
isBuilderChartConfig(queriedConfig) &&
queriedConfig.groupByColumnsOnLeft &&
Array.isArray(queriedConfig.select)
) {
groupByKeys = queriedConfig.groupBy.split(',').map(v => v.trim());

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This old logic failed when the groupBy contained an alias

const seriesKeys = allKeys.filter(key => !groupByKeys.includes(key));
orderedKeys = [...groupByKeys, ...seriesKeys];
}

return Object.keys(rows?.[0])
return orderedKeys
.filter(key => !hiddenColumns?.includes(key))
.map(key => ({
// If it's an alias, wrap in quotes to support a variety of formats (ex "Time (ms)", "Req/s", etc)
Expand Down
Loading
Loading