Skip to content

Commit bb6fa03

Browse files
laura-b-gousmaneo
andauthored
feat(streams): add Default/Routing/Performance view buttons to Streams table (#26555)
Co-authored-by: Ousmane SAMBA <ousmane.samba@graylog.com>
1 parent fb795ce commit bb6fa03

9 files changed

Lines changed: 232 additions & 104 deletions

File tree

changelog/unreleased/pr-26555.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
type = "a"
2+
message = "Add Default, Routing, and Performance view buttons to the streams table for quickly switching between predefined column sets."
3+
4+
pulls = ["26555", "Graylog2/graylog-plugin-enterprise#14711"]

graylog2-web-interface/src/@types/graylog-web-plugin/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ type IndexRetentionConfig = {
202202

203203
type StreamsOverviewTableElement = {
204204
attributeName: string;
205+
group?: 'routing' | 'performance';
205206
attributes: Array<Attribute>;
206207
columnRenderers: ColumnRenderersByAttribute<Stream>;
207208
// Optional map of column id → backend metric fields. Plugins use this to plug their

graylog2-web-interface/src/components/streams/StreamsOverview/Constants.ts

Lines changed: 80 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -16,70 +16,110 @@
1616
*/
1717
import type { Attribute, Sort } from 'stores/PaginationTypes';
1818

19+
import type { ExtensionColumnGroups } from './hooks/useStreamsOverviewExtensions';
1920
import { METRIC_COLUMN_IDS, METRIC_COLUMN_TITLES } from './metricColumns';
2021

22+
export const STREAM_VIEW_VARIANTS = {
23+
default: '' as const,
24+
routing: 'routing' as const,
25+
performance: 'performance' as const,
26+
};
27+
28+
const SHARED_LAYOUT = {
29+
entityTableId: 'streams',
30+
defaultPageSize: 20,
31+
defaultSort: { attributeId: 'title', direction: 'asc' } as Sort,
32+
};
33+
2134
const getStreamTableElements = (
2235
isPipelineColumnPermitted: boolean,
2336
extensionAttributes?: {
2437
attributeNames?: Array<string>;
2538
attributes?: Array<Attribute>;
2639
},
40+
extensionColumnGroups?: ExtensionColumnGroups,
2741
) => {
28-
const defaultLayout = {
29-
entityTableId: 'streams',
30-
defaultPageSize: 20,
31-
defaultSort: { attributeId: 'title', direction: 'asc' } as Sort,
32-
defaultDisplayedAttributes: [
33-
'title',
34-
'index_set_title',
35-
'rules',
36-
...(isPipelineColumnPermitted ? ['pipelines'] : []),
37-
'outputs',
38-
'archiving',
39-
...(extensionAttributes?.attributeNames || []),
40-
'destination_filters',
41-
'disabled',
42-
'throughput',
43-
],
44-
defaultColumnOrder: [
45-
'title',
46-
'index_set_title',
47-
'rules',
48-
'outputs',
49-
'archiving',
50-
...(extensionAttributes?.attributeNames || []),
51-
'destination_filters',
52-
'disabled',
53-
'throughput',
54-
METRIC_COLUMN_IDS.messageCount,
55-
METRIC_COLUMN_IDS.avgProcessingTime,
56-
METRIC_COLUMN_IDS.maxProcessingTime,
57-
METRIC_COLUMN_IDS.associatedInputs,
58-
...(isPipelineColumnPermitted ? ['pipelines', METRIC_COLUMN_IDS.routingPipelines] : []),
59-
'created_at',
60-
],
42+
const extRouting = extensionColumnGroups?.routing ?? [];
43+
const extPerformance = extensionColumnGroups?.performance ?? [];
44+
45+
const groupedIds = new Set([...extRouting, ...extPerformance]);
46+
const ungroupedExtNames = (extensionAttributes?.attributeNames ?? []).filter((id) => !groupedIds.has(id));
47+
48+
const defaultCols = [
49+
'title',
50+
'index_set_title',
51+
'rules',
52+
...(isPipelineColumnPermitted ? ['pipelines'] : []),
53+
'destination_filters',
54+
'disabled',
55+
'throughput',
56+
];
57+
58+
const routingCols = [
59+
METRIC_COLUMN_IDS.associatedInputs,
60+
...(isPipelineColumnPermitted ? [METRIC_COLUMN_IDS.routingPipelines] : []),
61+
'outputs',
62+
...extRouting,
63+
'archiving',
64+
];
65+
66+
const performanceCols = [
67+
METRIC_COLUMN_IDS.messageCount,
68+
...extPerformance,
69+
METRIC_COLUMN_IDS.avgProcessingTime,
70+
METRIC_COLUMN_IDS.maxProcessingTime,
71+
];
72+
73+
const defaultColumnOrder = [
74+
...defaultCols,
75+
...routingCols,
76+
...performanceCols,
77+
...ungroupedExtNames,
78+
'created_at',
79+
];
80+
81+
const defaultVariantLayout = {
82+
...SHARED_LAYOUT,
83+
defaultColumnOrder,
84+
defaultDisplayedAttributes: defaultCols,
85+
};
86+
87+
const routingVariantLayout = {
88+
...SHARED_LAYOUT,
89+
layoutVariant: STREAM_VIEW_VARIANTS.routing,
90+
defaultColumnOrder,
91+
defaultDisplayedAttributes: [...defaultCols, ...routingCols],
92+
};
93+
94+
const performanceVariantLayout = {
95+
...SHARED_LAYOUT,
96+
layoutVariant: STREAM_VIEW_VARIANTS.performance,
97+
defaultColumnOrder,
98+
defaultDisplayedAttributes: [...defaultCols, ...performanceCols],
6199
};
62100

63101
const additionalAttributes: Array<Attribute> = [
64102
{ id: 'index_set_title', title: 'Index Set', sortable: true, permissions: ['indexsets:read'] },
65103
{ id: 'throughput', title: 'Throughput' },
66104
{ id: 'rules', title: 'Stream Rules' },
67105
...(isPipelineColumnPermitted ? [{ id: 'pipelines', title: 'Pipelines' }] : []),
68-
{ id: 'outputs', title: 'Outputs' },
69-
{ id: 'archiving', title: 'Archiving' },
70-
...(extensionAttributes?.attributes || []),
71106
{ id: 'destination_filters', title: 'Filter Rules' },
72-
{ id: METRIC_COLUMN_IDS.messageCount, title: METRIC_COLUMN_TITLES[METRIC_COLUMN_IDS.messageCount] },
73-
{ id: METRIC_COLUMN_IDS.avgProcessingTime, title: METRIC_COLUMN_TITLES[METRIC_COLUMN_IDS.avgProcessingTime] },
74-
{ id: METRIC_COLUMN_IDS.maxProcessingTime, title: METRIC_COLUMN_TITLES[METRIC_COLUMN_IDS.maxProcessingTime] },
75107
{ id: METRIC_COLUMN_IDS.associatedInputs, title: METRIC_COLUMN_TITLES[METRIC_COLUMN_IDS.associatedInputs] },
76108
...(isPipelineColumnPermitted
77109
? [{ id: METRIC_COLUMN_IDS.routingPipelines, title: METRIC_COLUMN_TITLES[METRIC_COLUMN_IDS.routingPipelines] }]
78110
: []),
111+
{ id: 'outputs', title: 'Outputs' },
112+
...(extensionAttributes?.attributes || []),
113+
{ id: 'archiving', title: 'Archiving' },
114+
{ id: METRIC_COLUMN_IDS.messageCount, title: METRIC_COLUMN_TITLES[METRIC_COLUMN_IDS.messageCount] },
115+
{ id: METRIC_COLUMN_IDS.avgProcessingTime, title: METRIC_COLUMN_TITLES[METRIC_COLUMN_IDS.avgProcessingTime] },
116+
{ id: METRIC_COLUMN_IDS.maxProcessingTime, title: METRIC_COLUMN_TITLES[METRIC_COLUMN_IDS.maxProcessingTime] },
79117
];
80118

81119
return {
82-
defaultLayout,
120+
defaultVariantLayout,
121+
routingVariantLayout,
122+
performanceVariantLayout,
83123
additionalAttributes,
84124
};
85125
};
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright (C) 2020 Graylog, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the Server Side Public License, version 1,
6+
* as published by MongoDB, Inc.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* Server Side Public License for more details.
12+
*
13+
* You should have received a copy of the Server Side Public License
14+
* along with this program. If not, see
15+
* <http://www.mongodb.com/licensing/server-side-public-license>.
16+
*/
17+
import * as React from 'react';
18+
import { useCallback } from 'react';
19+
import styled, { css } from 'styled-components';
20+
21+
import SegmentedControl from 'components/bootstrap/SegmentedControl';
22+
import useLayoutVariant from 'components/common/PaginatedEntityTable/hooks/useLayoutVariant';
23+
24+
import { STREAM_VIEW_VARIANTS } from './Constants';
25+
26+
const Container = styled.div(
27+
({ theme }) => css`
28+
display: flex;
29+
align-items: center;
30+
gap: ${theme.spacings.sm};
31+
`,
32+
);
33+
34+
const Label = styled.span(
35+
({ theme }) => css`
36+
color: ${theme.colors.text.secondary};
37+
white-space: nowrap;
38+
`,
39+
);
40+
41+
const NarrowSegmentedControl = styled(SegmentedControl)`
42+
width: fit-content;
43+
`;
44+
45+
const SEGMENTS = [
46+
{ label: 'Default', value: STREAM_VIEW_VARIANTS.default },
47+
{ label: 'Routing', value: STREAM_VIEW_VARIANTS.routing },
48+
{ label: 'Performance', value: STREAM_VIEW_VARIANTS.performance },
49+
];
50+
51+
type VariantValue = (typeof STREAM_VIEW_VARIANTS)[keyof typeof STREAM_VIEW_VARIANTS];
52+
53+
const StreamViewButtons = () => {
54+
const { activeLayoutVariant, selectLayoutVariant } = useLayoutVariant();
55+
56+
const onChange = useCallback(
57+
(value: VariantValue) => {
58+
if (value === STREAM_VIEW_VARIANTS.default) {
59+
if (activeLayoutVariant) selectLayoutVariant(activeLayoutVariant);
60+
} else {
61+
selectLayoutVariant(value);
62+
}
63+
},
64+
[activeLayoutVariant, selectLayoutVariant],
65+
);
66+
67+
return (
68+
<Container>
69+
<Label>View:</Label>
70+
<NarrowSegmentedControl<VariantValue>
71+
data={SEGMENTS}
72+
value={activeLayoutVariant as VariantValue}
73+
onChange={onChange}
74+
/>
75+
</Container>
76+
);
77+
};
78+
79+
export default StreamViewButtons;

graylog2-web-interface/src/components/streams/StreamsOverview/StreamsOverview.test.tsx

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -294,17 +294,28 @@ describe('StreamsOverview', () => {
294294
expect(screen.queryByText(/1 connected output\./i)).not.toBeInTheDocument();
295295
});
296296

297-
it('should render stream overview table elements from plugins', async () => {
297+
it('should only show grouped plugin table elements in their matching view', async () => {
298298
const plugin = new PluginManifest(
299299
{},
300300
{
301301
'components.streams.overview.tableElements': [
302302
{
303-
attributeName: 'data_lake',
304-
attributes: [{ id: 'data_lake', title: 'Data Lake' }],
303+
attributeName: 'my_plugin_column',
304+
group: 'routing',
305+
attributes: [{ id: 'my_plugin_column', title: 'My Plugin Column' }],
305306
columnRenderers: {
306-
data_lake: {
307-
renderCell: () => 'Preview logs',
307+
my_plugin_column: {
308+
renderCell: () => 'plugin cell',
309+
staticWidth: 'matchHeader',
310+
},
311+
},
312+
},
313+
{
314+
attributeName: 'my_ungrouped_column',
315+
attributes: [{ id: 'my_ungrouped_column', title: 'My Ungrouped Column' }],
316+
columnRenderers: {
317+
my_ungrouped_column: {
318+
renderCell: () => 'ungrouped cell',
308319
staticWidth: 'matchHeader',
309320
},
310321
},
@@ -327,8 +338,15 @@ describe('StreamsOverview', () => {
327338
try {
328339
renderSut();
329340

330-
await screen.findByText('Data Lake');
331-
await screen.findByText('Preview logs');
341+
await screen.findByText('Title');
342+
expect(screen.queryByText('My Plugin Column')).not.toBeInTheDocument();
343+
expect(screen.queryByText('My Ungrouped Column')).not.toBeInTheDocument();
344+
345+
await userEvent.click(screen.getByRole('radio', { name: 'Routing' }));
346+
347+
await screen.findByText('My Plugin Column');
348+
expect(screen.getByText('plugin cell')).toBeInTheDocument();
349+
expect(screen.queryByText('My Ungrouped Column')).not.toBeInTheDocument();
332350
} finally {
333351
PluginStore.unregister(plugin);
334352
}

graylog2-web-interface/src/components/streams/StreamsOverview/StreamsOverview.tsx

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ import QueryHelper from 'components/common/QueryHelper';
2020
import type { Stream } from 'logic/streams/types';
2121
import type { IndexSet } from 'stores/indices/IndexSetsStore';
2222
import { keyFn, fetchStreams } from 'components/streams/hooks/useStreams';
23-
import getStreamTableElements from 'components/streams/StreamsOverview/Constants';
23+
import getStreamTableElements, { STREAM_VIEW_VARIANTS } from 'components/streams/StreamsOverview/Constants';
2424
import FilterValueRenderers from 'components/streams/StreamsOverview/FilterValueRenderers';
2525
import useTableElements from 'components/streams/StreamsOverview/hooks/useTableComponents';
2626
import PaginatedEntityTable from 'components/common/PaginatedEntityTable';
2727
import useUserLayoutPreferences from 'components/common/EntityDataTable/hooks/useUserLayoutPreferences';
28+
import useLayoutVariant from 'components/common/PaginatedEntityTable/hooks/useLayoutVariant';
2829
import { ATTRIBUTE_STATUS } from 'components/common/EntityDataTable/Constants';
2930
import { CurrentUserStore } from 'stores/users/CurrentUserStore';
3031
import type { SearchParams } from 'stores/PaginationTypes';
@@ -33,6 +34,7 @@ import type { PaginatedResponse } from 'components/common/PaginatedEntityTable/u
3334
import CustomColumnRenderers from './ColumnRenderers';
3435
import usePipelineColumn from './hooks/usePipelineColumn';
3536
import useStreamsOverviewExtensions from './hooks/useStreamsOverviewExtensions';
37+
import StreamViewButtons from './StreamViewButtons';
3638
import { StreamMetricsProvider } from './StreamMetricsContext';
3739
import { backendFieldsForVisibleColumns } from './metricColumns';
3840

@@ -48,17 +50,24 @@ const StreamsOverview = ({ indexSets }: Props) => {
4850
const {
4951
columnRenderers: extensionColumnRenderers,
5052
attributes: extensionAttributes,
53+
columnGroups: extensionColumnGroups,
5154
expandedSections: pluggableExpandedSections,
5255
metricFields: extensionMetricFields,
5356
} = useStreamsOverviewExtensions();
5457

5558
const { entityActions, expandedSections, bulkActions } = useTableElements({ indexSets, pluggableExpandedSections });
5659

5760
const columnRenderers = CustomColumnRenderers(indexSets, isPipelineColumnPermitted, extensionColumnRenderers);
58-
const { additionalAttributes, defaultLayout } = getStreamTableElements(
59-
isPipelineColumnPermitted,
60-
extensionAttributes,
61-
);
61+
const { defaultVariantLayout, routingVariantLayout, performanceVariantLayout, additionalAttributes } =
62+
getStreamTableElements(isPipelineColumnPermitted, extensionAttributes, extensionColumnGroups);
63+
64+
const { activeLayoutVariant } = useLayoutVariant();
65+
66+
const variantLayouts: Record<string, typeof defaultVariantLayout> = {
67+
[STREAM_VIEW_VARIANTS.routing]: routingVariantLayout,
68+
[STREAM_VIEW_VARIANTS.performance]: performanceVariantLayout,
69+
};
70+
const activeLayout = variantLayouts[activeLayoutVariant] ?? defaultVariantLayout;
6271

6372
const fetchEntities = (options: SearchParams): Promise<PaginatedResponse<Stream>> => {
6473
CurrentUserStore.update(CurrentUserStore.getInitialState().currentUser.username);
@@ -75,12 +84,12 @@ const StreamsOverview = ({ indexSets }: Props) => {
7584
);
7685
};
7786

78-
const { data: layoutPreferences } = useUserLayoutPreferences(defaultLayout.entityTableId);
87+
const { data: layoutPreferences } = useUserLayoutPreferences(activeLayout.entityTableId, activeLayoutVariant);
7988
const userPrefs = layoutPreferences?.attributes ?? {};
8089
const userSelection = Object.entries(userPrefs)
8190
.filter(([, pref]) => pref.status === ATTRIBUTE_STATUS.show)
8291
.map(([attributeId]) => attributeId);
83-
const visibleColumns = userSelection.length > 0 ? userSelection : defaultLayout.defaultDisplayedAttributes;
92+
const visibleColumns = userSelection.length > 0 ? userSelection : activeLayout.defaultDisplayedAttributes;
8493
const requestedFields = backendFieldsForVisibleColumns(visibleColumns, extensionMetricFields);
8594

8695
return (
@@ -90,7 +99,7 @@ const StreamsOverview = ({ indexSets }: Props) => {
9099
additionalAttributes={additionalAttributes}
91100
queryHelpComponent={<QueryHelper entityName="stream" />}
92101
entityActions={entityActions}
93-
tableLayout={defaultLayout}
102+
tableLayout={activeLayout}
94103
fetchEntities={fetchEntities}
95104
onDataLoaded={onDataLoaded}
96105
keyFn={keyFn}
@@ -99,6 +108,7 @@ const StreamsOverview = ({ indexSets }: Props) => {
99108
entityAttributesAreCamelCase={false}
100109
filterValueRenderers={FilterValueRenderers}
101110
columnRenderers={columnRenderers}
111+
topSection={StreamViewButtons}
102112
/>
103113
</StreamMetricsProvider>
104114
);

graylog2-web-interface/src/components/streams/StreamsOverview/cells/MessageCountCell.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import * as React from 'react';
1818

1919
import type { Stream } from 'logic/streams/types';
20-
import { CountBadge, Spinner } from 'components/common';
20+
import { Spinner } from 'components/common';
2121
import { useStreamMetricsFor } from 'components/streams/StreamsOverview/StreamMetricsContext';
2222
import { formatCount } from 'components/inputs/helpers/InputThroughputUtils';
2323

@@ -37,10 +37,7 @@ const MessageCountCell = ({ stream }: Props) => {
3737
}
3838

3939
return (
40-
<CountBadge
41-
count={formatCount(metrics.message_count)}
42-
title={`${metrics.message_count} messages in the last 24 hours`}
43-
/>
40+
<span title={`${metrics.message_count} messages in the last 24 hours`}>{formatCount(metrics.message_count)}</span>
4441
);
4542
};
4643

0 commit comments

Comments
 (0)