diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_overview/index.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_overview/index.tsx
index 1fdc95821172e..6feed8fb004be 100644
--- a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_overview/index.tsx
+++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_detail_overview/index.tsx
@@ -7,14 +7,15 @@
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiPanel } 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 { useDateRange } from '@kbn/observability-utils-browser/hooks/use_date_range';
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';
export function StreamDetailOverview({ definition }: { definition?: ReadStreamDefinition }) {
const {
@@ -35,18 +36,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'),
diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_list_view/index.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_list_view/index.tsx
index 7ffda5f40295a..926b9d746f924 100644
--- a/x-pack/solutions/observability/plugins/streams_app/public/components/stream_list_view/index.tsx
+++ b/x-pack/solutions/observability/plugins/streams_app/public/components/stream_list_view/index.tsx
@@ -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() {
/>
-
+
diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/streams_list/index.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/streams_list/index.tsx
new file mode 100644
index 0000000000000..68c9403e39a27
--- /dev/null
+++ b/x-pack/solutions/observability/plugins/streams_app/public/components/streams_list/index.tsx
@@ -0,0 +1,277 @@
+/*
+ * 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,
+} 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 { AbortableAsyncState } from '@kbn/observability-ai-assistant-plugin/public';
+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({
+ listFetch,
+ query,
+}: {
+ listFetch: AbortableAsyncState<{ streams: StreamDefinition[] }>;
+ query: string;
+}) {
+ const [collapsed, setCollapsed] = React.useState>({});
+ const [showClassic, setShowClassic] = React.useState(true);
+ const items = useMemo(() => {
+ return listFetch.value?.streams ?? [];
+ }, [listFetch.value?.streams]);
+
+ 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 (
+
+
+
+ {i18n.translate('xpack.streams.streamsTable.tableTitle', {
+ defaultMessage: 'Streams',
+ })}
+
+
+
+
+ {Object.keys(collapsed).length === 0 ? (
+
+ setCollapsed(Object.fromEntries(items.map((item) => [item.name, true])))
+ }
+ >
+ {i18n.translate('xpack.streams.streamsTable.collapseAll', {
+ defaultMessage: 'Collapse all',
+ })}
+
+ ) : (
+ setCollapsed({})} size="s">
+ {i18n.translate('xpack.streams.streamsTable.expandAll', {
+ defaultMessage: 'Expand all',
+ })}
+
+ )}
+ setShowClassic(e.target.checked)}
+ />
+
+
+
+ {treeView.map((tree) => (
+
+ ))}
+
+
+ );
+}
+
+function StreamNode({
+ node,
+ collapsed,
+ setCollapsed,
+}: {
+ node: StreamTree;
+ collapsed: Record;
+ setCollapsed: (collapsed: Record) => 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.definition]);
+
+ return (
+
+
+ {node.children.length > 0 && (
+ // Using a regular button here instead of the EUI one to control styling
+
+ )}
+
+ {node.id}
+
+ {node.type === 'root' && (
+
+
+
+ )}
+ {node.type === 'classic' && (
+
+
+
+ )}
+
+
+
+
+
+
+ {node.children.length > 0 && !collapsed?.[node.id] && (
+
+
+ {node.children.map((child, index) => (
+
+
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/x-pack/solutions/observability/plugins/streams_app/public/components/streams_table/index.tsx b/x-pack/solutions/observability/plugins/streams_app/public/components/streams_table/index.tsx
deleted file mode 100644
index ef80d1346edd4..0000000000000
--- a/x-pack/solutions/observability/plugins/streams_app/public/components/streams_table/index.tsx
+++ /dev/null
@@ -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>>(() => {
- return [
- {
- field: 'name',
- name: i18n.translate('xpack.streams.streamsTable.nameColumnTitle', {
- defaultMessage: 'Name',
- }),
- render: (_, { name, stream }) => {
- return (
-
-
-
- {name}
-
-
- );
- },
- },
- ];
- }, [router]);
-
- return (
-
-
-
- {i18n.translate('xpack.streams.streamsTable.tableTitle', {
- defaultMessage: 'Streams',
- })}
-
-
-
-
- );
-}
diff --git a/x-pack/solutions/observability/plugins/streams_app/public/util/hierarchy_helpers.ts b/x-pack/solutions/observability/plugins/streams_app/public/util/hierarchy_helpers.ts
new file mode 100644
index 0000000000000..889c396679ea7
--- /dev/null
+++ b/x-pack/solutions/observability/plugins/streams_app/public/util/hierarchy_helpers.ts
@@ -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 { ReadStreamDefinition, isIngestReadStream, isWiredReadStream } from '@kbn/streams-schema';
+
+export function getIndexPatterns(definition: ReadStreamDefinition | undefined) {
+ if (!definition) {
+ return undefined;
+ }
+ if (!isWiredReadStream(definition) && isIngestReadStream(definition)) {
+ return [definition.name as string];
+ }
+ const isRoot = definition.name.indexOf('.') === -1;
+ const dataStreamOfDefinition = definition.name;
+ return isRoot
+ ? [dataStreamOfDefinition, `${dataStreamOfDefinition}.*`]
+ : [`${dataStreamOfDefinition}*`];
+}
diff --git a/x-pack/solutions/observability/plugins/streams_app/tsconfig.json b/x-pack/solutions/observability/plugins/streams_app/tsconfig.json
index 7824c84d6ea6b..49e5680f68a2c 100644
--- a/x-pack/solutions/observability/plugins/streams_app/tsconfig.json
+++ b/x-pack/solutions/observability/plugins/streams_app/tsconfig.json
@@ -38,5 +38,6 @@
"@kbn/navigation-plugin",
"@kbn/core-notifications-browser",
"@kbn/streams-schema",
+ "@kbn/observability-ai-assistant-plugin",
]
}