mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
🌊 Stream overview page (#204079)
Stacked on https://github.com/elastic/kibana/pull/204004 <img width="1275" alt="Screenshot 2024-12-12 at 17 19 58" src="https://github.com/user-attachments/assets/2ad14305-15c0-4522-8e70-5691c50e381b" /> Adds some bits to the stream overview page: * Number of docs for the current time range (let's stop here and don't build more of Kibana) * List of child streams for wired streams * Quick links tab (currently empty) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
2a7a53aaf3
commit
58d1522bfd
8 changed files with 604 additions and 97 deletions
|
@ -222,7 +222,6 @@ async function listManagedStreams({
|
|||
|
||||
const streams = streamsSearchResponse.hits.hits.map((hit) => ({
|
||||
...hit._source!,
|
||||
managed: true,
|
||||
}));
|
||||
|
||||
const privileges = await scopedClusterClient.asCurrentUser.security.hasPrivileges({
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { dashboardRoutes } from './dashboards/route';
|
||||
import { esqlRoutes } from './esql/route';
|
||||
import { deleteStreamRoute } from './streams/delete';
|
||||
import { streamDetailRoute } from './streams/details';
|
||||
import { disableStreamsRoute } from './streams/disable';
|
||||
import { editStreamRoute } from './streams/edit';
|
||||
import { enableStreamsRoute } from './streams/enable';
|
||||
|
@ -33,6 +34,7 @@ export const streamsRouteRepository = {
|
|||
...disableStreamsRoute,
|
||||
...dashboardRoutes,
|
||||
...sampleStreamRoute,
|
||||
...streamDetailRoute,
|
||||
...unmappedFieldsRoute,
|
||||
...schemaFieldsSimulationRoute,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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 { z } from '@kbn/zod';
|
||||
import { notFound, internal } from '@hapi/boom';
|
||||
import { SearchTotalHits } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { createServerRoute } from '../create_server_route';
|
||||
import { DefinitionNotFound } from '../../lib/streams/errors';
|
||||
import { readStream } from '../../lib/streams/stream_crud';
|
||||
|
||||
export interface StreamDetailsResponse {
|
||||
details: {
|
||||
count: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const streamDetailRoute = createServerRoute({
|
||||
endpoint: 'GET /api/streams/{id}/_details',
|
||||
options: {
|
||||
access: 'internal',
|
||||
},
|
||||
security: {
|
||||
authz: {
|
||||
enabled: false,
|
||||
reason:
|
||||
'This API delegates security to the currently logged in user and their Elasticsearch permissions.',
|
||||
},
|
||||
},
|
||||
params: z.object({
|
||||
path: z.object({ id: z.string() }),
|
||||
query: z.object({
|
||||
start: z.string(),
|
||||
end: z.string(),
|
||||
}),
|
||||
}),
|
||||
handler: async ({
|
||||
response,
|
||||
params,
|
||||
request,
|
||||
logger,
|
||||
getScopedClients,
|
||||
}): Promise<StreamDetailsResponse> => {
|
||||
try {
|
||||
const { scopedClusterClient } = await getScopedClients({ request });
|
||||
const streamEntity = await readStream({
|
||||
scopedClusterClient,
|
||||
id: params.path.id,
|
||||
});
|
||||
|
||||
// check doc count
|
||||
const docCountResponse = await scopedClusterClient.asCurrentUser.search({
|
||||
index: streamEntity.name,
|
||||
body: {
|
||||
track_total_hits: true,
|
||||
query: {
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: params.query.start,
|
||||
lte: params.query.end,
|
||||
},
|
||||
},
|
||||
},
|
||||
size: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const count = (docCountResponse.hits.total as SearchTotalHits).value;
|
||||
|
||||
return {
|
||||
details: {
|
||||
count,
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof DefinitionNotFound) {
|
||||
throw notFound(e);
|
||||
}
|
||||
|
||||
throw internal(e);
|
||||
}
|
||||
},
|
||||
});
|
|
@ -4,17 +4,38 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiImage,
|
||||
EuiLoadingSpinner,
|
||||
EuiPanel,
|
||||
EuiTab,
|
||||
EuiTabs,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { calculateAuto } from '@kbn/calculate-auto';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useDateRange } from '@kbn/observability-utils-browser/hooks/use_date_range';
|
||||
import moment from 'moment';
|
||||
import React, { useMemo } from 'react';
|
||||
import { ReadStreamDefinition } from '@kbn/streams-schema';
|
||||
import { css } from '@emotion/css';
|
||||
import { ReadStreamDefinition, isWiredReadStream, isWiredStream } from '@kbn/streams-schema';
|
||||
import { useDateRange } from '@kbn/observability-utils-browser/hooks/use_date_range';
|
||||
import illustration from '../assets/illustration.png';
|
||||
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';
|
||||
|
||||
const formatNumber = (val: number) => {
|
||||
return Number(val).toLocaleString('en', {
|
||||
maximumFractionDigits: 1,
|
||||
});
|
||||
};
|
||||
|
||||
export function StreamDetailOverview({ definition }: { definition?: ReadStreamDefinition }) {
|
||||
const {
|
||||
|
@ -35,18 +56,8 @@ export function StreamDetailOverview({ definition }: { definition?: ReadStreamDe
|
|||
} = useDateRange({ data });
|
||||
|
||||
const indexPatterns = useMemo(() => {
|
||||
if (!definition?.name) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const isRoot = definition.name.indexOf('.') === -1;
|
||||
|
||||
const dataStreamOfDefinition = definition.name;
|
||||
|
||||
return isRoot
|
||||
? [dataStreamOfDefinition, `${dataStreamOfDefinition}.*`]
|
||||
: [`${dataStreamOfDefinition}*`];
|
||||
}, [definition?.name]);
|
||||
return getIndexPatterns(definition);
|
||||
}, [definition]);
|
||||
|
||||
const discoverLocator = useMemo(
|
||||
() => share.url.locators.get('DISCOVER_APP_LOCATOR'),
|
||||
|
@ -111,16 +122,75 @@ export function StreamDetailOverview({ definition }: { definition?: ReadStreamDe
|
|||
[indexPatterns, dataViews, streamsRepositoryClient, queries?.histogramQuery, start, end]
|
||||
);
|
||||
|
||||
const docCountFetch = useStreamsAppFetch(
|
||||
async ({ signal }) => {
|
||||
if (!definition) {
|
||||
return undefined;
|
||||
}
|
||||
return streamsRepositoryClient.fetch('GET /api/streams/{id}/_details', {
|
||||
signal,
|
||||
params: {
|
||||
path: {
|
||||
id: definition.name as string,
|
||||
},
|
||||
query: {
|
||||
start: String(start),
|
||||
end: String(end),
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[definition, dataViews, streamsRepositoryClient, start, end]
|
||||
);
|
||||
|
||||
const [selectedTab, setSelectedTab] = React.useState<string | undefined>(undefined);
|
||||
|
||||
const tabs = [
|
||||
...(definition && isWiredReadStream(definition)
|
||||
? [
|
||||
{
|
||||
id: 'streams',
|
||||
name: i18n.translate('xpack.streams.entityDetailOverview.tabs.streams', {
|
||||
defaultMessage: 'Streams',
|
||||
}),
|
||||
content: <ChildStreamList stream={definition} />,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'quicklinks',
|
||||
name: i18n.translate('xpack.streams.entityDetailOverview.tabs.quicklinks', {
|
||||
defaultMessage: 'Quick Links',
|
||||
}),
|
||||
content: <>TODO</>,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup direction="row" gutterSize="s">
|
||||
<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;
|
||||
}
|
||||
|
||||
|
@ -166,7 +236,112 @@ export function StreamDetailOverview({ definition }: { definition?: ReadStreamDe
|
|||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ChildStreamList({ stream }: { stream?: ReadStreamDefinition }) {
|
||||
const {
|
||||
dependencies: {
|
||||
start: {
|
||||
streams: { streamsRepositoryClient },
|
||||
},
|
||||
},
|
||||
} = useKibana();
|
||||
const router = useStreamsAppRouter();
|
||||
|
||||
const streamsListFetch = useStreamsAppFetch(
|
||||
({ signal }) => {
|
||||
return streamsRepositoryClient.fetch('GET /api/streams', {
|
||||
signal,
|
||||
});
|
||||
},
|
||||
[streamsRepositoryClient]
|
||||
);
|
||||
|
||||
const childDefinitions = useMemo(() => {
|
||||
if (!stream) {
|
||||
return [];
|
||||
}
|
||||
return streamsListFetch.value?.streams.filter(
|
||||
(d) => isWiredStream(d) && d.name.startsWith(stream.name as string)
|
||||
);
|
||||
}, [stream, streamsListFetch.value?.streams]);
|
||||
|
||||
if (stream && childDefinitions?.length === 1) {
|
||||
return (
|
||||
<EuiFlexItem grow>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="center">
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
className={css`
|
||||
max-width: 350px;
|
||||
`}
|
||||
>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiImage
|
||||
src={illustration}
|
||||
alt="Illustration"
|
||||
className={css`
|
||||
width: 250px;
|
||||
`}
|
||||
/>
|
||||
<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
|
||||
iconType="plusInCircle"
|
||||
href={router.link('/{key}/management/{subtab}', {
|
||||
path: {
|
||||
key: stream?.name as string,
|
||||
subtab: 'route',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{i18n.translate('xpack.streams.entityDetailOverview.createChildStream', {
|
||||
defaultMessage: 'Create child stream',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
return <StreamsList definitions={childDefinitions} showControls={false} />;
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import { useStreamsAppFetch } from '../../hooks/use_streams_app_fetch';
|
|||
import { StreamsAppPageHeader } from '../streams_app_page_header';
|
||||
import { StreamsAppPageHeaderTitle } from '../streams_app_page_header/streams_app_page_header_title';
|
||||
import { StreamsAppPageBody } from '../streams_app_page_body';
|
||||
import { StreamsTable } from '../streams_table';
|
||||
import { StreamsList } from '../streams_list';
|
||||
|
||||
export function StreamListView() {
|
||||
const {
|
||||
|
@ -61,7 +61,7 @@ export function StreamListView() {
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow>
|
||||
<StreamsTable listFetch={streamsListFetch} query={query} />
|
||||
<StreamsList definitions={streamsListFetch.value?.streams} query={query} showControls />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</StreamsAppPageBody>
|
||||
|
|
|
@ -0,0 +1,301 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiBadge,
|
||||
EuiButtonEmpty,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
EuiSwitch,
|
||||
EuiTitle,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useMemo } from 'react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { css } from '@emotion/css';
|
||||
import { StreamDefinition, isWiredStream } from '@kbn/streams-schema';
|
||||
import { useStreamsAppRouter } from '../../hooks/use_streams_app_router';
|
||||
import { NestedView } from '../nested_view';
|
||||
import { useKibana } from '../../hooks/use_kibana';
|
||||
import { getIndexPatterns } from '../../util/hierarchy_helpers';
|
||||
|
||||
export interface StreamTree {
|
||||
id: string;
|
||||
type: 'wired' | 'root' | 'classic';
|
||||
definition: StreamDefinition;
|
||||
children: StreamTree[];
|
||||
}
|
||||
|
||||
function asTrees(definitions: StreamDefinition[]) {
|
||||
const trees: StreamTree[] = [];
|
||||
const wiredDefinitions = definitions.filter((definition) => isWiredStream(definition));
|
||||
wiredDefinitions.sort((a, b) => a.name.split('.').length - b.name.split('.').length);
|
||||
|
||||
wiredDefinitions.forEach((definition) => {
|
||||
let currentTree = trees;
|
||||
let existingNode: StreamTree | undefined;
|
||||
// traverse the tree following the prefix of the current id.
|
||||
// once we reach the leaf, the current id is added as child - this works because the ids are sorted by depth
|
||||
while ((existingNode = currentTree.find((node) => definition.name.startsWith(node.id)))) {
|
||||
currentTree = existingNode.children;
|
||||
}
|
||||
if (!existingNode) {
|
||||
const newNode: StreamTree = {
|
||||
id: definition.name,
|
||||
children: [],
|
||||
definition,
|
||||
type: definition.name.split('.').length === 1 ? 'root' : 'wired',
|
||||
};
|
||||
currentTree.push(newNode);
|
||||
}
|
||||
});
|
||||
|
||||
return trees;
|
||||
}
|
||||
|
||||
export function StreamsList({
|
||||
definitions,
|
||||
query,
|
||||
showControls,
|
||||
}: {
|
||||
definitions: StreamDefinition[] | undefined;
|
||||
query?: string;
|
||||
showControls: boolean;
|
||||
}) {
|
||||
const [collapsed, setCollapsed] = React.useState<Record<string, boolean>>({});
|
||||
const [showClassic, setShowClassic] = React.useState(true);
|
||||
const items = useMemo(() => {
|
||||
return definitions ?? [];
|
||||
}, [definitions]);
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
return items
|
||||
.filter((item) => showClassic || isWiredStream(item))
|
||||
.filter((item) => !query || item.name.toLowerCase().includes(query.toLowerCase()));
|
||||
}, [query, items, showClassic]);
|
||||
|
||||
const classicStreams = useMemo(() => {
|
||||
return filteredItems.filter((item) => !isWiredStream(item));
|
||||
}, [filteredItems]);
|
||||
|
||||
const treeView = useMemo(() => {
|
||||
const trees = asTrees(filteredItems);
|
||||
const classicList = classicStreams.map((definition) => ({
|
||||
id: definition.name,
|
||||
type: 'classic' as const,
|
||||
definition,
|
||||
children: [],
|
||||
}));
|
||||
return [...trees, ...classicList];
|
||||
}, [filteredItems, classicStreams]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
{showControls && (
|
||||
<>
|
||||
<EuiTitle size="xxs">
|
||||
<h2>
|
||||
{i18n.translate('xpack.streams.streamsTable.tableTitle', {
|
||||
defaultMessage: 'Streams',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="m" justifyContent="spaceBetween">
|
||||
{Object.keys(collapsed).length === 0 ? (
|
||||
<EuiButtonEmpty
|
||||
iconType="fold"
|
||||
size="s"
|
||||
onClick={() =>
|
||||
setCollapsed(Object.fromEntries(items.map((item) => [item.name, true])))
|
||||
}
|
||||
>
|
||||
{i18n.translate('xpack.streams.streamsTable.collapseAll', {
|
||||
defaultMessage: 'Collapse all',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
) : (
|
||||
<EuiButtonEmpty iconType="unfold" onClick={() => setCollapsed({})} size="s">
|
||||
{i18n.translate('xpack.streams.streamsTable.expandAll', {
|
||||
defaultMessage: 'Expand all',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
<EuiSwitch
|
||||
label={i18n.translate('xpack.streams.streamsTable.showClassicStreams', {
|
||||
defaultMessage: 'Show classic streams',
|
||||
})}
|
||||
compressed
|
||||
checked={showClassic}
|
||||
onChange={(e) => setShowClassic(e.target.checked)}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
{treeView.map((tree) => (
|
||||
<StreamNode key={tree.id} node={tree} collapsed={collapsed} setCollapsed={setCollapsed} />
|
||||
))}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function StreamNode({
|
||||
node,
|
||||
collapsed,
|
||||
setCollapsed,
|
||||
}: {
|
||||
node: StreamTree;
|
||||
collapsed: Record<string, boolean>;
|
||||
setCollapsed: (collapsed: Record<string, boolean>) => void;
|
||||
}) {
|
||||
const router = useStreamsAppRouter();
|
||||
const {
|
||||
dependencies: {
|
||||
start: { share },
|
||||
},
|
||||
} = useKibana();
|
||||
const discoverLocator = useMemo(
|
||||
() => share.url.locators.get('DISCOVER_APP_LOCATOR'),
|
||||
[share.url.locators]
|
||||
);
|
||||
|
||||
const discoverUrl = useMemo(() => {
|
||||
const indexPatterns = getIndexPatterns(node.definition);
|
||||
|
||||
if (!discoverLocator || !indexPatterns) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return discoverLocator.getRedirectUrl({
|
||||
query: {
|
||||
esql: `FROM ${indexPatterns.join(', ')}`,
|
||||
},
|
||||
});
|
||||
}, [discoverLocator, node]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="xs"
|
||||
className={css`
|
||||
margin-top: ${euiThemeVars.euiSizeXS};
|
||||
margin-left: ${euiThemeVars.euiSizeS};
|
||||
`}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
alignItems="center"
|
||||
className={css`
|
||||
padding: ${euiThemeVars.euiSizeXS};
|
||||
border-radius: ${euiThemeVars.euiBorderRadius};
|
||||
&:hover {
|
||||
background-color: ${euiThemeVars.euiColorLightestShade};
|
||||
.links {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`}
|
||||
>
|
||||
{node.children.length > 0 && (
|
||||
// Using a regular button here instead of the EUI one to control styling
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setCollapsed?.({ ...collapsed, [node.id]: !collapsed?.[node.id] });
|
||||
}}
|
||||
className={css`
|
||||
background: none;
|
||||
margin-left: -${euiThemeVars.euiSizeXS};
|
||||
margin-right: ${euiThemeVars.euiSizeXS};
|
||||
`}
|
||||
>
|
||||
<EuiIcon type={collapsed?.[node.id] ? 'arrowRight' : 'arrowDown'} />
|
||||
</button>
|
||||
)}
|
||||
<EuiLink color="text" href={router.link('/{key}', { path: { key: node.id } })}>
|
||||
{node.id}
|
||||
</EuiLink>
|
||||
{node.type === 'root' && (
|
||||
<EuiBadge color="hollow">
|
||||
<EuiIcon type="branch" size="s" />
|
||||
</EuiBadge>
|
||||
)}
|
||||
{node.type === 'classic' && (
|
||||
<EuiBadge color="hollow">
|
||||
<EuiIcon type="bullseye" size="s" />
|
||||
</EuiBadge>
|
||||
)}
|
||||
<EuiFlexGroup
|
||||
className={`links ${css`
|
||||
opacity: 0;
|
||||
`}`}
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
>
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.streams.streamsTable.openInNewTab', {
|
||||
defaultMessage: 'Open in new tab',
|
||||
})}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate('xpack.streams.streamsTable.openInNewTab', {
|
||||
defaultMessage: 'Open in new tab',
|
||||
})}
|
||||
iconType="popout"
|
||||
target="_blank"
|
||||
href={router.link('/{key}', { path: { key: node.id } })}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.streams.streamsTable.openInDiscover', {
|
||||
defaultMessage: 'Open in Discover',
|
||||
})}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
iconType="discoverApp"
|
||||
href={discoverUrl}
|
||||
aria-label={i18n.translate('xpack.streams.streamsTable.openInDiscover', {
|
||||
defaultMessage: 'Open in Discover',
|
||||
})}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.streams.streamsTable.management', {
|
||||
defaultMessage: 'Management',
|
||||
})}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
iconType="gear"
|
||||
aria-label={i18n.translate('xpack.streams.streamsTable.management', {
|
||||
defaultMessage: 'Management',
|
||||
})}
|
||||
href={router.link('/{key}/management', { path: { key: node.id } })}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
{node.children.length > 0 && !collapsed?.[node.id] && (
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="column" gutterSize="xs">
|
||||
{node.children.map((child, index) => (
|
||||
<NestedView key={child.id} last={index === node.children.length - 1}>
|
||||
<StreamNode node={child} collapsed={collapsed} setCollapsed={setCollapsed} />
|
||||
</NestedView>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiBasicTable,
|
||||
EuiBasicTableColumn,
|
||||
EuiFlexGroup,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { AbortableAsyncState } from '@kbn/observability-utils-browser/hooks/use_abortable_async';
|
||||
import React, { useMemo } from 'react';
|
||||
import { isWiredStreamConfig, StreamDefinition } from '@kbn/streams-schema';
|
||||
import { useStreamsAppRouter } from '../../hooks/use_streams_app_router';
|
||||
|
||||
export function StreamsTable({
|
||||
listFetch,
|
||||
query,
|
||||
}: {
|
||||
listFetch: AbortableAsyncState<{ streams: StreamDefinition[] }>;
|
||||
query: string;
|
||||
}) {
|
||||
const router = useStreamsAppRouter();
|
||||
|
||||
const items = useMemo(() => {
|
||||
return listFetch.value?.streams ?? [];
|
||||
}, [listFetch.value?.streams]);
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
if (!query) {
|
||||
return items;
|
||||
}
|
||||
|
||||
return items.filter((item) => item.name.toLowerCase().includes(query.toLowerCase()));
|
||||
}, [query, items]);
|
||||
|
||||
const columns = useMemo<Array<EuiBasicTableColumn<StreamDefinition>>>(() => {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
name: i18n.translate('xpack.streams.streamsTable.nameColumnTitle', {
|
||||
defaultMessage: 'Name',
|
||||
}),
|
||||
render: (_, { name, stream }) => {
|
||||
return (
|
||||
<EuiFlexGroup direction="row" gutterSize="s" alignItems="center">
|
||||
<EuiIcon type={isWiredStreamConfig(stream) ? 'branch' : 'bullseye'} />
|
||||
<EuiLink
|
||||
data-test-subj="logsaiColumnsLink"
|
||||
href={router.link('/{key}', { path: { key: name } })}
|
||||
>
|
||||
{name}
|
||||
</EuiLink>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiTitle size="xxs">
|
||||
<h2>
|
||||
{i18n.translate('xpack.streams.streamsTable.tableTitle', {
|
||||
defaultMessage: 'Streams',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiBasicTable columns={columns} items={filteredItems} loading={listFetch.loading} />
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { StreamDefinition, isIngestStream, isWiredStream } from '@kbn/streams-schema';
|
||||
|
||||
export function getIndexPatterns(definition: StreamDefinition | undefined) {
|
||||
if (!definition) {
|
||||
return undefined;
|
||||
}
|
||||
if (!isWiredStream(definition) && isIngestStream(definition)) {
|
||||
return [definition.name as string];
|
||||
}
|
||||
const isRoot = definition.name.indexOf('.') === -1;
|
||||
const dataStreamOfDefinition = definition.name;
|
||||
return isRoot
|
||||
? [dataStreamOfDefinition, `${dataStreamOfDefinition}.*`]
|
||||
: [`${dataStreamOfDefinition}*`];
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue