Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
13a159f
wip
thomheymann Mar 17, 2025
2b3f1e8
Merge branch 'main' of https://github.com/elastic/kibana into 180-str…
thomheymann Mar 17, 2025
952320a
wip
thomheymann Mar 19, 2025
3ad551c
Merge branch 'main' of https://github.com/elastic/kibana into 180-str…
thomheymann Mar 19, 2025
6d4387b
wip
thomheymann Mar 21, 2025
d1c8fd4
Merge branch 'main' of https://github.com/elastic/kibana into 180-str…
thomheymann Mar 21, 2025
86a7103
wip
thomheymann Mar 21, 2025
8859e61
.
thomheymann Mar 23, 2025
e9d5155
Merge branch 'main' of https://github.com/elastic/kibana into 180-str…
thomheymann Mar 23, 2025
d84d13e
[CI] Auto-commit changed files from 'node scripts/styled_components_m…
kibanamachine Mar 24, 2025
ee504ae
remove unused translations
thomheymann Mar 24, 2025
d1a5bdd
Merge branch '180-streams-landing-page-improvements' of https://githu…
thomheymann Mar 24, 2025
46df32f
Added suggestions from code and design review
thomheymann Mar 26, 2025
11d1db8
Merge branch 'main' of https://github.com/elastic/kibana into 180-str…
thomheymann Mar 26, 2025
10f1774
Timefilter provider
thomheymann Mar 26, 2025
58221a0
Fix broken charts
thomheymann Mar 26, 2025
5d030f9
Remove delay for rendering loading states
thomheymann Mar 27, 2025
e52f9b9
Merge branch 'main' of https://github.com/elastic/kibana into 180-str…
thomheymann Mar 27, 2025
128bc5c
Increase default pagination
thomheymann Mar 27, 2025
5d2387d
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Mar 27, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -45385,7 +45385,6 @@
"xpack.streams.streamDetailView.welcomeImage": "Image de bienvenue pour l'application de flux",
"xpack.streams.streamsAppDeepLinkTitle": "Flux",
"xpack.streams.streamsAppLinkTitle": "Flux",
"xpack.streams.streamsListViewPageHeaderTitle": "Flux",
"xpack.streams.streamsTable.collapseAll": "Tout réduire",
"xpack.streams.streamsTable.expandAll": "Tout développer",
"xpack.streams.streamsTable.management": "Gestion",
Expand Down Expand Up @@ -49221,4 +49220,4 @@
"xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "Ce champ est requis.",
"xpack.watcher.watcherDescription": "Détectez les modifications survenant dans vos données en créant, gérant et monitorant des alertes."
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45350,7 +45350,6 @@
"xpack.streams.streamDetailView.welcomeImage": "ストリームアプリのウェルカム画像",
"xpack.streams.streamsAppDeepLinkTitle": "ストリーム",
"xpack.streams.streamsAppLinkTitle": "ストリーム",
"xpack.streams.streamsListViewPageHeaderTitle": "ストリーム",
"xpack.streams.streamsTable.collapseAll": "すべて縮小",
"xpack.streams.streamsTable.expandAll": "すべて拡張",
"xpack.streams.streamsTable.management": "管理",
Expand Down Expand Up @@ -49184,4 +49183,4 @@
"xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。",
"xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45422,7 +45422,6 @@
"xpack.streams.streamDetailView.welcomeImage": "流应用的欢迎图像",
"xpack.streams.streamsAppDeepLinkTitle": "流计数",
"xpack.streams.streamsAppLinkTitle": "流计数",
"xpack.streams.streamsListViewPageHeaderTitle": "流计数",
"xpack.streams.streamsTable.collapseAll": "折叠全部",
"xpack.streams.streamsTable.expandAll": "展开全部",
"xpack.streams.streamsTable.management": "管理",
Expand Down Expand Up @@ -49260,4 +49259,4 @@
"xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。",
"xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,45 @@
import { SearchTotalHits } from '@elastic/elasticsearch/lib/api/types';
import { StreamDefinition, isGroupStreamDefinition } from '@kbn/streams-schema';
import { z } from '@kbn/zod';
import { estypes } from '@elastic/elasticsearch';
import { UnwiredIngestStreamEffectiveLifecycle } from '@kbn/streams-schema';
import { createServerRoute } from '../../../create_server_route';
import { getDataStreamLifecycle } from '../../../../lib/streams/stream_crud';

