Skip to content

Commit ed805b8

Browse files
committed
[Fleet] Support expending config grouped by version
1 parent 706b84d commit ed805b8

7 files changed

Lines changed: 507 additions & 8 deletions

File tree

x-pack/platform/plugins/shared/fleet/common/types/rest_spec/collector.ts

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,17 @@ import { type TypeOf, schema } from '@kbn/config-schema';
99

1010
export const GetCollectorGroupsRequestSchema = {
1111
query: schema.object({
12-
groupBy: schema.oneOf([schema.literal('collector.group'), schema.literal('config.name')], {
13-
defaultValue: 'collector.group' as const,
14-
meta: { description: 'Field to group collectors by' },
15-
}),
12+
groupBy: schema.oneOf(
13+
[
14+
schema.literal('collector.group'),
15+
schema.literal('config.name'),
16+
schema.literal('pipeline_config'),
17+
],
18+
{
19+
defaultValue: 'collector.group' as const,
20+
meta: { description: 'Field to group collectors by' },
21+
}
22+
),
1623
kuery: schema.maybe(
1724
schema.string({
1825
maxLength: 4096,
@@ -66,6 +73,37 @@ export const CollectorGroupSchema = schema.object({
6673
},
6774
})
6875
),
76+
firstSeen: schema.maybe(
77+
schema.string({
78+
meta: { description: 'Earliest enrolled_at timestamp in this group (ISO date)' },
79+
})
80+
),
81+
lastSeen: schema.maybe(
82+
schema.string({
83+
meta: { description: 'Latest last_checkin timestamp in this group (ISO date)' },
84+
})
85+
),
86+
pipelineConfigs: schema.maybe(
87+
schema.object(
88+
{
89+
top: schema.arrayOf(schema.string({ maxLength: 4096 }), {
90+
maxSize: 3,
91+
meta: {
92+
description: 'Top pipeline configuration fingerprints by frequency',
93+
},
94+
}),
95+
total: schema.number({
96+
meta: { description: 'Total number of distinct pipeline configurations in this group' },
97+
}),
98+
},
99+
{
100+
meta: {
101+
description:
102+
'Distinct pipeline configuration fingerprints and their count within the group',
103+
},
104+
}
105+
)
106+
),
69107
});
70108

71109
export const GetCollectorGroupsResponseSchema = schema.object({

x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/collectors/components/collector_groups_table.tsx

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ import { i18n } from '@kbn/i18n';
2525
import { FormattedMessage } from '@kbn/i18n-react';
2626

2727
import { AGENT_TYPE_OPAMP } from '../../../../../../common/constants';
28+
import { pipelineConfigLabel } from '../../../../../../common/services/pipeline_config_label';
2829
import type { Agent, CollectorGroup } from '../../../../../../common/types';
2930
import { useGetAgentsQuery } from '../../../../../hooks/use_request/agents';
3031

3132
import { CollectorsTable } from './collectors_table';
33+
import { ExpandedConfigGroup } from './expanded_config_group';
3234

3335
import { getSignalBadgeColor } from './signal_colors';
3436

@@ -123,9 +125,41 @@ const CollectorGroupRow: React.FC<{
123125
<EuiFlexItem grow>
124126
<EuiFlexGroup direction="column" gutterSize="xs" responsive={false}>
125127
<EuiFlexItem grow={false}>
126-
<EuiText size="s">
127-
<strong>{displayName}</strong>
128-
</EuiText>
128+
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap>
129+
<EuiFlexItem grow={false}>
130+
<EuiText size="s">
131+
<strong>{displayName}</strong>
132+
</EuiText>
133+
</EuiFlexItem>
134+
{groupBy === 'config.name' && group.pipelineConfigs != null && (
135+
<>
136+
{group.pipelineConfigs.top.map((fingerprint) => (
137+
<EuiFlexItem grow={false} key={fingerprint}>
138+
<EuiToolTip content={fingerprint}>
139+
<EuiBadge color="hollow">
140+
{pipelineConfigLabel(fingerprint)}
141+
</EuiBadge>
142+
</EuiToolTip>
143+
</EuiFlexItem>
144+
))}
145+
{group.pipelineConfigs.total > group.pipelineConfigs.top.length && (
146+
<EuiFlexItem grow={false}>
147+
<EuiBadge color="hollow">
148+
<FormattedMessage
149+
id="xpack.fleet.collectorGroups.morePipelineConfigs"
150+
defaultMessage="+{count} more"
151+
values={{
152+
count:
153+
group.pipelineConfigs.total -
154+
group.pipelineConfigs.top.length,
155+
}}
156+
/>
157+
</EuiBadge>
158+
</EuiFlexItem>
159+
)}
160+
</>
161+
)}
162+
</EuiFlexGroup>
129163
</EuiFlexItem>
130164
{group.signals.length > 0 && (
131165
<EuiFlexItem grow={false}>
@@ -232,7 +266,7 @@ const CollectorGroupRow: React.FC<{
232266
{groupBy === 'collector.group' ? (
233267
<ExpandedGroupCollectors group={group} />
234268
) : (
235-
'TODO: implement expanded content for config.name grouping'
269+
<ExpandedConfigGroup group={group} />
236270
)}
237271
</EuiPanel>
238272
)}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React, { useMemo } from 'react';
9+
import { css } from '@emotion/react';
10+
import type { EuiBasicTableColumn } from '@elastic/eui';
11+
import {
12+
EuiBasicTable,
13+
EuiBadge,
14+
EuiFlexGroup,
15+
EuiFlexItem,
16+
EuiLoadingSpinner,
17+
EuiText,
18+
EuiToolTip,
19+
useEuiTheme,
20+
} from '@elastic/eui';
21+
import { i18n } from '@kbn/i18n';
22+
import { FormattedDate, FormattedRelative } from '@kbn/i18n-react';
23+
24+
import { pipelineConfigLabel } from '../../../../../../common/services/pipeline_config_label';
25+
import type { CollectorGroup } from '../../../../../../common/types';
26+
import { useGetCollectorGroupsQuery } from '../../../../../hooks/use_request/agents';
27+
28+
import { getSignalBadgeColor } from './signal_colors';
29+
30+
const ConfigHashTable: React.FC<{ group: CollectorGroup }> = ({ group }) => {
31+
const { euiTheme } = useEuiTheme();
32+
33+
const kuery = useMemo(() => {
34+
if (group.isUngrouped) {
35+
return 'NOT non_identifying_attributes.config.name:*';
36+
}
37+
const escapedConfigName = group.group.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
38+
return `non_identifying_attributes.config.name:"${escapedConfigName}"`;
39+
}, [group.group, group.isUngrouped]);
40+
41+
const { data, isLoading } = useGetCollectorGroupsQuery(
42+
{ groupBy: 'pipeline_config', kuery, perPage: 100, showInactive: false },
43+
{ enabled: true }
44+
);
45+
46+
const items = data?.items ?? [];
47+
48+
const columns: Array<EuiBasicTableColumn<CollectorGroup>> = useMemo(
49+
() => [
50+
{
51+
field: 'group',
52+
name: i18n.translate('xpack.fleet.expandedConfigGroup.versionsColumn', {
53+
defaultMessage: 'Versions',
54+
}),
55+
width: '200px',
56+
render: (fingerprint: string) => (
57+
<EuiToolTip content={fingerprint}>
58+
<EuiBadge color="hollow">{pipelineConfigLabel(fingerprint)}</EuiBadge>
59+
</EuiToolTip>
60+
),
61+
},
62+
{
63+
field: 'docCount',
64+
name: i18n.translate('xpack.fleet.expandedConfigGroup.collectorsColumn', {
65+
defaultMessage: 'Collectors',
66+
}),
67+
width: '100px',
68+
},
69+
{
70+
field: 'signals',
71+
name: i18n.translate('xpack.fleet.expandedConfigGroup.signalsColumn', {
72+
defaultMessage: 'Signals',
73+
}),
74+
width: '190px',
75+
render: (signals: string[]) => {
76+
if (!signals?.length) return '-';
77+
return (
78+
<EuiFlexGroup gutterSize="xs" wrap responsive={false}>
79+
{signals.map((signal) => {
80+
const [bgColor, textColor] = getSignalBadgeColor(euiTheme.colors.vis, signal);
81+
return (
82+
<EuiFlexItem grow={false} key={signal}>
83+
<EuiBadge
84+
color={bgColor}
85+
css={css`
86+
color: ${textColor};
87+
`}
88+
>
89+
{signal}
90+
</EuiBadge>
91+
</EuiFlexItem>
92+
);
93+
})}
94+
</EuiFlexGroup>
95+
);
96+
},
97+
},
98+
{
99+
name: i18n.translate('xpack.fleet.expandedConfigGroup.alertsColumn', {
100+
defaultMessage: 'Alerts',
101+
}),
102+
width: '80px',
103+
render: () => '-',
104+
},
105+
{
106+
field: 'firstSeen',
107+
name: i18n.translate('xpack.fleet.expandedConfigGroup.firstSeenColumn', {
108+
defaultMessage: 'First seen',
109+
}),
110+
width: '120px',
111+
render: (firstSeen: string | undefined) => {
112+
if (!firstSeen) return '-';
113+
return (
114+
<EuiToolTip
115+
content={
116+
<FormattedDate
117+
value={firstSeen}
118+
year="numeric"
119+
month="short"
120+
day="2-digit"
121+
hour="2-digit"
122+
minute="2-digit"
123+
/>
124+
}
125+
>
126+
<EuiText size="xs">
127+
<FormattedRelative value={firstSeen} />
128+
</EuiText>
129+
</EuiToolTip>
130+
);
131+
},
132+
},
133+
{
134+
field: 'lastSeen',
135+
name: i18n.translate('xpack.fleet.expandedConfigGroup.lastSeenColumn', {
136+
defaultMessage: 'Last seen',
137+
}),
138+
width: '120px',
139+
render: (lastSeen: string | undefined) => {
140+
if (!lastSeen) return '-';
141+
return (
142+
<EuiToolTip
143+
content={
144+
<FormattedDate
145+
value={lastSeen}
146+
year="numeric"
147+
month="short"
148+
day="2-digit"
149+
hour="2-digit"
150+
minute="2-digit"
151+
/>
152+
}
153+
>
154+
<EuiText size="xs">
155+
<FormattedRelative value={lastSeen} />
156+
</EuiText>
157+
</EuiToolTip>
158+
);
159+
},
160+
},
161+
{
162+
name: i18n.translate('xpack.fleet.expandedConfigGroup.actionsColumn', {
163+
defaultMessage: 'Actions',
164+
}),
165+
width: '70px',
166+
actions: [],
167+
},
168+
],
169+
[euiTheme.colors.vis]
170+
);
171+
172+
if (isLoading) {
173+
return (
174+
<EuiFlexGroup justifyContent="center">
175+
<EuiFlexItem grow={false}>
176+
<EuiLoadingSpinner size="l" />
177+
</EuiFlexItem>
178+
</EuiFlexGroup>
179+
);
180+
}
181+
182+
return (
183+
<EuiBasicTable<CollectorGroup>
184+
data-test-subj="fleetConfigHashTable"
185+
tableCaption={i18n.translate('xpack.fleet.expandedConfigGroup.tableCaption', {
186+
defaultMessage: 'Pipeline configuration versions',
187+
})}
188+
items={items}
189+
itemId="group"
190+
columns={columns}
191+
/>
192+
);
193+
};
194+
195+
export const ExpandedConfigGroup: React.FC<{ group: CollectorGroup }> = ({ group }) => {
196+
return <ConfigHashTable group={group} />;
197+
};

x-pack/platform/plugins/shared/fleet/server/routes/agent/examples/get_collector_groups.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,25 @@ responses:
1515
- 'logs'
1616
- 'metrics'
1717
isUngrouped: false
18+
firstSeen: '2026-05-20T08:00:00.000Z'
19+
lastSeen: '2026-05-27T09:30:00.000Z'
20+
pipelineConfigs:
21+
top:
22+
- 'pipe:logs/access[otlp||elasticsearch];receivers:otlp;exporters:elasticsearch'
23+
- 'pipe:logs/error[otlp||elasticsearch];receivers:otlp;exporters:elasticsearch'
24+
total: 2
1825
- group: 'database-servers'
1926
groupDisplayName: 'database-servers'
2027
docCount: 3
2128
signals:
2229
- 'metrics'
2330
- 'traces'
31+
firstSeen: '2026-05-22T12:00:00.000Z'
32+
lastSeen: '2026-05-27T09:25:00.000Z'
33+
pipelineConfigs:
34+
top:
35+
- 'pipe:metrics/db[otlp||elasticsearch];receivers:otlp;exporters:elasticsearch'
36+
total: 1
2437
afterKey: '{"collector.group":"database-servers"}'
2538
400:
2639
description: 'Bad Request'

0 commit comments

Comments
 (0)