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/nested-filter-keys-sorted.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@hyperdx/app': patch
---

fix: keys inside Map and JSON filter groups (e.g. `LogAttributes`) in the search
page filter sidebar are sorted alphabetically, after keys with a selected value
and pinned keys.
3 changes: 3 additions & 0 deletions packages/app/src/components/DBSearchPageFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ import {
type FacetEntry,
getFilterStateEntry,
groupFacetsByBaseName,
sortNestedFacetKeys,
toQuotedClickHouseKeyExpression,
} from './DBSearchPageFilters/utils';

Expand Down Expand Up @@ -1366,6 +1367,8 @@ const DBSearchPageFiltersComponent = ({
});
}

sortNestedFacetKeys(_facets);
Comment thread
knudtty marked this conversation as resolved.
Comment thread
knudtty marked this conversation as resolved.
Comment thread
knudtty marked this conversation as resolved.

// prioritize facets that are primary keys
_facets.sort((a, b) => {
const aIsPk = isFieldPrimary(tableMetadata, a.key);
Expand Down
29 changes: 29 additions & 0 deletions packages/app/src/components/DBSearchPageFilters/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
getFilterStateEntry,
groupFacetsByBaseName,
listRequestedFacets,
sortNestedFacetKeys,
toClickHouseKeyExpression,
toQuotedClickHouseKeyExpression,
} from './utils';
Expand Down Expand Up @@ -162,6 +163,34 @@ describe('listRequestedFacets', () => {
});
});

describe('sortNestedFacetKeys', () => {
Comment thread
knudtty marked this conversation as resolved.
Comment thread
knudtty marked this conversation as resolved.
Comment thread
knudtty marked this conversation as resolved.
it('sorts keys A–Z within each nested group without reordering groups or other facets', () => {
const facets = [
'SeverityText',
"ResourceAttributes['service.name']",
"LogAttributes['user.id']",
'ServiceName',
"ResourceAttributes['host.name']",
"LogAttributes['attr10']",
'LogAttributes.http.method',
"LogAttributes['attr9']",
].map(key => ({ key }));

sortNestedFacetKeys(facets);

expect(facets.map(f => f.key)).toEqual([
'SeverityText',
'ServiceName',
"ResourceAttributes['host.name']",
"ResourceAttributes['service.name']",
"LogAttributes['attr9']",
"LogAttributes['attr10']",
'LogAttributes.http.method',
"LogAttributes['user.id']",
]);
});
});

describe('getFilterStateEntry', () => {
const makeEntry = (included: string[]): FilterState[string] => ({
included: new Set<string | boolean>(included),
Expand Down
25 changes: 25 additions & 0 deletions packages/app/src/components/DBSearchPageFilters/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,31 @@ export function groupFacetsByBaseName(facets: FacetEntry[]) {
return { grouped: Array.from(grouped.values()), nonGrouped };
}

const naturalCompare = new Intl.Collator(undefined, { numeric: true }).compare;

// Sorts the keys inside each nested group A–Z, keeping groups in the order they
// first appear. Other facets keep their order and move ahead of the nested
// ones, which is harmless because grouped and non-grouped facets render
// separately.
export function sortNestedFacetKeys(facets: { key: string }[]) {
const groupOrder = new Map<string, number>();
for (const { key } of facets) {
const baseName = parseMapFieldName(key)?.baseName;
if (baseName != null && !groupOrder.has(baseName)) {
groupOrder.set(baseName, groupOrder.size);
}
}
facets.sort((a, b) => {
Comment thread
knudtty marked this conversation as resolved.
const nestedA = parseMapFieldName(a.key);
Comment thread
knudtty marked this conversation as resolved.
const nestedB = parseMapFieldName(b.key);
if (!nestedA || !nestedB) return Number(!!nestedA) - Number(!!nestedB);
return (
groupOrder.get(nestedA.baseName)! - groupOrder.get(nestedB.baseName)! ||
naturalCompare(nestedA.propertyPath, nestedB.propertyPath)
);
});
}

// Look up a filterState entry by either bracket-form or dot-form map sub-key.
// Bracket form is the canonical SQL key used in facet results; dot form is
// what setFilterValue stores after its parseKeyPath().join('.') normalization
Expand Down
Loading