export interface ListStreamDetail {
stream: StreamDefinition;
effective_lifecycle: UnwiredIngestStreamEffectiveLifecycle;
data_stream: estypes.IndicesDataStream;
}

export const listStreamsRoute = createServerRoute({
endpoint: 'GET /internal/streams',
options: {
access: 'internal',
},
params: z.object({}),
handler: async ({ request, getScopedClients }): Promise<{ streams: ListStreamDetail[] }> => {
const { streamsClient, scopedClusterClient } = await getScopedClients({ request });
const streams = await streamsClient.listStreams();
const dataStreams = await scopedClusterClient.asCurrentUser.indices.getDataStream({
name: streams.map((stream) => stream.name),
});

const enrichedStreams = streams.reduce<ListStreamDetail[]>((acc, stream) => {
const match = dataStreams.data_streams.find((dataStream) => dataStream.name === stream.name);
if (match) {
acc.push({
stream,
effective_lifecycle: getDataStreamLifecycle(match),
data_stream: match,
});
}
return acc;
}, []);

return { streams: enrichedStreams };
},
});

export interface StreamDetailsResponse {
details: {
Expand Down Expand Up @@ -103,6 +141,7 @@ export const resolveIndexRoute = createServerRoute({
});

export const internalCrudRoutes = {
...listStreamsRoute,
...streamDetailRoute,
...resolveIndexRoute,
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { streamsAppRouter } from '../../routes/config';
import { StreamsAppStartDependencies } from '../../types';
import { StreamsAppServices } from '../../services/types';
import { HeaderMenuPortal } from '../header_menu';
import { TimeFilterProvider } from '../../hooks/use_timefilter';

export function AppRoot({
coreStart,
Expand Down Expand Up @@ -47,12 +48,14 @@ export function AppRoot({
return (
<StreamsAppContextProvider context={context}>
<RedirectAppLinks coreStart={coreStart}>
<RouterProvider history={history} router={streamsAppRouter}>
<BreadcrumbsContextProvider>
<RouteRenderer />
</BreadcrumbsContextProvider>
<StreamsAppHeaderActionMenu appMountParameters={appMountParameters} />
</RouterProvider>
<TimeFilterProvider timefilter={pluginsStart.data.query.timefilter.timefilter}>
<RouterProvider history={history} router={streamsAppRouter}>
<BreadcrumbsContextProvider>
<RouteRenderer />
</BreadcrumbsContextProvider>
<StreamsAppHeaderActionMenu appMountParameters={appMountParameters} />
</RouterProvider>
</TimeFilterProvider>
</RedirectAppLinks>
</StreamsAppContextProvider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
niceTimeFormatter,
LIGHT_THEME,
DARK_THEME,
DomainRange,
} from '@elastic/charts';
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer, useEuiTheme } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
Expand Down Expand Up @@ -62,14 +63,14 @@ export function ControlledEsqlChart<T extends string>({
metricNames,
chartType = 'line',
height,
timerange,
xDomain: customXDomain,
}: {
id: string;
result: AbortableAsyncState<UnparsedEsqlResponse>;
metricNames: T[];
chartType?: 'area' | 'bar' | 'line';
height?: number;
timerange?: { start: number; end: number };
xDomain?: DomainRange;
}) {
const {
core: { uiSettings },
Expand Down Expand Up @@ -102,14 +103,16 @@ export function ControlledEsqlChart<T extends string>({
const xValues = allTimeseries.flatMap(({ data }) => data.map(({ x }) => x));

// todo - pull in time range here
const min = timerange?.start ?? Math.min(...xValues);
const max = timerange?.end ?? Math.max(...xValues);
const min = customXDomain?.min ?? Math.min(...xValues);
const max = customXDomain?.max ?? Math.max(...xValues);

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

const xFormatter = niceTimeFormatter([min, max]);

const xDomain = isEmpty ? { min: 0, max: 1 } : { min, max };
const xDomain: DomainRange = isEmpty
? { min: 0, max: 1 }
: { min, max, minInterval: customXDomain?.minInterval };

const yTickFormat = (value: number | null) => (value === null ? '' : String(value));
const yLabelFormat = (label: string) => label;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@e
import { css } from '@emotion/css';
import { i18n } from '@kbn/i18n';
import React from 'react';
import type { DomainRange } from '@elastic/charts';

import { AbortableAsyncState } from '@kbn/react-hooks';
import type { UnparsedEsqlResponse } from '@kbn/traced-es-client';
Expand All @@ -16,16 +17,13 @@ import { ControlledEsqlChart } from '../../esql_chart/controlled_esql_chart';
interface StreamChartPanelProps {
histogramQueryFetch: AbortableAsyncState<UnparsedEsqlResponse | undefined>;
discoverLink?: string;
timerange: {
start: number;
end: number;
};
xDomain?: DomainRange;
}

export function StreamChartPanel({
histogramQueryFetch,
discoverLink,
timerange,
xDomain,
}: StreamChartPanelProps) {
return (
<EuiPanel hasShadow={false} hasBorder>
Expand Down Expand Up @@ -62,7 +60,7 @@ export function StreamChartPanel({
id="entity_log_rate"
metricNames={['metric']}
chartType={'bar'}
timerange={timerange}
xDomain={xDomain}
/>
</EuiFlexItem>
</EuiFlexGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { IngestStreamGetResponse, isWiredStreamDefinition } from '@kbn/streams-s
import { ILM_LOCATOR_ID, IlmLocatorParams } from '@kbn/index-lifecycle-management-common-shared';

import { computeInterval } from '@kbn/visualization-utils';
import moment, { DurationInputArg1, DurationInputArg2 } from 'moment';
import { useKibana } from '../../hooks/use_kibana';
import { useStreamsAppFetch } from '../../hooks/use_streams_app_fetch';
import { StreamsAppSearchBar } from '../streams_app_search_bar';
Expand Down Expand Up @@ -155,6 +156,8 @@ export function StreamDetailOverview({ definition }: { definition?: IngestStream

const ilmLocator = share.url.locators.get<IlmLocatorParams>(ILM_LOCATOR_ID);

const [value, unit] = bucketSize.split(' ') as [DurationInputArg1, DurationInputArg2];

return (
<>
<EuiFlexGroup direction="column">
Expand Down Expand Up @@ -210,7 +213,11 @@ export function StreamDetailOverview({ definition }: { definition?: IngestStream
<StreamChartPanel
histogramQueryFetch={histogramQueryFetch}
discoverLink={discoverLink}
timerange={{ start, end }}
xDomain={{
min: start,
max: end,
minInterval: moment.duration(value, unit).asMilliseconds(),
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiI18nNumber,
EuiSkeletonRectangle,
EuiDelayRender,
} from '@elastic/eui';
import { euiThemeVars } from '@kbn/ui-theme';
import { css } from '@emotion/css';
import {
BarSeries,
Chart,
ScaleType,
Settings,
LIGHT_THEME,
DARK_THEME,
niceTimeFormatter,
Tooltip,
TooltipStickTo,
type SettingsProps,
} from '@elastic/charts';
import { i18n } from '@kbn/i18n';
import { useEuiTheme } from '@elastic/eui';
import { useStreamsAppFetch } from '../../hooks/use_streams_app_fetch';
import { useKibana } from '../../hooks/use_kibana';
import { esqlResultToTimeseries } from '../../util/esql_result_to_timeseries';
import { useTimeFilter } from '../../hooks/use_timefilter';

export function DocumentsColumn({
indexPattern,
numDataPoints,
}: {
indexPattern: string;
numDataPoints: number;
}) {
const {
dependencies: {
start: {
streams: { streamsRepositoryClient },
},
},
} = useKibana();

const { absoluteTimeRange } = useTimeFilter();
const minInterval = Math.floor((absoluteTimeRange.end - absoluteTimeRange.start) / numDataPoints);

const histogramQueryFetch = useStreamsAppFetch(
async ({ signal }) => {
return streamsRepositoryClient.fetch('POST /internal/streams/esql', {
params: {
body: {
operationName: 'get_doc_count_for_stream',
query: `FROM ${indexPattern} | STATS doc_count = COUNT(*) BY @timestamp = BUCKET(@timestamp, ${minInterval} ms)`,
start: absoluteTimeRange.start,
end: absoluteTimeRange.end,
},
},
signal,
});
},
[streamsRepositoryClient, indexPattern, absoluteTimeRange, minInterval]
);

const allTimeseries = React.useMemo(
() =>
esqlResultToTimeseries({
result: histogramQueryFetch,
metricNames: ['doc_count'],
}),
[histogramQueryFetch]
);

const docCount = allTimeseries.reduce(
(acc, series) => acc + series.data.reduce((acc2, item) => acc2 + (item.doc_count || 0), 0),
Copy link
Contributor

Choose a reason for hiding this comment

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

It doesn't matter for the classic streams, but there is a detail here we need to think about for wired streams - should we show the count for the stream and its children or just for the current layer? @LucaWintergerst any opinions here?

0
);

const xFormatter = niceTimeFormatter([absoluteTimeRange.start, absoluteTimeRange.end]);

return (
<EuiFlexGroup
alignItems="center"
gutterSize="m"
className={css`
height: ${euiThemeVars.euiSizeXL};
white-space: nowrap;
`}
>
{histogramQueryFetch.loading ? (
<EuiDelayRender delay={300}>
<EuiFlexItem>
<EuiSkeletonRectangle isLoading width="100%" height={euiThemeVars.euiFontSizeS} />
</EuiFlexItem>
<EuiFlexItem>
<EuiSkeletonRectangle isLoading width="100%" height={euiThemeVars.euiSizeL} />
</EuiFlexItem>
</EuiDelayRender>
) : (
<>
<EuiFlexItem
className={css`
text-align: right;
`}
>
<EuiI18nNumber value={docCount} />
</EuiFlexItem>
<EuiFlexItem
className={css`
border-bottom: 1px solid ${euiThemeVars.euiColorLightestShade};
`}
>
<Chart size={{ width: '100%', height: euiThemeVars.euiSizeL }}>
<SettingsWithTheme
xDomain={{ min: absoluteTimeRange.start, max: absoluteTimeRange.end, minInterval }}
noResults={<div />}
/>
<Tooltip
stickTo={TooltipStickTo.Middle}
headerFormatter={({ value }) => xFormatter(value)}
/>
{allTimeseries.map((serie) => (
<BarSeries
key={serie.id}
id={serie.id}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor="x"
yAccessors={['doc_count']}
data={serie.data}
/>
))}
</Chart>
</EuiFlexItem>
</>
)}
</EuiFlexGroup>
);
}

function SettingsWithTheme(props: SettingsProps) {
const { colorMode } = useEuiTheme();
return (
<Settings
locale={i18n.getLocale()}
baseTheme={colorMode === 'LIGHT' ? LIGHT_THEME : DARK_THEME}
theme={{ background: { color: 'transparent' } }}
{...props}
/>
);
}
Loading