Skip to content

Commit 184d0a3

Browse files
flash1293kibanamachine
andauthoredMar 20, 2025··
🌊 Streams: Overview page redesign (#214196)
This PR overhauls the overview page. Classic stream: <img width="1004" alt="Screenshot 2025-03-12 at 21 00 39" src="https://github.com/user-attachments/assets/a058da08-0ae2-48cc-abca-359b23288b32" /> Wired stream: <img width="1019" alt="Screenshot 2025-03-12 at 21 01 56" src="https://github.com/user-attachments/assets/bca04537-f79b-4814-8e31-9d3dae18ad90" /> ## Doubts / things I changed from the design * Quick links is just all dashboards, so I adjusted the wording accordingly. Also, since we render all dashboards, there isn't really value in "View all assets" * The panel on top is already stating the count of docs, why should we repeat that in the histogram panel? * No search bar - in the beginning we said we don't want this page to become discover, a search bar feels like we are going there. Also, what should the user enter there? I don't think we want to buy deeper in KQL * Should the count of docs be the total count of the count for the currently selected time range? Not sure what makes more sense * For wired streams I left the tabs in place to switch between child streams and quick links. We can revisit this once we get closer to actually releasing wired streams --------- Co-authored-by: kibanamachine <[email protected]>
1 parent f89e03c commit 184d0a3

File tree

23 files changed

+772
-367
lines changed

23 files changed

+772
-367
lines changed
 

‎src/platform/packages/shared/kbn-visualization-utils/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ export { useDebouncedValue } from './src/debounced_value';
1414
export { ChartType } from './src/types';
1515
export { getDatasourceId } from './src/get_datasource_id';
1616
export { mapVisToChartType } from './src/map_vis_to_chart_type';
17+
export { computeInterval } from './src/compute_interval';

‎src/platform/plugins/shared/unified_histogram/public/utils/compute_interval.ts ‎src/platform/packages/shared/kbn-visualization-utils/src/compute_interval.ts

-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
1111
import type { TimeRange } from '@kbn/es-query';
1212

1313
// follows the same logic with vega auto_date function
14-
// we could move to a package and reuse in the future
1514
const barTarget = 50; // same as vega
1615
const roundInterval = (interval: number) => {
1716
{

‎src/platform/packages/shared/kbn-visualization-utils/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@
1313
"@kbn/interpreter",
1414
"@kbn/data-views-plugin",
1515
"@kbn/es-query",
16+
"@kbn/data-plugin",
1617
]
1718
}

‎src/platform/plugins/shared/data/public/query/timefilter/use_timefilter.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
import { TimeRange } from '@kbn/es-query';
11-
import { useCallback, useEffect, useMemo, useState } from 'react';
11+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
1212
import type { Timefilter } from './timefilter';
1313

1414
export interface TimefilterHook {
@@ -18,18 +18,22 @@ export interface TimefilterHook {
1818
end: number;
1919
};
2020
setTimeRange: React.Dispatch<React.SetStateAction<TimeRange>>;
21+
refreshAbsoluteTimeRange: () => boolean;
2122
}
2223

2324
export function createUseTimefilterHook(timefilter: Timefilter) {
2425
return function useTimefilter(): TimefilterHook {
2526
const [timeRange, setTimeRange] = useState(() => timefilter.getTime());
2627

2728
const [absoluteTimeRange, setAbsoluteTimeRange] = useState(() => timefilter.getAbsoluteTime());
29+
const absoluteTimeRangeRef = useRef(absoluteTimeRange);
2830

2931
useEffect(() => {
3032
const timeUpdateSubscription = timefilter.getTimeUpdate$().subscribe({
3133
next: () => {
3234
setTimeRange(() => timefilter.getTime());
35+
const newAbsoluteTimeRange = timefilter.getAbsoluteTime();
36+
absoluteTimeRangeRef.current = newAbsoluteTimeRange;
3337
setAbsoluteTimeRange(() => timefilter.getAbsoluteTime());
3438
},
3539
});
@@ -51,6 +55,19 @@ export function createUseTimefilterHook(timefilter: Timefilter) {
5155
[]
5256
);
5357

58+
const refreshAbsoluteTimeRange = useCallback(() => {
59+
const newAbsoluteTimeRange = timefilter.getAbsoluteTime();
60+
if (
61+
newAbsoluteTimeRange.from !== absoluteTimeRangeRef.current.from ||
62+
newAbsoluteTimeRange.to !== absoluteTimeRangeRef.current.to
63+
) {
64+
setAbsoluteTimeRange(newAbsoluteTimeRange);
65+
absoluteTimeRangeRef.current = newAbsoluteTimeRange;
66+
return true;
67+
}
68+
return false;
69+
}, []);
70+
5471
const asEpoch = useMemo(() => {
5572
return {
5673
start: new Date(absoluteTimeRange.from).getTime(),
@@ -62,6 +79,7 @@ export function createUseTimefilterHook(timefilter: Timefilter) {
6279
timeRange,
6380
absoluteTimeRange: asEpoch,
6481
setTimeRange: setTimeRangeMemoized,
82+
refreshAbsoluteTimeRange,
6583
};
6684
};
6785
}

‎src/platform/plugins/shared/unified_histogram/public/services/lens_vis_service.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
getLensAttributesFromSuggestion,
3333
ChartType,
3434
mapVisToChartType,
35+
computeInterval,
3536
} from '@kbn/visualization-utils';
3637
import { LegendSize } from '@kbn/visualizations-plugin/public';
3738
import { XYConfiguration } from '@kbn/visualizations-plugin/common';
@@ -51,7 +52,6 @@ import {
5152
injectESQLQueryIntoLensLayers,
5253
TIMESTAMP_COLUMN,
5354
} from '../utils/external_vis_context';
54-
import { computeInterval } from '../utils/compute_interval';
5555
import { enrichLensAttributesWithTablesData } from '../utils/lens_vis_from_table';
5656

5757
const UNIFIED_HISTOGRAM_LAYER_ID = 'unifiedHistogram';

‎x-pack/platform/plugins/private/translations/translations/fr-FR.json

-1
Original file line numberDiff line numberDiff line change
@@ -44811,7 +44811,6 @@
4481144811
"xpack.streams.dashboardTable.dashboardNameColumnTitle": "Nom du tableau de bord",
4481244812
"xpack.streams.dashboardTable.tagsColumnTitle": "Balises",
4481344813
"xpack.streams.entityDetailOverview.createChildStream": "Créer un flux enfant",
44814-
"xpack.streams.entityDetailOverview.docCount": "{docCount} documents",
4481544814
"xpack.streams.entityDetailOverview.noChildStreams": "Créer des sous-flux pour diviser les données ayant des différences de politiques de conservation, de schémas, etc.",
4481644815
"xpack.streams.entityDetailOverview.searchBarPlaceholder": "Filtrer les données avec KQL",
4481744816
"xpack.streams.entityDetailOverview.tabs.quicklinks": "Liens rapides",

‎x-pack/platform/plugins/private/translations/translations/zh-CN.json

-1
Original file line numberDiff line numberDiff line change
@@ -44848,7 +44848,6 @@
4484844848
"xpack.streams.dashboardTable.dashboardNameColumnTitle": "仪表板名称",
4484944849
"xpack.streams.dashboardTable.tagsColumnTitle": "标签",
4485044850
"xpack.streams.entityDetailOverview.createChildStream": "创建子数据流",
44851-
"xpack.streams.entityDetailOverview.docCount": "{docCount} 个文档",
4485244851
"xpack.streams.entityDetailOverview.noChildStreams": "创建子流以分割具有不同保留策略、方案等的数据。",
4485344852
"xpack.streams.entityDetailOverview.searchBarPlaceholder": "通过使用 KQL 来筛选数据",
4485444853
"xpack.streams.entityDetailOverview.tabs.quicklinks": "快速链接",

‎x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_lifecycle/helpers/format_bytes.ts

+15
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,20 @@
66
*/
77

88
import { formatNumber } from '@elastic/eui';
9+
import { i18n } from '@kbn/i18n';
910

1011
export const formatBytes = (value: number) => formatNumber(value, '0.0 b');
12+
13+
export const formatIngestionRate = (bytesPerDay: number, perDayOnly = false) => {
14+
const perDay = formatBytes(bytesPerDay);
15+
const perMonth = formatBytes(bytesPerDay * 30);
16+
if (perDayOnly)
17+
return i18n.translate('xpack.streams.streamDetailOverview.ingestionRatePerDay', {
18+
defaultMessage: '{perDay} / Day',
19+
values: { perDay },
20+
});
21+
return i18n.translate('xpack.streams.streamDetailOverview.ingestionRatePerDayPerMonth', {
22+
defaultMessage: '{perDay} / Day ({perMonth} / Month)',
23+
values: { perDay, perMonth },
24+
});
25+
};

‎x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_lifecycle/metadata.tsx

+1-7
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import { LifecycleEditAction } from './modal';
3838
import { IlmLink } from './ilm_link';
3939
import { useStreamsAppRouter } from '../../../hooks/use_streams_app_router';
4040
import { DataStreamStats } from './hooks/use_data_stream_stats';
41-
import { formatBytes } from './helpers/format_bytes';
41+
import { formatIngestionRate } from './helpers/format_bytes';
4242

4343
export function RetentionMetadata({
4444
definition,
@@ -243,9 +243,3 @@ function MetadataRow({
243243
</EuiFlexGroup>
244244
);
245245
}
246-
247-
const formatIngestionRate = (bytesPerDay: number) => {
248-
const perDay = formatBytes(bytesPerDay);
249-
const perMonth = formatBytes(bytesPerDay * 30);
250-
return `${perDay} / Day - ${perMonth} / Month`;
251-
};

‎x-pack/platform/plugins/shared/streams_app/public/components/entity_detail_view/index.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface EntityViewTab {
3030
name: string;
3131
label: string;
3232
content: React.ReactElement;
33+
background: boolean;
3334
}
3435

3536
export function EntityDetailViewWithoutParams({
@@ -75,6 +76,7 @@ export function EntityDetailViewWithoutParams({
7576
}),
7677
label: tab.label,
7778
content: tab.content,
79+
background: tab.background,
7880
},
7981
];
8082
})
@@ -126,7 +128,9 @@ export function EntityDetailViewWithoutParams({
126128
/>
127129
</StreamsAppPageHeader>
128130
</EuiFlexItem>
129-
<StreamsAppPageBody>{selectedTabObject.content}</StreamsAppPageBody>
131+
<StreamsAppPageBody background={selectedTabObject.background}>
132+
{selectedTabObject.content}
133+
</StreamsAppPageBody>
130134
</EuiFlexGroup>
131135
);
132136
}

‎x-pack/platform/plugins/shared/streams_app/public/components/esql_chart/controlled_esql_chart.tsx

+12-6
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,14 @@ export function ControlledEsqlChart<T extends string>({
6262
metricNames,
6363
chartType = 'line',
6464
height,
65+
timerange,
6566
}: {
6667
id: string;
6768
result: AbortableAsyncState<UnparsedEsqlResponse>;
6869
metricNames: T[];
6970
chartType?: 'area' | 'bar' | 'line';
70-
height: number;
71+
height?: number;
72+
timerange?: { start: number; end: number };
7173
}) {
7274
const {
7375
core: { uiSettings },
@@ -84,21 +86,24 @@ export function ControlledEsqlChart<T extends string>({
8486
[result, ...metricNames]
8587
);
8688

89+
const effectiveHeight = height ? `${height}px` : '100%';
90+
8791
if (result.loading && !result.value?.values.length) {
8892
return (
8993
<LoadingPanel
9094
loading
9195
className={css`
92-
height: ${height}px;
96+
height: ${effectiveHeight};
9397
`}
9498
/>
9599
);
96100
}
97101

98102
const xValues = allTimeseries.flatMap(({ data }) => data.map(({ x }) => x));
99103

100-
const min = Math.min(...xValues);
101-
const max = Math.max(...xValues);
104+
// todo - pull in time range here
105+
const min = timerange?.start ?? Math.min(...xValues);
106+
const max = timerange?.end ?? Math.max(...xValues);
102107

103108
const isEmpty = min === 0 && max === 0;
104109

@@ -115,7 +120,7 @@ export function ControlledEsqlChart<T extends string>({
115120
<Chart
116121
id={id}
117122
className={css`
118-
height: ${height}px;
123+
height: ${effectiveHeight};
119124
`}
120125
>
121126
<Tooltip
@@ -146,7 +151,7 @@ export function ControlledEsqlChart<T extends string>({
146151
}}
147152
/>
148153
<Settings
149-
showLegend
154+
showLegend={false}
150155
legendPosition={Position.Bottom}
151156
xDomain={xDomain}
152157
locale={i18n.getLocale()}
@@ -173,6 +178,7 @@ export function ControlledEsqlChart<T extends string>({
173178
<Series
174179
timeZone={timeZone}
175180
key={serie.id}
181+
color="#61A2FF"
176182
id={serie.id}
177183
xScaleType={ScaleType.Time}
178184
yScaleType={ScaleType.Linear}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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+
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
8+
import { i18n } from '@kbn/i18n';
9+
import React, { useMemo } from 'react';
10+
import { css } from '@emotion/css';
11+
import { IngestStreamGetResponse, isDescendantOf } from '@kbn/streams-schema';
12+
13+
import { useStreamsAppRouter } from '../../hooks/use_streams_app_router';
14+
import { AssetImage } from '../asset_image';
15+
import { StreamsList } from '../streams_list';
16+
import { useWiredStreams } from '../../hooks/use_wired_streams';
17+
18+
export function ChildStreamList({ definition }: { definition?: IngestStreamGetResponse }) {
19+
const router = useStreamsAppRouter();
20+
21+
const { wiredStreams } = useWiredStreams();
22+
23+
const childrenStreams = useMemo(() => {
24+
if (!definition) {
25+
return [];
26+
}
27+
return wiredStreams?.filter((d) => isDescendantOf(definition.stream.name, d.name));
28+
}, [definition, wiredStreams]);
29+
30+
if (definition && childrenStreams?.length === 0) {
31+
return (
32+
<EuiFlexItem grow>
33+
<EuiFlexGroup alignItems="center" justifyContent="center">
34+
<EuiFlexItem
35+
grow={false}
36+
className={css`
37+
max-width: 350px;
38+
`}
39+
>
40+
<EuiFlexGroup direction="column" gutterSize="s">
41+
<AssetImage type="welcome" />
42+
<EuiText size="m" textAlign="center">
43+
{i18n.translate('xpack.streams.entityDetailOverview.noChildStreams', {
44+
defaultMessage: 'Create streams for your logs',
45+
})}
46+
</EuiText>
47+
<EuiText size="xs" textAlign="center">
48+
{i18n.translate('xpack.streams.entityDetailOverview.noChildStreams', {
49+
defaultMessage:
50+
'Create sub streams to split out data with different retention policies, schemas, and more.',
51+
})}
52+
</EuiText>
53+
<EuiFlexGroup justifyContent="center">
54+
<EuiButton
55+
data-test-subj="streamsAppChildStreamListCreateChildStreamButton"
56+
iconType="plusInCircle"
57+
href={router.link('/{key}/management/{subtab}', {
58+
path: {
59+
key: definition?.stream.name,
60+
subtab: 'route',
61+
},
62+
})}
63+
>
64+
{i18n.translate('xpack.streams.entityDetailOverview.createChildStream', {
65+
defaultMessage: 'Create child stream',
66+
})}
67+
</EuiButton>
68+
</EuiFlexGroup>
69+
</EuiFlexGroup>
70+
</EuiFlexItem>
71+
</EuiFlexGroup>
72+
</EuiFlexItem>
73+
);
74+
}
75+
76+
return <StreamsList streams={childrenStreams} showControls={false} />;
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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+
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui';
8+
import { css } from '@emotion/css';
9+
import { i18n } from '@kbn/i18n';
10+
import React from 'react';
11+
12+
import { AbortableAsyncState } from '@kbn/react-hooks';
13+
import type { UnparsedEsqlResponse } from '@kbn/traced-es-client';
14+
import { ControlledEsqlChart } from '../../esql_chart/controlled_esql_chart';
15+
16+
interface StreamChartPanelProps {
17+
histogramQueryFetch: AbortableAsyncState<UnparsedEsqlResponse | undefined>;
18+
discoverLink?: string;
19+
timerange: {
20+
start: number;
21+
end: number;
22+
};
23+
}
24+
25+
export function StreamChartPanel({
26+
histogramQueryFetch,
27+
discoverLink,
28+
timerange,
29+
}: StreamChartPanelProps) {
30+
return (
31+
<EuiPanel hasShadow={false} hasBorder>
32+
<EuiFlexGroup
33+
direction="column"
34+
className={css`
35+
height: 100%;
36+
`}
37+
>
38+
<EuiFlexItem grow={false}>
39+
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
40+
<EuiFlexItem grow={false}>
41+
<EuiText size="s">
42+
{i18n.translate('xpack.streams.streamDetailOverview.logRate', {
43+
defaultMessage: 'Documents',
44+
})}
45+
</EuiText>
46+
</EuiFlexItem>
47+
<EuiButtonEmpty
48+
data-test-subj="streamsDetailOverviewOpenInDiscoverButton"
49+
iconType="discoverApp"
50+
href={discoverLink}
51+
isDisabled={!discoverLink}
52+
>
53+
{i18n.translate('xpack.streams.streamDetailOverview.openInDiscoverButtonLabel', {
54+
defaultMessage: 'Open in Discover',
55+
})}
56+
</EuiButtonEmpty>
57+
</EuiFlexGroup>
58+
</EuiFlexItem>
59+
<EuiFlexItem grow>
60+
<ControlledEsqlChart
61+
result={histogramQueryFetch}
62+
id="entity_log_rate"
63+
metricNames={['metric']}
64+
chartType={'bar'}
65+
timerange={timerange}
66+
/>
67+
</EuiFlexItem>
68+
</EuiFlexGroup>
69+
</EuiPanel>
70+
);
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
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+
import {
8+
EuiFlexGroup,
9+
EuiFlexItem,
10+
EuiIconTip,
11+
EuiPanel,
12+
EuiText,
13+
formatNumber,
14+
useEuiTheme,
15+
} from '@elastic/eui';
16+
import { css } from '@emotion/css';
17+
import { i18n } from '@kbn/i18n';
18+
import React, { ReactNode } from 'react';
19+
import { IngestStreamGetResponse, IngestStreamLifecycleILM } from '@kbn/streams-schema';
20+
import { IlmLocatorParams } from '@kbn/index-lifecycle-management-common-shared';
21+
22+
import { LocatorPublic } from '@kbn/share-plugin/public';
23+
import type { StreamDetailsResponse } from '@kbn/streams-plugin/server/routes/internal/streams/crud/route';
24+
import { IlmLink } from '../../data_management/stream_detail_lifecycle/ilm_link';
25+
import {
26+
formatBytes,
27+
formatIngestionRate,
28+
} from '../../data_management/stream_detail_lifecycle/helpers/format_bytes';
29+
import { DataStreamStats } from '../../data_management/stream_detail_lifecycle/hooks/use_data_stream_stats';
30+
31+
interface StreamStatsPanelProps {
32+
definition?: IngestStreamGetResponse;
33+
dataStreamStats?: DataStreamStats;
34+
docCount?: StreamDetailsResponse;
35+
ilmLocator?: LocatorPublic<IlmLocatorParams>;
36+
}
37+
38+
const RetentionDisplay = ({
39+
definition,
40+
ilmLocator,
41+
}: {
42+
definition?: IngestStreamGetResponse;
43+
ilmLocator?: LocatorPublic<IlmLocatorParams>;
44+
}) => {
45+
if (!definition) return <>-</>;
46+
47+
if ('dsl' in definition.effective_lifecycle) {
48+
return (
49+
<>
50+
{definition?.effective_lifecycle.dsl.data_retention ||
51+
i18n.translate('xpack.streams.entityDetailOverview.unlimited', {
52+
defaultMessage: 'Keep indefinitely',
53+
})}
54+
</>
55+
);
56+
}
57+
58+
return (
59+
<IlmLink
60+
lifecycle={definition.effective_lifecycle as IngestStreamLifecycleILM}
61+
ilmLocator={ilmLocator}
62+
/>
63+
);
64+
};
65+
66+
interface StatItemProps {
67+
label: ReactNode;
68+
value: ReactNode;
69+
withBorder?: boolean;
70+
}
71+
72+
const StatItem = ({ label, value, withBorder = false }: StatItemProps) => {
73+
const { euiTheme } = useEuiTheme();
74+
75+
const borderStyle = withBorder
76+
? css`
77+
border-left: 1px solid ${euiTheme.colors.borderBaseSubdued};
78+
padding-left: ${euiTheme.size.s};
79+
`
80+
: '';
81+
82+
return (
83+
<EuiFlexItem grow className={borderStyle}>
84+
<EuiFlexGroup direction="column" gutterSize="xs">
85+
<EuiText size="xs" color="subdued">
86+
{label}
87+
</EuiText>
88+
<EuiText
89+
size="m"
90+
className={css`
91+
font-weight: bold;
92+
`}
93+
>
94+
{value}
95+
</EuiText>
96+
</EuiFlexGroup>
97+
</EuiFlexItem>
98+
);
99+
};
100+
101+
export function StreamStatsPanel({
102+
definition,
103+
dataStreamStats,
104+
docCount,
105+
ilmLocator,
106+
}: StreamStatsPanelProps) {
107+
const retentionLabel = i18n.translate('xpack.streams.entityDetailOverview.retention', {
108+
defaultMessage: 'Data retention',
109+
});
110+
111+
const documentCountLabel = i18n.translate('xpack.streams.entityDetailOverview.count', {
112+
defaultMessage: 'Document count',
113+
});
114+
115+
const storageSizeLabel = i18n.translate('xpack.streams.entityDetailOverview.size', {
116+
defaultMessage: 'Storage size',
117+
});
118+
119+
const ingestionLabel = i18n.translate('xpack.streams.entityDetailOverview.ingestion', {
120+
defaultMessage: 'Ingestion',
121+
});
122+
123+
return (
124+
<EuiFlexGroup direction="row" gutterSize="s">
125+
<EuiFlexItem grow={3}>
126+
<EuiPanel hasShadow={false} hasBorder>
127+
<EuiFlexGroup direction="column" gutterSize="xs">
128+
<EuiText size="xs" color="subdued">
129+
{retentionLabel}
130+
</EuiText>
131+
<EuiText size="m">
132+
<RetentionDisplay definition={definition} ilmLocator={ilmLocator} />
133+
</EuiText>
134+
</EuiFlexGroup>
135+
</EuiPanel>
136+
</EuiFlexItem>
137+
<EuiFlexItem grow={9}>
138+
<EuiPanel hasShadow={false} hasBorder>
139+
<EuiFlexGroup>
140+
<StatItem
141+
label={documentCountLabel}
142+
value={docCount ? formatNumber(docCount.details.count || 0, 'decimal0') : '-'}
143+
/>
144+
<StatItem
145+
label={
146+
<>
147+
{storageSizeLabel}
148+
<EuiIconTip
149+
content={i18n.translate('xpack.streams.streamDetailOverview.sizeTip', {
150+
defaultMessage:
151+
'Estimated size based on the number of documents in the current time range and the total size of the stream.',
152+
})}
153+
position="right"
154+
/>
155+
</>
156+
}
157+
value={
158+
dataStreamStats && docCount
159+
? formatBytes(getStorageSizeForTimeRange(dataStreamStats, docCount))
160+
: '-'
161+
}
162+
withBorder
163+
/>
164+
<StatItem
165+
label={
166+
<>
167+
{ingestionLabel}
168+
<EuiIconTip
169+
content={i18n.translate(
170+
'xpack.streams.streamDetailLifecycle.ingestionRateDetails',
171+
{
172+
defaultMessage:
173+
'Estimated average (stream total size divided by the number of days since creation).',
174+
}
175+
)}
176+
position="right"
177+
/>
178+
</>
179+
}
180+
value={
181+
dataStreamStats ? formatIngestionRate(dataStreamStats.bytesPerDay || 0, true) : '-'
182+
}
183+
withBorder
184+
/>
185+
</EuiFlexGroup>
186+
</EuiPanel>
187+
</EuiFlexItem>
188+
</EuiFlexGroup>
189+
);
190+
}
191+
192+
function getStorageSizeForTimeRange(
193+
dataStreamStats: DataStreamStats,
194+
docCount: StreamDetailsResponse
195+
) {
196+
const storageSize = dataStreamStats.sizeBytes;
197+
const totalCount = dataStreamStats.totalDocs;
198+
const countForTimeRange = docCount.details.count;
199+
if (!storageSize || !totalCount || !countForTimeRange) {
200+
return 0;
201+
}
202+
const bytesPerDoc = totalCount ? storageSize / totalCount : 0;
203+
return bytesPerDoc * countForTimeRange;
204+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
import { EuiFlexGroup, EuiPanel, EuiTab, EuiTabs } from '@elastic/eui';
8+
import { css } from '@emotion/css';
9+
import React, { useState, ReactNode } from 'react';
10+
11+
interface Tab {
12+
id: string;
13+
name: string;
14+
content: ReactNode;
15+
}
16+
17+
interface TabsPanelProps {
18+
tabs: Tab[];
19+
}
20+
21+
export function TabsPanel({ tabs }: TabsPanelProps) {
22+
const [selectedTab, setSelectedTab] = useState<string | undefined>(undefined);
23+
24+
if (tabs.length === 0) {
25+
return null;
26+
}
27+
28+
return (
29+
<EuiPanel hasShadow={false} hasBorder>
30+
<EuiFlexGroup
31+
direction="column"
32+
gutterSize="s"
33+
className={css`
34+
height: 100%;
35+
`}
36+
>
37+
{tabs.length === 1 ? (
38+
tabs[0].content
39+
) : (
40+
<>
41+
<EuiTabs>
42+
{tabs.map((tab, index) => (
43+
<EuiTab
44+
isSelected={(!selectedTab && index === 0) || selectedTab === tab.id}
45+
onClick={() => setSelectedTab(tab.id)}
46+
key={tab.id}
47+
>
48+
{tab.name}
49+
</EuiTab>
50+
))}
51+
</EuiTabs>
52+
{
53+
tabs.find((tab, index) => (!selectedTab && index === 0) || selectedTab === tab.id)
54+
?.content
55+
}
56+
</>
57+
)}
58+
</EuiFlexGroup>
59+
</EuiPanel>
60+
);
61+
}

‎x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_overview/index.tsx

+1-344
Original file line numberDiff line numberDiff line change
@@ -4,348 +4,5 @@
44
* 2.0; you may not use this file except in compliance with the Elastic License
55
* 2.0.
66
*/
7-
import {
8-
EuiButton,
9-
EuiFlexGroup,
10-
EuiFlexItem,
11-
EuiLoadingSpinner,
12-
EuiPanel,
13-
EuiTab,
14-
EuiTabs,
15-
EuiText,
16-
} from '@elastic/eui';
17-
import { calculateAuto } from '@kbn/calculate-auto';
18-
import { i18n } from '@kbn/i18n';
19-
import moment from 'moment';
20-
import React, { useMemo } from 'react';
21-
import { css } from '@emotion/css';
22-
import {
23-
IngestStreamGetResponse,
24-
isDescendantOf,
25-
isUnwiredStreamGetResponse,
26-
isWiredStreamDefinition,
27-
} from '@kbn/streams-schema';
28-
import type { SanitizedDashboardAsset } from '@kbn/streams-plugin/server/routes/dashboards/route';
29-
import { useKibana } from '../../hooks/use_kibana';
30-
import { useStreamsAppFetch } from '../../hooks/use_streams_app_fetch';
31-
import { ControlledEsqlChart } from '../esql_chart/controlled_esql_chart';
32-
import { StreamsAppSearchBar } from '../streams_app_search_bar';
33-
import { getIndexPatterns } from '../../util/hierarchy_helpers';
34-
import { StreamsList } from '../streams_list';
35-
import { useStreamsAppRouter } from '../../hooks/use_streams_app_router';
36-
import { useDashboardsFetch } from '../../hooks/use_dashboards_fetch';
37-
import { DashboardsTable } from '../stream_detail_dashboards_view/dashboard_table';
38-
import { AssetImage } from '../asset_image';
39-
import { useWiredStreams } from '../../hooks/use_wired_streams';
407

41-
const formatNumber = (val: number) => {
42-
return Number(val).toLocaleString('en', {
43-
maximumFractionDigits: 1,
44-
});
45-
};
46-
47-
export function StreamDetailOverview({ definition }: { definition?: IngestStreamGetResponse }) {
48-
const {
49-
dependencies: {
50-
start: {
51-
data,
52-
dataViews,
53-
streams: { streamsRepositoryClient },
54-
share,
55-
},
56-
},
57-
} = useKibana();
58-
59-
const {
60-
timeRange,
61-
setTimeRange,
62-
absoluteTimeRange: { start, end },
63-
} = data.query.timefilter.timefilter.useTimefilter();
64-
65-
const indexPatterns = useMemo(() => {
66-
return getIndexPatterns(definition?.stream);
67-
}, [definition]);
68-
69-
const discoverLocator = useMemo(
70-
() => share.url.locators.get('DISCOVER_APP_LOCATOR'),
71-
[share.url.locators]
72-
);
73-
74-
const queries = useMemo(() => {
75-
if (!indexPatterns) {
76-
return undefined;
77-
}
78-
79-
const baseQuery = `FROM ${indexPatterns.join(', ')}`;
80-
81-
const bucketSize = Math.round(
82-
calculateAuto.atLeast(50, moment.duration(1, 'minute'))!.asSeconds()
83-
);
84-
85-
const histogramQuery = `${baseQuery} | STATS metric = COUNT(*) BY @timestamp = BUCKET(@timestamp, ${bucketSize} seconds)`;
86-
87-
return {
88-
baseQuery,
89-
histogramQuery,
90-
};
91-
}, [indexPatterns]);
92-
93-
const discoverLink = useMemo(() => {
94-
if (!discoverLocator || !queries?.baseQuery) {
95-
return undefined;
96-
}
97-
98-
return discoverLocator.getRedirectUrl({
99-
query: {
100-
esql: queries.baseQuery,
101-
},
102-
});
103-
}, [queries?.baseQuery, discoverLocator]);
104-
105-
const histogramQueryFetch = useStreamsAppFetch(
106-
async ({ signal }) => {
107-
if (!queries?.histogramQuery || !indexPatterns) {
108-
return undefined;
109-
}
110-
111-
const existingIndices = await dataViews.getExistingIndices(indexPatterns);
112-
113-
if (existingIndices.length === 0) {
114-
return undefined;
115-
}
116-
117-
return streamsRepositoryClient.fetch('POST /internal/streams/esql', {
118-
params: {
119-
body: {
120-
operationName: 'get_histogram_for_stream',
121-
query: queries.histogramQuery,
122-
start,
123-
end,
124-
},
125-
},
126-
signal,
127-
});
128-
},
129-
[indexPatterns, dataViews, streamsRepositoryClient, queries?.histogramQuery, start, end]
130-
);
131-
132-
const docCountFetch = useStreamsAppFetch(
133-
async ({ signal }) => {
134-
if (
135-
!definition ||
136-
(isUnwiredStreamGetResponse(definition) && !definition.data_stream_exists)
137-
) {
138-
return undefined;
139-
}
140-
return streamsRepositoryClient.fetch('GET /internal/streams/{name}/_details', {
141-
signal,
142-
params: {
143-
path: {
144-
name: definition.stream.name,
145-
},
146-
query: {
147-
start: String(start),
148-
end: String(end),
149-
},
150-
},
151-
});
152-
},
153-
154-
[definition, dataViews, streamsRepositoryClient, start, end]
155-
);
156-
157-
const [selectedTab, setSelectedTab] = React.useState<string | undefined>(undefined);
158-
159-
const tabs = [
160-
...(definition && isWiredStreamDefinition(definition.stream)
161-
? [
162-
{
163-
id: 'streams',
164-
name: i18n.translate('xpack.streams.entityDetailOverview.tabs.streams', {
165-
defaultMessage: 'Streams',
166-
}),
167-
content: <ChildStreamList definition={definition} />,
168-
},
169-
]
170-
: []),
171-
{
172-
id: 'quicklinks',
173-
name: i18n.translate('xpack.streams.entityDetailOverview.tabs.quicklinks', {
174-
defaultMessage: 'Quick Links',
175-
}),
176-
content: <QuickLinks definition={definition} />,
177-
},
178-
];
179-
180-
return (
181-
<>
182-
<EuiFlexGroup direction="column">
183-
<EuiFlexItem grow={false}>
184-
<EuiFlexGroup direction="row" gutterSize="s" alignItems="center">
185-
<EuiFlexItem>
186-
{docCountFetch.loading ? (
187-
<EuiLoadingSpinner size="m" />
188-
) : (
189-
docCountFetch.value && (
190-
<EuiText>
191-
{i18n.translate('xpack.streams.entityDetailOverview.docCount', {
192-
defaultMessage: '{docCount} documents',
193-
values: { docCount: formatNumber(docCountFetch.value.details.count) },
194-
})}
195-
</EuiText>
196-
)
197-
)}
198-
</EuiFlexItem>
199-
<EuiFlexItem grow>
200-
<StreamsAppSearchBar
201-
onQuerySubmit={({ dateRange }, isUpdate) => {
202-
if (!isUpdate) {
203-
histogramQueryFetch.refresh();
204-
docCountFetch.refresh();
205-
return;
206-
}
207-
208-
if (dateRange) {
209-
setTimeRange({ from: dateRange.from, to: dateRange?.to, mode: dateRange.mode });
210-
}
211-
}}
212-
onRefresh={() => {
213-
histogramQueryFetch.refresh();
214-
}}
215-
placeholder={i18n.translate(
216-
'xpack.streams.entityDetailOverview.searchBarPlaceholder',
217-
{
218-
defaultMessage: 'Filter data by using KQL',
219-
}
220-
)}
221-
dateRangeFrom={timeRange.from}
222-
dateRangeTo={timeRange.to}
223-
/>
224-
</EuiFlexItem>
225-
<EuiButton
226-
data-test-subj="streamsDetailOverviewOpenInDiscoverButton"
227-
iconType="discoverApp"
228-
href={discoverLink}
229-
color="text"
230-
>
231-
{i18n.translate('xpack.streams.streamDetailOverview.openInDiscoverButtonLabel', {
232-
defaultMessage: 'Open in Discover',
233-
})}
234-
</EuiButton>
235-
</EuiFlexGroup>
236-
</EuiFlexItem>
237-
<EuiFlexItem grow={false}>
238-
<EuiPanel hasShadow={false} hasBorder>
239-
<EuiFlexGroup direction="column">
240-
<ControlledEsqlChart
241-
result={histogramQueryFetch}
242-
id="entity_log_rate"
243-
metricNames={['metric']}
244-
height={200}
245-
chartType={'bar'}
246-
/>
247-
</EuiFlexGroup>
248-
</EuiPanel>
249-
</EuiFlexItem>
250-
<EuiFlexItem grow>
251-
<EuiFlexGroup direction="column" gutterSize="s">
252-
{definition && (
253-
<>
254-
<EuiTabs>
255-
{tabs.map((tab, index) => (
256-
<EuiTab
257-
isSelected={(!selectedTab && index === 0) || selectedTab === tab.id}
258-
onClick={() => setSelectedTab(tab.id)}
259-
key={tab.id}
260-
>
261-
{tab.name}
262-
</EuiTab>
263-
))}
264-
</EuiTabs>
265-
{
266-
tabs.find((tab, index) => (!selectedTab && index === 0) || selectedTab === tab.id)
267-
?.content
268-
}
269-
</>
270-
)}
271-
</EuiFlexGroup>
272-
</EuiFlexItem>
273-
</EuiFlexGroup>
274-
</>
275-
);
276-
}
277-
278-
const EMPTY_DASHBOARD_LIST: SanitizedDashboardAsset[] = [];
279-
280-
function QuickLinks({ definition }: { definition?: IngestStreamGetResponse }) {
281-
const dashboardsFetch = useDashboardsFetch(definition?.stream.name);
282-
283-
return (
284-
<DashboardsTable
285-
entityId={definition?.stream.name}
286-
dashboards={dashboardsFetch.value?.dashboards ?? EMPTY_DASHBOARD_LIST}
287-
loading={dashboardsFetch.loading}
288-
/>
289-
);
290-
}
291-
292-
function ChildStreamList({ definition }: { definition?: IngestStreamGetResponse }) {
293-
const router = useStreamsAppRouter();
294-
295-
const { wiredStreams } = useWiredStreams();
296-
297-
const childrenStreams = useMemo(() => {
298-
if (!definition) {
299-
return [];
300-
}
301-
return wiredStreams?.filter((d) => isDescendantOf(definition.stream.name, d.name));
302-
}, [definition, wiredStreams]);
303-
304-
if (definition && childrenStreams?.length === 0) {
305-
return (
306-
<EuiFlexItem grow>
307-
<EuiFlexGroup alignItems="center" justifyContent="center">
308-
<EuiFlexItem
309-
grow={false}
310-
className={css`
311-
max-width: 350px;
312-
`}
313-
>
314-
<EuiFlexGroup direction="column" gutterSize="s">
315-
<AssetImage type="welcome" />
316-
<EuiText size="m" textAlign="center">
317-
{i18n.translate('xpack.streams.entityDetailOverview.noChildStreams', {
318-
defaultMessage: 'Create streams for your logs',
319-
})}
320-
</EuiText>
321-
<EuiText size="xs" textAlign="center">
322-
{i18n.translate('xpack.streams.entityDetailOverview.noChildStreams', {
323-
defaultMessage:
324-
'Create sub streams to split out data with different retention policies, schemas, and more.',
325-
})}
326-
</EuiText>
327-
<EuiFlexGroup justifyContent="center">
328-
<EuiButton
329-
data-test-subj="streamsAppChildStreamListCreateChildStreamButton"
330-
iconType="plusInCircle"
331-
href={router.link('/{key}/management/{subtab}', {
332-
path: {
333-
key: definition?.stream.name,
334-
subtab: 'route',
335-
},
336-
})}
337-
>
338-
{i18n.translate('xpack.streams.entityDetailOverview.createChildStream', {
339-
defaultMessage: 'Create child stream',
340-
})}
341-
</EuiButton>
342-
</EuiFlexGroup>
343-
</EuiFlexGroup>
344-
</EuiFlexItem>
345-
</EuiFlexGroup>
346-
</EuiFlexItem>
347-
);
348-
}
349-
350-
return <StreamsList streams={childrenStreams} showControls={false} />;
351-
}
8+
export { StreamDetailOverview } from './stream_detail_overview';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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+
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText } from '@elastic/eui';
8+
import { i18n } from '@kbn/i18n';
9+
import React from 'react';
10+
import { css } from '@emotion/css';
11+
import { IngestStreamGetResponse } from '@kbn/streams-schema';
12+
import type { SanitizedDashboardAsset } from '@kbn/streams-plugin/server/routes/dashboards/route';
13+
14+
import { useDashboardsFetch } from '../../hooks/use_dashboards_fetch';
15+
import { AssetImage } from '../asset_image';
16+
import { useStreamsAppRouter } from '../../hooks/use_streams_app_router';
17+
import { DashboardsTable } from '../stream_detail_dashboards_view/dashboard_table';
18+
19+
const EMPTY_DASHBOARD_LIST: SanitizedDashboardAsset[] = [];
20+
21+
export function QuickLinks({ definition }: { definition?: IngestStreamGetResponse }) {
22+
const router = useStreamsAppRouter();
23+
const dashboardsFetch = useDashboardsFetch(definition?.stream.name);
24+
25+
if (definition && !dashboardsFetch.loading && dashboardsFetch.value?.dashboards.length === 0) {
26+
return (
27+
<EuiFlexItem grow>
28+
<EuiFlexGroup alignItems="center" justifyContent="center">
29+
<EuiFlexItem
30+
grow={false}
31+
className={css`
32+
max-width: 200px;
33+
`}
34+
>
35+
<EuiFlexGroup direction="column" gutterSize="s">
36+
<AssetImage type="welcome" />
37+
<EuiText size="xs" textAlign="center" color="subdued">
38+
{i18n.translate('xpack.streams.entityDetailOverview.linkDashboardsText', {
39+
defaultMessage: 'Link dashboards to this stream for quick access',
40+
})}
41+
</EuiText>
42+
<EuiFlexGroup justifyContent="center">
43+
<EuiLink
44+
href={router.link('/{key}/{tab}', {
45+
path: {
46+
key: definition?.stream.name,
47+
tab: 'dashboards',
48+
},
49+
})}
50+
>
51+
{i18n.translate('xpack.streams.entityDetailOverview.addDashboardButton', {
52+
defaultMessage: 'Add dashboards',
53+
})}
54+
</EuiLink>
55+
</EuiFlexGroup>
56+
</EuiFlexGroup>
57+
</EuiFlexItem>
58+
</EuiFlexGroup>
59+
</EuiFlexItem>
60+
);
61+
}
62+
63+
return (
64+
<DashboardsTable
65+
dashboards={dashboardsFetch.value?.dashboards ?? EMPTY_DASHBOARD_LIST}
66+
loading={dashboardsFetch.loading}
67+
/>
68+
);
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
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+
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
8+
import { i18n } from '@kbn/i18n';
9+
import React, { useMemo } from 'react';
10+
import { IngestStreamGetResponse, isWiredStreamDefinition } from '@kbn/streams-schema';
11+
import { ILM_LOCATOR_ID, IlmLocatorParams } from '@kbn/index-lifecycle-management-common-shared';
12+
13+
import { computeInterval } from '@kbn/visualization-utils';
14+
import { useKibana } from '../../hooks/use_kibana';
15+
import { useStreamsAppFetch } from '../../hooks/use_streams_app_fetch';
16+
import { StreamsAppSearchBar } from '../streams_app_search_bar';
17+
import { getIndexPatterns } from '../../util/hierarchy_helpers';
18+
import { useDataStreamStats } from '../data_management/stream_detail_lifecycle/hooks/use_data_stream_stats';
19+
import { QuickLinks } from './quick_links';
20+
import { ChildStreamList } from './child_stream_list';
21+
import { StreamStatsPanel } from './components/stream_stats_panel';
22+
import { StreamChartPanel } from './components/stream_chart_panel';
23+
import { TabsPanel } from './components/tabs_panel';
24+
25+
export function StreamDetailOverview({ definition }: { definition?: IngestStreamGetResponse }) {
26+
const {
27+
dependencies: {
28+
start: {
29+
data,
30+
dataViews,
31+
streams: { streamsRepositoryClient },
32+
share,
33+
},
34+
},
35+
} = useKibana();
36+
37+
const {
38+
timeRange,
39+
setTimeRange,
40+
absoluteTimeRange: { start, end },
41+
refreshAbsoluteTimeRange,
42+
} = data.query.timefilter.timefilter.useTimefilter();
43+
44+
const indexPatterns = useMemo(() => {
45+
return getIndexPatterns(definition?.stream);
46+
}, [definition]);
47+
48+
const discoverLocator = useMemo(
49+
() => share.url.locators.get('DISCOVER_APP_LOCATOR'),
50+
[share.url.locators]
51+
);
52+
53+
const bucketSize = useMemo(() => computeInterval(timeRange, data), [data, timeRange]);
54+
55+
const queries = useMemo(() => {
56+
if (!indexPatterns) {
57+
return undefined;
58+
}
59+
60+
const baseQuery = `FROM ${indexPatterns.join(', ')}`;
61+
62+
const histogramQuery = `${baseQuery} | STATS metric = COUNT(*) BY @timestamp = BUCKET(@timestamp, ${bucketSize})`;
63+
64+
return {
65+
baseQuery,
66+
histogramQuery,
67+
};
68+
}, [bucketSize, indexPatterns]);
69+
70+
const discoverLink = useMemo(() => {
71+
if (!discoverLocator || !queries?.baseQuery) {
72+
return undefined;
73+
}
74+
75+
return discoverLocator.getRedirectUrl({
76+
query: {
77+
esql: queries.baseQuery,
78+
},
79+
});
80+
}, [queries?.baseQuery, discoverLocator]);
81+
82+
const histogramQueryFetch = useStreamsAppFetch(
83+
async ({ signal }) => {
84+
if (!queries?.histogramQuery || !indexPatterns) {
85+
return undefined;
86+
}
87+
88+
const existingIndices = await dataViews.getExistingIndices(indexPatterns);
89+
90+
if (existingIndices.length === 0) {
91+
return undefined;
92+
}
93+
94+
return streamsRepositoryClient.fetch('POST /internal/streams/esql', {
95+
params: {
96+
body: {
97+
operationName: 'get_histogram_for_stream',
98+
query: queries.histogramQuery,
99+
start,
100+
end,
101+
},
102+
},
103+
signal,
104+
});
105+
},
106+
[indexPatterns, dataViews, streamsRepositoryClient, queries?.histogramQuery, start, end]
107+
);
108+
109+
const docCountFetch = useStreamsAppFetch(
110+
async ({ signal }) => {
111+
if (!definition) {
112+
return undefined;
113+
}
114+
return streamsRepositoryClient.fetch('GET /internal/streams/{name}/_details', {
115+
signal,
116+
params: {
117+
path: {
118+
name: definition.stream.name,
119+
},
120+
query: {
121+
start: String(start),
122+
end: String(end),
123+
},
124+
},
125+
});
126+
},
127+
[definition, streamsRepositoryClient, start, end]
128+
);
129+
130+
const dataStreamStats = useDataStreamStats({ definition });
131+
132+
const tabs = useMemo(
133+
() => [
134+
...(definition && isWiredStreamDefinition(definition.stream)
135+
? [
136+
{
137+
id: 'streams',
138+
name: i18n.translate('xpack.streams.entityDetailOverview.tabs.streams', {
139+
defaultMessage: 'Streams',
140+
}),
141+
content: <ChildStreamList definition={definition} />,
142+
},
143+
]
144+
: []),
145+
{
146+
id: 'quicklinks',
147+
name: i18n.translate('xpack.streams.entityDetailOverview.tabs.quicklinks', {
148+
defaultMessage: 'Quick Links',
149+
}),
150+
content: <QuickLinks definition={definition} />,
151+
},
152+
],
153+
[definition]
154+
);
155+
156+
const ilmLocator = share.url.locators.get<IlmLocatorParams>(ILM_LOCATOR_ID);
157+
158+
return (
159+
<>
160+
<EuiFlexGroup direction="column">
161+
<EuiFlexItem grow={false}>
162+
<EuiFlexGroup direction="row" justifyContent="flexEnd">
163+
<EuiFlexItem grow>
164+
<StreamsAppSearchBar
165+
onQuerySubmit={({ dateRange }, isUpdate) => {
166+
if (!isUpdate) {
167+
if (!refreshAbsoluteTimeRange()) {
168+
// if absolute time range didn't change, we need to manually refresh the histogram
169+
// otherwise it will be refreshed by the changed absolute time range
170+
histogramQueryFetch.refresh();
171+
docCountFetch.refresh();
172+
}
173+
return;
174+
}
175+
176+
if (dateRange) {
177+
setTimeRange({ from: dateRange.from, to: dateRange?.to, mode: dateRange.mode });
178+
}
179+
}}
180+
onRefresh={() => {
181+
histogramQueryFetch.refresh();
182+
docCountFetch.refresh();
183+
}}
184+
placeholder={i18n.translate(
185+
'xpack.streams.entityDetailOverview.searchBarPlaceholder',
186+
{
187+
defaultMessage: 'Filter data by using KQL',
188+
}
189+
)}
190+
dateRangeFrom={timeRange.from}
191+
dateRangeTo={timeRange.to}
192+
/>
193+
</EuiFlexItem>
194+
</EuiFlexGroup>
195+
</EuiFlexItem>
196+
197+
<EuiFlexItem grow={false}>
198+
<StreamStatsPanel
199+
definition={definition}
200+
dataStreamStats={dataStreamStats.stats}
201+
docCount={docCountFetch.value}
202+
ilmLocator={ilmLocator}
203+
/>
204+
</EuiFlexItem>
205+
206+
<EuiFlexItem grow>
207+
<EuiFlexGroup direction="row">
208+
<EuiFlexItem grow={4}>{definition && <TabsPanel tabs={tabs} />}</EuiFlexItem>
209+
<EuiFlexItem grow={8}>
210+
<StreamChartPanel
211+
histogramQueryFetch={histogramQueryFetch}
212+
discoverLink={discoverLink}
213+
timerange={{ start, end }}
214+
/>
215+
</EuiFlexItem>
216+
</EuiFlexGroup>
217+
</EuiFlexItem>
218+
</EuiFlexGroup>
219+
</>
220+
);
221+
}

‎x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_view/index.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,23 @@ export function StreamDetailViewContent({ name, tab }: { name: string; tab: stri
4545
label: i18n.translate('xpack.streams.streamDetailView.overviewTab', {
4646
defaultMessage: 'Overview',
4747
}),
48+
background: false,
4849
},
4950
{
5051
name: 'dashboards',
5152
content: <StreamDetailDashboardsView definition={definition} />,
5253
label: i18n.translate('xpack.streams.streamDetailView.dashboardsTab', {
5354
defaultMessage: 'Dashboards',
5455
}),
56+
background: true,
5557
},
5658
{
5759
name: 'management',
5860
content: <StreamDetailManagement definition={definition} refreshDefinition={refresh} />,
5961
label: i18n.translate('xpack.streams.streamDetailView.managementTab', {
6062
defaultMessage: 'Management',
6163
}),
64+
background: true,
6265
},
6366
];
6467

‎x-pack/platform/plugins/shared/streams_app/public/components/stream_list_view/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export function StreamListView() {
4747
}
4848
/>
4949
</EuiFlexItem>
50-
<StreamsAppPageBody>
50+
<StreamsAppPageBody background>
5151
<EuiFlexGroup direction="column">
5252
<EuiFlexItem grow={false}>
5353
<EuiSearchBar

‎x-pack/platform/plugins/shared/streams_app/public/components/streams_app_page_body/index.tsx

+8-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ import React from 'react';
88
import { EuiPanel, useEuiTheme } from '@elastic/eui';
99
import { css } from '@emotion/css';
1010

11-
export function StreamsAppPageBody({ children }: { children: React.ReactNode }) {
11+
export function StreamsAppPageBody({
12+
children,
13+
background,
14+
}: {
15+
children: React.ReactNode;
16+
background: boolean;
17+
}) {
1218
const theme = useEuiTheme().euiTheme;
1319
return (
1420
<EuiPanel
@@ -19,6 +25,7 @@ export function StreamsAppPageBody({ children }: { children: React.ReactNode })
1925
border-radius: 0px;
2026
display: flex;
2127
overflow-y: auto;
28+
${!background ? `background-color: transparent;` : ''}
2229
`}
2330
paddingSize="l"
2431
>

‎x-pack/platform/plugins/shared/streams_app/tsconfig.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,11 @@
2727
"@kbn/react-kibana-context-render",
2828
"@kbn/code-editor",
2929
"@kbn/ui-theme",
30-
"@kbn/calculate-auto",
3130
"@kbn/kibana-react-plugin",
3231
"@kbn/es-query",
3332
"@kbn/server-route-repository-client",
3433
"@kbn/logging",
3534
"@kbn/config-schema",
36-
"@kbn/calculate-auto",
3735
"@kbn/streams-plugin",
3836
"@kbn/share-plugin",
3937
"@kbn/code-editor",
@@ -59,6 +57,7 @@
5957
"@kbn/licensing-plugin",
6058
"@kbn/datemath",
6159
"@kbn/xstate-utils",
60+
"@kbn/visualization-utils",
6261
"@kbn/utility-types",
6362
"@kbn/discover-utils",
6463
"@kbn/discover-shared-plugin",

0 commit comments

Comments
 (0)
Please sign in to comment.