mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
🌊 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 <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
f89e03c286
commit
184d0a32ad
23 changed files with 772 additions and 367 deletions
|
@ -14,3 +14,4 @@ export { useDebouncedValue } from './src/debounced_value';
|
|||
export { ChartType } from './src/types';
|
||||
export { getDatasourceId } from './src/get_datasource_id';
|
||||
export { mapVisToChartType } from './src/map_vis_to_chart_type';
|
||||
export { computeInterval } from './src/compute_interval';
|
||||
|
|
|
@ -11,7 +11,6 @@ import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
|||
import type { TimeRange } from '@kbn/es-query';
|
||||
|
||||
// follows the same logic with vega auto_date function
|
||||
// we could move to a package and reuse in the future
|
||||
const barTarget = 50; // same as vega
|
||||
const roundInterval = (interval: number) => {
|
||||
{
|
|
@ -13,5 +13,6 @@
|
|||
"@kbn/interpreter",
|
||||
"@kbn/data-views-plugin",
|
||||
"@kbn/es-query",
|
||||
"@kbn/data-plugin",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
import { TimeRange } from '@kbn/es-query';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { Timefilter } from './timefilter';
|
||||
|
||||
export interface TimefilterHook {
|
||||
|
@ -18,6 +18,7 @@ export interface TimefilterHook {
|
|||
end: number;
|
||||
};
|
||||
setTimeRange: React.Dispatch<React.SetStateAction<TimeRange>>;
|
||||
refreshAbsoluteTimeRange: () => boolean;
|
||||
}
|
||||
|
||||
export function createUseTimefilterHook(timefilter: Timefilter) {
|
||||
|
@ -25,11 +26,14 @@ export function createUseTimefilterHook(timefilter: Timefilter) {
|
|||
const [timeRange, setTimeRange] = useState(() => timefilter.getTime());
|
||||
|
||||
const [absoluteTimeRange, setAbsoluteTimeRange] = useState(() => timefilter.getAbsoluteTime());
|
||||
const absoluteTimeRangeRef = useRef(absoluteTimeRange);
|
||||
|
||||
useEffect(() => {
|
||||
const timeUpdateSubscription = timefilter.getTimeUpdate$().subscribe({
|
||||
next: () => {
|
||||
setTimeRange(() => timefilter.getTime());
|
||||
const newAbsoluteTimeRange = timefilter.getAbsoluteTime();
|
||||
absoluteTimeRangeRef.current = newAbsoluteTimeRange;
|
||||
setAbsoluteTimeRange(() => timefilter.getAbsoluteTime());
|
||||
},
|
||||
});
|
||||
|
@ -51,6 +55,19 @@ export function createUseTimefilterHook(timefilter: Timefilter) {
|
|||
[]
|
||||
);
|
||||
|
||||
const refreshAbsoluteTimeRange = useCallback(() => {
|
||||
const newAbsoluteTimeRange = timefilter.getAbsoluteTime();
|
||||
if (
|
||||
newAbsoluteTimeRange.from !== absoluteTimeRangeRef.current.from ||
|
||||
newAbsoluteTimeRange.to !== absoluteTimeRangeRef.current.to
|
||||
) {
|
||||
setAbsoluteTimeRange(newAbsoluteTimeRange);
|
||||
absoluteTimeRangeRef.current = newAbsoluteTimeRange;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, []);
|
||||
|
||||
const asEpoch = useMemo(() => {
|
||||
return {
|
||||
start: new Date(absoluteTimeRange.from).getTime(),
|
||||
|
@ -62,6 +79,7 @@ export function createUseTimefilterHook(timefilter: Timefilter) {
|
|||
timeRange,
|
||||
absoluteTimeRange: asEpoch,
|
||||
setTimeRange: setTimeRangeMemoized,
|
||||
refreshAbsoluteTimeRange,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
getLensAttributesFromSuggestion,
|
||||
ChartType,
|
||||
mapVisToChartType,
|
||||
computeInterval,
|
||||
} from '@kbn/visualization-utils';
|
||||
import { LegendSize } from '@kbn/visualizations-plugin/public';
|
||||
import { XYConfiguration } from '@kbn/visualizations-plugin/common';
|
||||
|
@ -51,7 +52,6 @@ import {
|
|||
injectESQLQueryIntoLensLayers,
|
||||
TIMESTAMP_COLUMN,
|
||||
} from '../utils/external_vis_context';
|
||||
import { computeInterval } from '../utils/compute_interval';
|
||||
import { enrichLensAttributesWithTablesData } from '../utils/lens_vis_from_table';
|
||||
|
||||
const UNIFIED_HISTOGRAM_LAYER_ID = 'unifiedHistogram';
|
||||
|
|
|
@ -44811,7 +44811,6 @@
|
|||
"xpack.streams.dashboardTable.dashboardNameColumnTitle": "Nom du tableau de bord",
|
||||
"xpack.streams.dashboardTable.tagsColumnTitle": "Balises",
|
||||
"xpack.streams.entityDetailOverview.createChildStream": "Créer un flux enfant",
|
||||
"xpack.streams.entityDetailOverview.docCount": "{docCount} documents",
|
||||
"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.",
|
||||
"xpack.streams.entityDetailOverview.searchBarPlaceholder": "Filtrer les données avec KQL",
|
||||
"xpack.streams.entityDetailOverview.tabs.quicklinks": "Liens rapides",
|
||||
|
|
|
@ -44848,7 +44848,6 @@
|
|||
"xpack.streams.dashboardTable.dashboardNameColumnTitle": "仪表板名称",
|
||||
"xpack.streams.dashboardTable.tagsColumnTitle": "标签",
|
||||
"xpack.streams.entityDetailOverview.createChildStream": "创建子数据流",
|
||||
"xpack.streams.entityDetailOverview.docCount": "{docCount} 个文档",
|
||||
"xpack.streams.entityDetailOverview.noChildStreams": "创建子流以分割具有不同保留策略、方案等的数据。",
|
||||
"xpack.streams.entityDetailOverview.searchBarPlaceholder": "通过使用 KQL 来筛选数据",
|
||||
"xpack.streams.entityDetailOverview.tabs.quicklinks": "快速链接",
|
||||
|
|
|
@ -6,5 +6,20 @@
|
|||
*/
|
||||
|
||||
import { formatNumber } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const formatBytes = (value: number) => formatNumber(value, '0.0 b');
|
||||
|
||||
export const formatIngestionRate = (bytesPerDay: number, perDayOnly = false) => {
|
||||
const perDay = formatBytes(bytesPerDay);
|
||||
const perMonth = formatBytes(bytesPerDay * 30);
|
||||
if (perDayOnly)
|
||||
return i18n.translate('xpack.streams.streamDetailOverview.ingestionRatePerDay', {
|
||||
defaultMessage: '{perDay} / Day',
|
||||
values: { perDay },
|
||||
});
|
||||
return i18n.translate('xpack.streams.streamDetailOverview.ingestionRatePerDayPerMonth', {
|
||||
defaultMessage: '{perDay} / Day ({perMonth} / Month)',
|
||||
values: { perDay, perMonth },
|
||||
});
|
||||
};
|
||||
|
|
|
@ -38,7 +38,7 @@ import { LifecycleEditAction } from './modal';
|
|||
import { IlmLink } from './ilm_link';
|
||||
import { useStreamsAppRouter } from '../../../hooks/use_streams_app_router';
|
||||
import { DataStreamStats } from './hooks/use_data_stream_stats';
|
||||
import { formatBytes } from './helpers/format_bytes';
|
||||
import { formatIngestionRate } from './helpers/format_bytes';
|
||||
|
||||
export function RetentionMetadata({
|
||||
definition,
|
||||
|
@ -243,9 +243,3 @@ function MetadataRow({
|
|||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
const formatIngestionRate = (bytesPerDay: number) => {
|
||||
const perDay = formatBytes(bytesPerDay);
|
||||
const perMonth = formatBytes(bytesPerDay * 30);
|
||||
return `${perDay} / Day - ${perMonth} / Month`;
|
||||
};
|
||||
|
|
|
@ -30,6 +30,7 @@ export interface EntityViewTab {
|
|||
name: string;
|
||||
label: string;
|
||||
content: React.ReactElement;
|
||||
background: boolean;
|
||||
}
|
||||
|
||||
export function EntityDetailViewWithoutParams({
|
||||
|
@ -75,6 +76,7 @@ export function EntityDetailViewWithoutParams({
|
|||
}),
|
||||
label: tab.label,
|
||||
content: tab.content,
|
||||
background: tab.background,
|
||||
},
|
||||
];
|
||||
})
|
||||
|
@ -126,7 +128,9 @@ export function EntityDetailViewWithoutParams({
|
|||
/>
|
||||
</StreamsAppPageHeader>
|
||||
</EuiFlexItem>
|
||||
<StreamsAppPageBody>{selectedTabObject.content}</StreamsAppPageBody>
|
||||
<StreamsAppPageBody background={selectedTabObject.background}>
|
||||
{selectedTabObject.content}
|
||||
</StreamsAppPageBody>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -62,12 +62,14 @@ export function ControlledEsqlChart<T extends string>({
|
|||
metricNames,
|
||||
chartType = 'line',
|
||||
height,
|
||||
timerange,
|
||||
}: {
|
||||
id: string;
|
||||
result: AbortableAsyncState<UnparsedEsqlResponse>;
|
||||
metricNames: T[];
|
||||
chartType?: 'area' | 'bar' | 'line';
|
||||
height: number;
|
||||
height?: number;
|
||||
timerange?: { start: number; end: number };
|
||||
}) {
|
||||
const {
|
||||
core: { uiSettings },
|
||||
|
@ -84,12 +86,14 @@ export function ControlledEsqlChart<T extends string>({
|
|||
[result, ...metricNames]
|
||||
);
|
||||
|
||||
const effectiveHeight = height ? `${height}px` : '100%';
|
||||
|
||||
if (result.loading && !result.value?.values.length) {
|
||||
return (
|
||||
<LoadingPanel
|
||||
loading
|
||||
className={css`
|
||||
height: ${height}px;
|
||||
height: ${effectiveHeight};
|
||||
`}
|
||||
/>
|
||||
);
|
||||
|
@ -97,8 +101,9 @@ export function ControlledEsqlChart<T extends string>({
|
|||
|
||||
const xValues = allTimeseries.flatMap(({ data }) => data.map(({ x }) => x));
|
||||
|
||||
const min = Math.min(...xValues);
|
||||
const max = Math.max(...xValues);
|
||||
// todo - pull in time range here
|
||||
const min = timerange?.start ?? Math.min(...xValues);
|
||||
const max = timerange?.end ?? Math.max(...xValues);
|
||||
|
||||
const isEmpty = min === 0 && max === 0;
|
||||
|
||||
|
@ -115,7 +120,7 @@ export function ControlledEsqlChart<T extends string>({
|
|||
<Chart
|
||||
id={id}
|
||||
className={css`
|
||||
height: ${height}px;
|
||||
height: ${effectiveHeight};
|
||||
`}
|
||||
>
|
||||
<Tooltip
|
||||
|
@ -146,7 +151,7 @@ export function ControlledEsqlChart<T extends string>({
|
|||
}}
|
||||
/>
|
||||
<Settings
|
||||
showLegend
|
||||
showLegend={false}
|
||||
legendPosition={Position.Bottom}
|
||||
xDomain={xDomain}
|
||||
locale={i18n.getLocale()}
|
||||
|
@ -173,6 +178,7 @@ export function ControlledEsqlChart<T extends string>({
|
|||
<Series
|
||||
timeZone={timeZone}
|
||||
key={serie.id}
|
||||
color="#61A2FF"
|
||||
id={serie.id}
|
||||
xScaleType={ScaleType.Time}
|
||||
yScaleType={ScaleType.Linear}
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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 { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useMemo } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { IngestStreamGetResponse, isDescendantOf } from '@kbn/streams-schema';
|
||||
|
||||
import { useStreamsAppRouter } from '../../hooks/use_streams_app_router';
|
||||
import { AssetImage } from '../asset_image';
|
||||
import { StreamsList } from '../streams_list';
|
||||
import { useWiredStreams } from '../../hooks/use_wired_streams';
|
||||
|
||||
export function ChildStreamList({ definition }: { definition?: IngestStreamGetResponse }) {
|
||||
const router = useStreamsAppRouter();
|
||||
|
||||
const { wiredStreams } = useWiredStreams();
|
||||
|
||||
const childrenStreams = useMemo(() => {
|
||||
if (!definition) {
|
||||
return [];
|
||||
}
|
||||
return wiredStreams?.filter((d) => isDescendantOf(definition.stream.name, d.name));
|
||||
}, [definition, wiredStreams]);
|
||||
|
||||
if (definition && childrenStreams?.length === 0) {
|
||||
return (
|
||||
<EuiFlexItem grow>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="center">
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
className={css`
|
||||
max-width: 350px;
|
||||
`}
|
||||
>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<AssetImage type="welcome" />
|
||||
<EuiText size="m" textAlign="center">
|
||||
{i18n.translate('xpack.streams.entityDetailOverview.noChildStreams', {
|
||||
defaultMessage: 'Create streams for your logs',
|
||||
})}
|
||||
</EuiText>
|
||||
<EuiText size="xs" textAlign="center">
|
||||
{i18n.translate('xpack.streams.entityDetailOverview.noChildStreams', {
|
||||
defaultMessage:
|
||||
'Create sub streams to split out data with different retention policies, schemas, and more.',
|
||||
})}
|
||||
</EuiText>
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
<EuiButton
|
||||
data-test-subj="streamsAppChildStreamListCreateChildStreamButton"
|
||||
iconType="plusInCircle"
|
||||
href={router.link('/{key}/management/{subtab}', {
|
||||
path: {
|
||||
key: definition?.stream.name,
|
||||
subtab: 'route',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{i18n.translate('xpack.streams.entityDetailOverview.createChildStream', {
|
||||
defaultMessage: 'Create child stream',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
return <StreamsList streams={childrenStreams} showControls={false} />;
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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 { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui';
|
||||
import { css } from '@emotion/css';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
|
||||
import { AbortableAsyncState } from '@kbn/react-hooks';
|
||||
import type { UnparsedEsqlResponse } from '@kbn/traced-es-client';
|
||||
import { ControlledEsqlChart } from '../../esql_chart/controlled_esql_chart';
|
||||
|
||||
interface StreamChartPanelProps {
|
||||
histogramQueryFetch: AbortableAsyncState<UnparsedEsqlResponse | undefined>;
|
||||
discoverLink?: string;
|
||||
timerange: {
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function StreamChartPanel({
|
||||
histogramQueryFetch,
|
||||
discoverLink,
|
||||
timerange,
|
||||
}: StreamChartPanelProps) {
|
||||
return (
|
||||
<EuiPanel hasShadow={false} hasBorder>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
className={css`
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s">
|
||||
{i18n.translate('xpack.streams.streamDetailOverview.logRate', {
|
||||
defaultMessage: 'Documents',
|
||||
})}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="streamsDetailOverviewOpenInDiscoverButton"
|
||||
iconType="discoverApp"
|
||||
href={discoverLink}
|
||||
isDisabled={!discoverLink}
|
||||
>
|
||||
{i18n.translate('xpack.streams.streamDetailOverview.openInDiscoverButtonLabel', {
|
||||
defaultMessage: 'Open in Discover',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow>
|
||||
<ControlledEsqlChart
|
||||
result={histogramQueryFetch}
|
||||
id="entity_log_rate"
|
||||
metricNames={['metric']}
|
||||
chartType={'bar'}
|
||||
timerange={timerange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIconTip,
|
||||
EuiPanel,
|
||||
EuiText,
|
||||
formatNumber,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/css';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { IngestStreamGetResponse, IngestStreamLifecycleILM } from '@kbn/streams-schema';
|
||||
import { IlmLocatorParams } from '@kbn/index-lifecycle-management-common-shared';
|
||||
|
||||
import { LocatorPublic } from '@kbn/share-plugin/public';
|
||||
import type { StreamDetailsResponse } from '@kbn/streams-plugin/server/routes/internal/streams/crud/route';
|
||||
import { IlmLink } from '../../data_management/stream_detail_lifecycle/ilm_link';
|
||||
import {
|
||||
formatBytes,
|
||||
formatIngestionRate,
|
||||
} from '../../data_management/stream_detail_lifecycle/helpers/format_bytes';
|
||||
import { DataStreamStats } from '../../data_management/stream_detail_lifecycle/hooks/use_data_stream_stats';
|
||||
|
||||
interface StreamStatsPanelProps {
|
||||
definition?: IngestStreamGetResponse;
|
||||
dataStreamStats?: DataStreamStats;
|
||||
docCount?: StreamDetailsResponse;
|
||||
ilmLocator?: LocatorPublic<IlmLocatorParams>;
|
||||
}
|
||||
|
||||
const RetentionDisplay = ({
|
||||
definition,
|
||||
ilmLocator,
|
||||
}: {
|
||||
definition?: IngestStreamGetResponse;
|
||||
ilmLocator?: LocatorPublic<IlmLocatorParams>;
|
||||
}) => {
|
||||
if (!definition) return <>-</>;
|
||||
|
||||
if ('dsl' in definition.effective_lifecycle) {
|
||||
return (
|
||||
<>
|
||||
{definition?.effective_lifecycle.dsl.data_retention ||
|
||||
i18n.translate('xpack.streams.entityDetailOverview.unlimited', {
|
||||
defaultMessage: 'Keep indefinitely',
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<IlmLink
|
||||
lifecycle={definition.effective_lifecycle as IngestStreamLifecycleILM}
|
||||
ilmLocator={ilmLocator}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface StatItemProps {
|
||||
label: ReactNode;
|
||||
value: ReactNode;
|
||||
withBorder?: boolean;
|
||||
}
|
||||
|
||||
const StatItem = ({ label, value, withBorder = false }: StatItemProps) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const borderStyle = withBorder
|
||||
? css`
|
||||
border-left: 1px solid ${euiTheme.colors.borderBaseSubdued};
|
||||
padding-left: ${euiTheme.size.s};
|
||||
`
|
||||
: '';
|
||||
|
||||
return (
|
||||
<EuiFlexItem grow className={borderStyle}>
|
||||
<EuiFlexGroup direction="column" gutterSize="xs">
|
||||
<EuiText size="xs" color="subdued">
|
||||
{label}
|
||||
</EuiText>
|
||||
<EuiText
|
||||
size="m"
|
||||
className={css`
|
||||
font-weight: bold;
|
||||
`}
|
||||
>
|
||||
{value}
|
||||
</EuiText>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
||||
|
||||
export function StreamStatsPanel({
|
||||
definition,
|
||||
dataStreamStats,
|
||||
docCount,
|
||||
ilmLocator,
|
||||
}: StreamStatsPanelProps) {
|
||||
const retentionLabel = i18n.translate('xpack.streams.entityDetailOverview.retention', {
|
||||
defaultMessage: 'Data retention',
|
||||
});
|
||||
|
||||
const documentCountLabel = i18n.translate('xpack.streams.entityDetailOverview.count', {
|
||||
defaultMessage: 'Document count',
|
||||
});
|
||||
|
||||
const storageSizeLabel = i18n.translate('xpack.streams.entityDetailOverview.size', {
|
||||
defaultMessage: 'Storage size',
|
||||
});
|
||||
|
||||
const ingestionLabel = i18n.translate('xpack.streams.entityDetailOverview.ingestion', {
|
||||
defaultMessage: 'Ingestion',
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="row" gutterSize="s">
|
||||
<EuiFlexItem grow={3}>
|
||||
<EuiPanel hasShadow={false} hasBorder>
|
||||
<EuiFlexGroup direction="column" gutterSize="xs">
|
||||
<EuiText size="xs" color="subdued">
|
||||
{retentionLabel}
|
||||
</EuiText>
|
||||
<EuiText size="m">
|
||||
<RetentionDisplay definition={definition} ilmLocator={ilmLocator} />
|
||||
</EuiText>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={9}>
|
||||
<EuiPanel hasShadow={false} hasBorder>
|
||||
<EuiFlexGroup>
|
||||
<StatItem
|
||||
label={documentCountLabel}
|
||||
value={docCount ? formatNumber(docCount.details.count || 0, 'decimal0') : '-'}
|
||||
/>
|
||||
<StatItem
|
||||
label={
|
||||
<>
|
||||
{storageSizeLabel}
|
||||
<EuiIconTip
|
||||
content={i18n.translate('xpack.streams.streamDetailOverview.sizeTip', {
|
||||
defaultMessage:
|
||||
'Estimated size based on the number of documents in the current time range and the total size of the stream.',
|
||||
})}
|
||||
position="right"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
value={
|
||||
dataStreamStats && docCount
|
||||
? formatBytes(getStorageSizeForTimeRange(dataStreamStats, docCount))
|
||||
: '-'
|
||||
}
|
||||
withBorder
|
||||
/>
|
||||
<StatItem
|
||||
label={
|
||||
<>
|
||||
{ingestionLabel}
|
||||
<EuiIconTip
|
||||
content={i18n.translate(
|
||||
'xpack.streams.streamDetailLifecycle.ingestionRateDetails',
|
||||
{
|
||||
defaultMessage:
|
||||
'Estimated average (stream total size divided by the number of days since creation).',
|
||||
}
|
||||
)}
|
||||
position="right"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
value={
|
||||
dataStreamStats ? formatIngestionRate(dataStreamStats.bytesPerDay || 0, true) : '-'
|
||||
}
|
||||
withBorder
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function getStorageSizeForTimeRange(
|
||||
dataStreamStats: DataStreamStats,
|
||||
docCount: StreamDetailsResponse
|
||||
) {
|
||||
const storageSize = dataStreamStats.sizeBytes;
|
||||
const totalCount = dataStreamStats.totalDocs;
|
||||
const countForTimeRange = docCount.details.count;
|
||||
if (!storageSize || !totalCount || !countForTimeRange) {
|
||||
return 0;
|
||||
}
|
||||
const bytesPerDoc = totalCount ? storageSize / totalCount : 0;
|
||||
return bytesPerDoc * countForTimeRange;
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiPanel, EuiTab, EuiTabs } from '@elastic/eui';
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useState, ReactNode } from 'react';
|
||||
|
||||
interface Tab {
|
||||
id: string;
|
||||
name: string;
|
||||
content: ReactNode;
|
||||
}
|
||||
|
||||
interface TabsPanelProps {
|
||||
tabs: Tab[];
|
||||
}
|
||||
|
||||
export function TabsPanel({ tabs }: TabsPanelProps) {
|
||||
const [selectedTab, setSelectedTab] = useState<string | undefined>(undefined);
|
||||
|
||||
if (tabs.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiPanel hasShadow={false} hasBorder>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="s"
|
||||
className={css`
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
{tabs.length === 1 ? (
|
||||
tabs[0].content
|
||||
) : (
|
||||
<>
|
||||
<EuiTabs>
|
||||
{tabs.map((tab, index) => (
|
||||
<EuiTab
|
||||
isSelected={(!selectedTab && index === 0) || selectedTab === tab.id}
|
||||
onClick={() => setSelectedTab(tab.id)}
|
||||
key={tab.id}
|
||||
>
|
||||
{tab.name}
|
||||
</EuiTab>
|
||||
))}
|
||||
</EuiTabs>
|
||||
{
|
||||
tabs.find((tab, index) => (!selectedTab && index === 0) || selectedTab === tab.id)
|
||||
?.content
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -4,348 +4,5 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
EuiPanel,
|
||||
EuiTab,
|
||||
EuiTabs,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { calculateAuto } from '@kbn/calculate-auto';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import moment from 'moment';
|
||||
import React, { useMemo } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import {
|
||||
IngestStreamGetResponse,
|
||||
isDescendantOf,
|
||||
isUnwiredStreamGetResponse,
|
||||
isWiredStreamDefinition,
|
||||
} from '@kbn/streams-schema';
|
||||
import type { SanitizedDashboardAsset } from '@kbn/streams-plugin/server/routes/dashboards/route';
|
||||
import { useKibana } from '../../hooks/use_kibana';
|
||||
import { useStreamsAppFetch } from '../../hooks/use_streams_app_fetch';
|
||||
import { ControlledEsqlChart } from '../esql_chart/controlled_esql_chart';
|
||||
import { StreamsAppSearchBar } from '../streams_app_search_bar';
|
||||
import { getIndexPatterns } from '../../util/hierarchy_helpers';
|
||||
import { StreamsList } from '../streams_list';
|
||||
import { useStreamsAppRouter } from '../../hooks/use_streams_app_router';
|
||||
import { useDashboardsFetch } from '../../hooks/use_dashboards_fetch';
|
||||
import { DashboardsTable } from '../stream_detail_dashboards_view/dashboard_table';
|
||||
import { AssetImage } from '../asset_image';
|
||||
import { useWiredStreams } from '../../hooks/use_wired_streams';
|
||||
|
||||
const formatNumber = (val: number) => {
|
||||
return Number(val).toLocaleString('en', {
|
||||
maximumFractionDigits: 1,
|
||||
});
|
||||
};
|
||||
|
||||
export function StreamDetailOverview({ definition }: { definition?: IngestStreamGetResponse }) {
|
||||
const {
|
||||
dependencies: {
|
||||
start: {
|
||||
data,
|
||||
dataViews,
|
||||
streams: { streamsRepositoryClient },
|
||||
share,
|
||||
},
|
||||
},
|
||||
} = useKibana();
|
||||
|
||||
const {
|
||||
timeRange,
|
||||
setTimeRange,
|
||||
absoluteTimeRange: { start, end },
|
||||
} = data.query.timefilter.timefilter.useTimefilter();
|
||||
|
||||
const indexPatterns = useMemo(() => {
|
||||
return getIndexPatterns(definition?.stream);
|
||||
}, [definition]);
|
||||
|
||||
const discoverLocator = useMemo(
|
||||
() => share.url.locators.get('DISCOVER_APP_LOCATOR'),
|
||||
[share.url.locators]
|
||||
);
|
||||
|
||||
const queries = useMemo(() => {
|
||||
if (!indexPatterns) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const baseQuery = `FROM ${indexPatterns.join(', ')}`;
|
||||
|
||||
const bucketSize = Math.round(
|
||||
calculateAuto.atLeast(50, moment.duration(1, 'minute'))!.asSeconds()
|
||||
);
|
||||
|
||||
const histogramQuery = `${baseQuery} | STATS metric = COUNT(*) BY @timestamp = BUCKET(@timestamp, ${bucketSize} seconds)`;
|
||||
|
||||
return {
|
||||
baseQuery,
|
||||
histogramQuery,
|
||||
};
|
||||
}, [indexPatterns]);
|
||||
|
||||
const discoverLink = useMemo(() => {
|
||||
if (!discoverLocator || !queries?.baseQuery) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return discoverLocator.getRedirectUrl({
|
||||
query: {
|
||||
esql: queries.baseQuery,
|
||||
},
|
||||
});
|
||||
}, [queries?.baseQuery, discoverLocator]);
|
||||
|
||||
const histogramQueryFetch = useStreamsAppFetch(
|
||||
async ({ signal }) => {
|
||||
if (!queries?.histogramQuery || !indexPatterns) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const existingIndices = await dataViews.getExistingIndices(indexPatterns);
|
||||
|
||||
if (existingIndices.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return streamsRepositoryClient.fetch('POST /internal/streams/esql', {
|
||||
params: {
|
||||
body: {
|
||||
operationName: 'get_histogram_for_stream',
|
||||
query: queries.histogramQuery,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
},
|
||||
signal,
|
||||
});
|
||||
},
|
||||
[indexPatterns, dataViews, streamsRepositoryClient, queries?.histogramQuery, start, end]
|
||||
);
|
||||
|
||||
const docCountFetch = useStreamsAppFetch(
|
||||
async ({ signal }) => {
|
||||
if (
|
||||
!definition ||
|
||||
(isUnwiredStreamGetResponse(definition) && !definition.data_stream_exists)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return streamsRepositoryClient.fetch('GET /internal/streams/{name}/_details', {
|
||||
signal,
|
||||
params: {
|
||||
path: {
|
||||
name: definition.stream.name,
|
||||
},
|
||||
query: {
|
||||
start: String(start),
|
||||
end: String(end),
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
[definition, dataViews, streamsRepositoryClient, start, end]
|
||||
);
|
||||
|
||||
const [selectedTab, setSelectedTab] = React.useState<string | undefined>(undefined);
|
||||
|
||||
const tabs = [
|
||||
...(definition && isWiredStreamDefinition(definition.stream)
|
||||
? [
|
||||
{
|
||||
id: 'streams',
|
||||
name: i18n.translate('xpack.streams.entityDetailOverview.tabs.streams', {
|
||||
defaultMessage: 'Streams',
|
||||
}),
|
||||
content: <ChildStreamList definition={definition} />,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'quicklinks',
|
||||
name: i18n.translate('xpack.streams.entityDetailOverview.tabs.quicklinks', {
|
||||
defaultMessage: 'Quick Links',
|
||||
}),
|
||||
content: <QuickLinks definition={definition} />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup direction="row" gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem>
|
||||
{docCountFetch.loading ? (
|
||||
<EuiLoadingSpinner size="m" />
|
||||
) : (
|
||||
docCountFetch.value && (
|
||||
<EuiText>
|
||||
{i18n.translate('xpack.streams.entityDetailOverview.docCount', {
|
||||
defaultMessage: '{docCount} documents',
|
||||
values: { docCount: formatNumber(docCountFetch.value.details.count) },
|
||||
})}
|
||||
</EuiText>
|
||||
)
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow>
|
||||
<StreamsAppSearchBar
|
||||
onQuerySubmit={({ dateRange }, isUpdate) => {
|
||||
if (!isUpdate) {
|
||||
histogramQueryFetch.refresh();
|
||||
docCountFetch.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
if (dateRange) {
|
||||
setTimeRange({ from: dateRange.from, to: dateRange?.to, mode: dateRange.mode });
|
||||
}
|
||||
}}
|
||||
onRefresh={() => {
|
||||
histogramQueryFetch.refresh();
|
||||
}}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.streams.entityDetailOverview.searchBarPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Filter data by using KQL',
|
||||
}
|
||||
)}
|
||||
dateRangeFrom={timeRange.from}
|
||||
dateRangeTo={timeRange.to}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiButton
|
||||
data-test-subj="streamsDetailOverviewOpenInDiscoverButton"
|
||||
iconType="discoverApp"
|
||||
href={discoverLink}
|
||||
color="text"
|
||||
>
|
||||
{i18n.translate('xpack.streams.streamDetailOverview.openInDiscoverButtonLabel', {
|
||||
defaultMessage: 'Open in Discover',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPanel hasShadow={false} hasBorder>
|
||||
<EuiFlexGroup direction="column">
|
||||
<ControlledEsqlChart
|
||||
result={histogramQueryFetch}
|
||||
id="entity_log_rate"
|
||||
metricNames={['metric']}
|
||||
height={200}
|
||||
chartType={'bar'}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
{definition && (
|
||||
<>
|
||||
<EuiTabs>
|
||||
{tabs.map((tab, index) => (
|
||||
<EuiTab
|
||||
isSelected={(!selectedTab && index === 0) || selectedTab === tab.id}
|
||||
onClick={() => setSelectedTab(tab.id)}
|
||||
key={tab.id}
|
||||
>
|
||||
{tab.name}
|
||||
</EuiTab>
|
||||
))}
|
||||
</EuiTabs>
|
||||
{
|
||||
tabs.find((tab, index) => (!selectedTab && index === 0) || selectedTab === tab.id)
|
||||
?.content
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const EMPTY_DASHBOARD_LIST: SanitizedDashboardAsset[] = [];
|
||||
|
||||
function QuickLinks({ definition }: { definition?: IngestStreamGetResponse }) {
|
||||
const dashboardsFetch = useDashboardsFetch(definition?.stream.name);
|
||||
|
||||
return (
|
||||
<DashboardsTable
|
||||
entityId={definition?.stream.name}
|
||||
dashboards={dashboardsFetch.value?.dashboards ?? EMPTY_DASHBOARD_LIST}
|
||||
loading={dashboardsFetch.loading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ChildStreamList({ definition }: { definition?: IngestStreamGetResponse }) {
|
||||
const router = useStreamsAppRouter();
|
||||
|
||||
const { wiredStreams } = useWiredStreams();
|
||||
|
||||
const childrenStreams = useMemo(() => {
|
||||
if (!definition) {
|
||||
return [];
|
||||
}
|
||||
return wiredStreams?.filter((d) => isDescendantOf(definition.stream.name, d.name));
|
||||
}, [definition, wiredStreams]);
|
||||
|
||||
if (definition && childrenStreams?.length === 0) {
|
||||
return (
|
||||
<EuiFlexItem grow>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="center">
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
className={css`
|
||||
max-width: 350px;
|
||||
`}
|
||||
>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<AssetImage type="welcome" />
|
||||
<EuiText size="m" textAlign="center">
|
||||
{i18n.translate('xpack.streams.entityDetailOverview.noChildStreams', {
|
||||
defaultMessage: 'Create streams for your logs',
|
||||
})}
|
||||
</EuiText>
|
||||
<EuiText size="xs" textAlign="center">
|
||||
{i18n.translate('xpack.streams.entityDetailOverview.noChildStreams', {
|
||||
defaultMessage:
|
||||
'Create sub streams to split out data with different retention policies, schemas, and more.',
|
||||
})}
|
||||
</EuiText>
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
<EuiButton
|
||||
data-test-subj="streamsAppChildStreamListCreateChildStreamButton"
|
||||
iconType="plusInCircle"
|
||||
href={router.link('/{key}/management/{subtab}', {
|
||||
path: {
|
||||
key: definition?.stream.name,
|
||||
subtab: 'route',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{i18n.translate('xpack.streams.entityDetailOverview.createChildStream', {
|
||||
defaultMessage: 'Create child stream',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
return <StreamsList streams={childrenStreams} showControls={false} />;
|
||||
}
|
||||
export { StreamDetailOverview } from './stream_detail_overview';
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { IngestStreamGetResponse } from '@kbn/streams-schema';
|
||||
import type { SanitizedDashboardAsset } from '@kbn/streams-plugin/server/routes/dashboards/route';
|
||||
|
||||
import { useDashboardsFetch } from '../../hooks/use_dashboards_fetch';
|
||||
import { AssetImage } from '../asset_image';
|
||||
import { useStreamsAppRouter } from '../../hooks/use_streams_app_router';
|
||||
import { DashboardsTable } from '../stream_detail_dashboards_view/dashboard_table';
|
||||
|
||||
const EMPTY_DASHBOARD_LIST: SanitizedDashboardAsset[] = [];
|
||||
|
||||
export function QuickLinks({ definition }: { definition?: IngestStreamGetResponse }) {
|
||||
const router = useStreamsAppRouter();
|
||||
const dashboardsFetch = useDashboardsFetch(definition?.stream.name);
|
||||
|
||||
if (definition && !dashboardsFetch.loading && dashboardsFetch.value?.dashboards.length === 0) {
|
||||
return (
|
||||
<EuiFlexItem grow>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="center">
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
className={css`
|
||||
max-width: 200px;
|
||||
`}
|
||||
>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<AssetImage type="welcome" />
|
||||
<EuiText size="xs" textAlign="center" color="subdued">
|
||||
{i18n.translate('xpack.streams.entityDetailOverview.linkDashboardsText', {
|
||||
defaultMessage: 'Link dashboards to this stream for quick access',
|
||||
})}
|
||||
</EuiText>
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
<EuiLink
|
||||
href={router.link('/{key}/{tab}', {
|
||||
path: {
|
||||
key: definition?.stream.name,
|
||||
tab: 'dashboards',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{i18n.translate('xpack.streams.entityDetailOverview.addDashboardButton', {
|
||||
defaultMessage: 'Add dashboards',
|
||||
})}
|
||||
</EuiLink>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DashboardsTable
|
||||
dashboards={dashboardsFetch.value?.dashboards ?? EMPTY_DASHBOARD_LIST}
|
||||
loading={dashboardsFetch.loading}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,221 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useMemo } from 'react';
|
||||
import { IngestStreamGetResponse, isWiredStreamDefinition } from '@kbn/streams-schema';
|
||||
import { ILM_LOCATOR_ID, IlmLocatorParams } from '@kbn/index-lifecycle-management-common-shared';
|
||||
|
||||
import { computeInterval } from '@kbn/visualization-utils';
|
||||
import { useKibana } from '../../hooks/use_kibana';
|
||||
import { useStreamsAppFetch } from '../../hooks/use_streams_app_fetch';
|
||||
import { StreamsAppSearchBar } from '../streams_app_search_bar';
|
||||
import { getIndexPatterns } from '../../util/hierarchy_helpers';
|
||||
import { useDataStreamStats } from '../data_management/stream_detail_lifecycle/hooks/use_data_stream_stats';
|
||||
import { QuickLinks } from './quick_links';
|
||||
import { ChildStreamList } from './child_stream_list';
|
||||
import { StreamStatsPanel } from './components/stream_stats_panel';
|
||||
import { StreamChartPanel } from './components/stream_chart_panel';
|
||||
import { TabsPanel } from './components/tabs_panel';
|
||||
|
||||
export function StreamDetailOverview({ definition }: { definition?: IngestStreamGetResponse }) {
|
||||
const {
|
||||
dependencies: {
|
||||
start: {
|
||||
data,
|
||||
dataViews,
|
||||
streams: { streamsRepositoryClient },
|
||||
share,
|
||||
},
|
||||
},
|
||||
} = useKibana();
|
||||
|
||||
const {
|
||||
timeRange,
|
||||
setTimeRange,
|
||||
absoluteTimeRange: { start, end },
|
||||
refreshAbsoluteTimeRange,
|
||||
} = data.query.timefilter.timefilter.useTimefilter();
|
||||
|
||||
const indexPatterns = useMemo(() => {
|
||||
return getIndexPatterns(definition?.stream);
|
||||
}, [definition]);
|
||||
|
||||
const discoverLocator = useMemo(
|
||||
() => share.url.locators.get('DISCOVER_APP_LOCATOR'),
|
||||
[share.url.locators]
|
||||
);
|
||||
|
||||
const bucketSize = useMemo(() => computeInterval(timeRange, data), [data, timeRange]);
|
||||
|
||||
const queries = useMemo(() => {
|
||||
if (!indexPatterns) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const baseQuery = `FROM ${indexPatterns.join(', ')}`;
|
||||
|
||||
const histogramQuery = `${baseQuery} | STATS metric = COUNT(*) BY @timestamp = BUCKET(@timestamp, ${bucketSize})`;
|
||||
|
||||
return {
|
||||
baseQuery,
|
||||
histogramQuery,
|
||||
};
|
||||
}, [bucketSize, indexPatterns]);
|
||||
|
||||
const discoverLink = useMemo(() => {
|
||||
if (!discoverLocator || !queries?.baseQuery) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return discoverLocator.getRedirectUrl({
|
||||
query: {
|
||||
esql: queries.baseQuery,
|
||||
},
|
||||
});
|
||||
}, [queries?.baseQuery, discoverLocator]);
|
||||
|
||||
const histogramQueryFetch = useStreamsAppFetch(
|
||||
async ({ signal }) => {
|
||||
if (!queries?.histogramQuery || !indexPatterns) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const existingIndices = await dataViews.getExistingIndices(indexPatterns);
|
||||
|
||||
if (existingIndices.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return streamsRepositoryClient.fetch('POST /internal/streams/esql', {
|
||||
params: {
|
||||
body: {
|
||||
operationName: 'get_histogram_for_stream',
|
||||
query: queries.histogramQuery,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
},
|
||||
signal,
|
||||
});
|
||||
},
|
||||
[indexPatterns, dataViews, streamsRepositoryClient, queries?.histogramQuery, start, end]
|
||||
);
|
||||
|
||||
const docCountFetch = useStreamsAppFetch(
|
||||
async ({ signal }) => {
|
||||
if (!definition) {
|
||||
return undefined;
|
||||
}
|
||||
return streamsRepositoryClient.fetch('GET /internal/streams/{name}/_details', {
|
||||
signal,
|
||||
params: {
|
||||
path: {
|
||||
name: definition.stream.name,
|
||||
},
|
||||
query: {
|
||||
start: String(start),
|
||||
end: String(end),
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[definition, streamsRepositoryClient, start, end]
|
||||
);
|
||||
|
||||
const dataStreamStats = useDataStreamStats({ definition });
|
||||
|
||||
const tabs = useMemo(
|
||||
() => [
|
||||
...(definition && isWiredStreamDefinition(definition.stream)
|
||||
? [
|
||||
{
|
||||
id: 'streams',
|
||||
name: i18n.translate('xpack.streams.entityDetailOverview.tabs.streams', {
|
||||
defaultMessage: 'Streams',
|
||||
}),
|
||||
content: <ChildStreamList definition={definition} />,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'quicklinks',
|
||||
name: i18n.translate('xpack.streams.entityDetailOverview.tabs.quicklinks', {
|
||||
defaultMessage: 'Quick Links',
|
||||
}),
|
||||
content: <QuickLinks definition={definition} />,
|
||||
},
|
||||
],
|
||||
[definition]
|
||||
);
|
||||
|
||||
const ilmLocator = share.url.locators.get<IlmLocatorParams>(ILM_LOCATOR_ID);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup direction="row" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow>
|
||||
<StreamsAppSearchBar
|
||||
onQuerySubmit={({ dateRange }, isUpdate) => {
|
||||
if (!isUpdate) {
|
||||
if (!refreshAbsoluteTimeRange()) {
|
||||
// if absolute time range didn't change, we need to manually refresh the histogram
|
||||
// otherwise it will be refreshed by the changed absolute time range
|
||||
histogramQueryFetch.refresh();
|
||||
docCountFetch.refresh();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (dateRange) {
|
||||
setTimeRange({ from: dateRange.from, to: dateRange?.to, mode: dateRange.mode });
|
||||
}
|
||||
}}
|
||||
onRefresh={() => {
|
||||
histogramQueryFetch.refresh();
|
||||
docCountFetch.refresh();
|
||||
}}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.streams.entityDetailOverview.searchBarPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Filter data by using KQL',
|
||||
}
|
||||
)}
|
||||
dateRangeFrom={timeRange.from}
|
||||
dateRangeTo={timeRange.to}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<StreamStatsPanel
|
||||
definition={definition}
|
||||
dataStreamStats={dataStreamStats.stats}
|
||||
docCount={docCountFetch.value}
|
||||
ilmLocator={ilmLocator}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow>
|
||||
<EuiFlexGroup direction="row">
|
||||
<EuiFlexItem grow={4}>{definition && <TabsPanel tabs={tabs} />}</EuiFlexItem>
|
||||
<EuiFlexItem grow={8}>
|
||||
<StreamChartPanel
|
||||
histogramQueryFetch={histogramQueryFetch}
|
||||
discoverLink={discoverLink}
|
||||
timerange={{ start, end }}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -45,6 +45,7 @@ export function StreamDetailViewContent({ name, tab }: { name: string; tab: stri
|
|||
label: i18n.translate('xpack.streams.streamDetailView.overviewTab', {
|
||||
defaultMessage: 'Overview',
|
||||
}),
|
||||
background: false,
|
||||
},
|
||||
{
|
||||
name: 'dashboards',
|
||||
|
@ -52,6 +53,7 @@ export function StreamDetailViewContent({ name, tab }: { name: string; tab: stri
|
|||
label: i18n.translate('xpack.streams.streamDetailView.dashboardsTab', {
|
||||
defaultMessage: 'Dashboards',
|
||||
}),
|
||||
background: true,
|
||||
},
|
||||
{
|
||||
name: 'management',
|
||||
|
@ -59,6 +61,7 @@ export function StreamDetailViewContent({ name, tab }: { name: string; tab: stri
|
|||
label: i18n.translate('xpack.streams.streamDetailView.managementTab', {
|
||||
defaultMessage: 'Management',
|
||||
}),
|
||||
background: true,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ export function StreamListView() {
|
|||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<StreamsAppPageBody>
|
||||
<StreamsAppPageBody background>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSearchBar
|
||||
|
|
|
@ -8,7 +8,13 @@ import React from 'react';
|
|||
import { EuiPanel, useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
export function StreamsAppPageBody({ children }: { children: React.ReactNode }) {
|
||||
export function StreamsAppPageBody({
|
||||
children,
|
||||
background,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
background: boolean;
|
||||
}) {
|
||||
const theme = useEuiTheme().euiTheme;
|
||||
return (
|
||||
<EuiPanel
|
||||
|
@ -19,6 +25,7 @@ export function StreamsAppPageBody({ children }: { children: React.ReactNode })
|
|||
border-radius: 0px;
|
||||
display: flex;
|
||||
overflow-y: auto;
|
||||
${!background ? `background-color: transparent;` : ''}
|
||||
`}
|
||||
paddingSize="l"
|
||||
>
|
||||
|
|
|
@ -27,13 +27,11 @@
|
|||
"@kbn/react-kibana-context-render",
|
||||
"@kbn/code-editor",
|
||||
"@kbn/ui-theme",
|
||||
"@kbn/calculate-auto",
|
||||
"@kbn/kibana-react-plugin",
|
||||
"@kbn/es-query",
|
||||
"@kbn/server-route-repository-client",
|
||||
"@kbn/logging",
|
||||
"@kbn/config-schema",
|
||||
"@kbn/calculate-auto",
|
||||
"@kbn/streams-plugin",
|
||||
"@kbn/share-plugin",
|
||||
"@kbn/code-editor",
|
||||
|
@ -59,6 +57,7 @@
|
|||
"@kbn/licensing-plugin",
|
||||
"@kbn/datemath",
|
||||
"@kbn/xstate-utils",
|
||||
"@kbn/visualization-utils",
|
||||
"@kbn/utility-types",
|
||||
"@kbn/discover-utils",
|
||||
"@kbn/discover-shared-plugin",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue