🌊 Streams Partitioning design fixes (#206319)

Stacked on
* https://github.com/elastic/kibana/pull/206310
* https://github.com/elastic/kibana/pull/206116

Fixes https://github.com/elastic/streams-program/issues/22
Fixes https://github.com/elastic/streams-program/issues/21

Number of descendants as badge in the list of children
<img width="336" alt="Screenshot 2025-01-13 at 11 35 04"
src="https://github.com/user-attachments/assets/167090d7-f1b0-4f7d-80ba-eda9ea6a21a2"
/>

Better heading formatting
<img width="497" alt="Screenshot 2025-01-13 at 11 35 17"
src="https://github.com/user-attachments/assets/007c173e-accd-4e1f-9ff1-2201b3b5bfde"
/>

Better link to parent stream
<img width="478" alt="Screenshot 2025-01-13 at 11 35 35"
src="https://github.com/user-attachments/assets/3bbdc6c9-ef80-4af4-8c8c-a51b8dff8d47"
/>

Show link to child stream in creation modal
<img width="381" alt="Screenshot 2025-01-13 at 11 44 45"
src="https://github.com/user-attachments/assets/af2d97cc-0b8d-4729-b6db-bbd0b3115f8a"
/>


Show all sub streams that will be deleted as well
<img width="902" alt="Screenshot 2025-01-13 at 11 37 17"
src="https://github.com/user-attachments/assets/eeaf386b-dee1-47c4-a7e7-f4ac8d9940db"
/>

---------

Co-authored-by: Kerry Gallagher <471693+Kerry350@users.noreply.github.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Joe Reuter 2025-01-14 18:13:27 +01:00 committed by GitHub
parent 3a95becf63
commit a0689a2945
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 162 additions and 62 deletions

View file

@ -8,6 +8,8 @@ import {
EuiButton,
EuiButtonEmpty,
EuiFlexGroup,
EuiListGroup,
EuiListGroupItem,
EuiModal,
EuiModalBody,
EuiModalFooter,
@ -19,18 +21,22 @@ import {
import { i18n } from '@kbn/i18n';
import { useAbortController } from '@kbn/observability-utils-browser/hooks/use_abort_controller';
import React from 'react';
import { isDescendantOf } from '@kbn/streams-schema';
import { useKibana } from '../../hooks/use_kibana';
import { useStreamsAppRouter } from '../../hooks/use_streams_app_router';
export function StreamDeleteModal({
closeModal,
clearChildUnderEdit,
refreshDefinition,
id,
availableStreams,
}: {
closeModal: () => void;
clearChildUnderEdit: () => void;
refreshDefinition: () => void;
id: string;
availableStreams: string[];
}) {
const {
core: { notifications },
@ -40,9 +46,13 @@ export function StreamDeleteModal({
},
},
} = useKibana();
const router = useStreamsAppRouter();
const abortController = useAbortController();
const [deleteInProgress, setDeleteInProgress] = React.useState(false);
const modalTitleId = useGeneratedHtmlId();
const streamsToBeDeleted = availableStreams.filter(
(stream) => stream === id || isDescendantOf(id, stream)
);
return (
<EuiModal aria-labelledby={modalTitleId} onClose={closeModal}>
<EuiModalHeader>
@ -54,12 +64,40 @@ export function StreamDeleteModal({
</EuiModalHeader>
<EuiModalBody>
<EuiText>
{i18n.translate('xpack.streams.streamDetailRouting.deleteModalDescription', {
defaultMessage:
'Deleting this stream will remove all of its children and the data will no longer be routed. All existing data will be removed as well.',
})}
</EuiText>
<EuiFlexGroup direction="column" gutterSize="m">
<EuiText>
{i18n.translate('xpack.streams.streamDetailRouting.deleteModalDescription', {
defaultMessage:
'Deleting this stream will remove all of its children and the data will no longer be routed. All existing data will be removed as well.',
})}
</EuiText>
{streamsToBeDeleted.length > 1 && (
<>
<EuiText>
{i18n.translate('xpack.streams.streamDetailRouting.deleteModalStreams', {
defaultMessage: 'The following streams will be deleted:',
})}
</EuiText>
<EuiListGroup flush={true} maxWidth={false}>
{streamsToBeDeleted.map((stream) => (
<li key={stream}>
<EuiListGroupItem
target="_blank"
href={router.link('/{key}/{tab}/{subtab}', {
path: {
key: stream,
tab: 'management',
subtab: 'route',
},
})}
label={stream}
/>
</li>
))}
</EuiListGroup>
</>
)}
</EuiFlexGroup>
</EuiModalBody>
<EuiModalFooter>

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import {
EuiBadge,
DropResult,
EuiButton,
EuiButtonEmpty,
@ -17,6 +18,7 @@ import {
EuiFlexItem,
EuiFormRow,
EuiIcon,
EuiLink,
EuiLoadingSpinner,
EuiPanel,
EuiResizableContainer,
@ -25,6 +27,8 @@ import {
useEuiTheme,
euiDragDropReorder,
DragStart,
EuiBreadcrumbs,
EuiBreadcrumb,
} from '@elastic/eui';
import { css } from '@emotion/css';
import { i18n } from '@kbn/i18n';
@ -35,10 +39,13 @@ import {
StreamChild,
ReadStreamDefinition,
WiredStreamConfigDefinition,
isRoot,
isDescendantOf,
} from '@kbn/streams-schema';
import { useUnsavedChangesPrompt } from '@kbn/unsaved-changes-prompt';
import { AbortableAsyncState } from '@kbn/observability-utils-browser/hooks/use_abortable_async';
import { DraggableProvided } from '@hello-pangea/dnd';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { useKibana } from '../../hooks/use_kibana';
import { useStreamsAppFetch } from '../../hooks/use_streams_app_fetch';
import { StreamsAppSearchBar } from '../streams_app_search_bar';
@ -124,6 +131,24 @@ export function StreamDetailRouting({
const theme = useEuiTheme().euiTheme;
const routingAppState = useRoutingState({ definition });
const {
dependencies: {
start: {
streams: { streamsRepositoryClient },
},
},
} = useKibana();
const streamsListFetch = useStreamsAppFetch(
({ signal }) => {
return streamsRepositoryClient.fetch('GET /api/streams', {
signal,
});
},
[streamsRepositoryClient]
);
const availableStreams = streamsListFetch.value?.streams.map((stream) => stream.name) ?? [];
useUnsavedChangesPrompt({
hasUnsavedChanges:
Boolean(routingAppState.childUnderEdit) || routingAppState.hasChildStreamsOrderChanged,
@ -152,6 +177,7 @@ export function StreamDetailRouting({
clearChildUnderEdit={() => routingAppState.setChildUnderEdit(undefined)}
refreshDefinition={refreshDefinition}
id={routingAppState.childUnderEdit.child.name}
availableStreams={availableStreams}
/>
)}
<EuiFlexGroup
@ -186,7 +212,11 @@ export function StreamDetailRouting({
display: flex;
`}
>
<ChildStreamList definition={definition} routingAppState={routingAppState} />
<ChildStreamList
definition={definition}
routingAppState={routingAppState}
availableStreams={availableStreams}
/>
</EuiResizablePanel>
<EuiResizableButton accountForScrollbars="both" />
@ -229,7 +259,7 @@ function ControlBar({
refreshDefinition: () => void;
}) {
const {
core: { notifications },
core,
dependencies: {
start: {
streams: { streamsRepositoryClient },
@ -237,6 +267,9 @@ function ControlBar({
},
} = useKibana();
const { notifications } = core;
const router = useStreamsAppRouter();
const { signal } = useAbortController();
if (!routingAppState.childUnderEdit && !routingAppState.hasChildStreamsOrderChanged) {
@ -320,6 +353,28 @@ function ControlBar({
title: i18n.translate('xpack.streams.streamDetailRouting.saved', {
defaultMessage: 'Stream saved',
}),
text: toMountPoint(
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButton
size="s"
target="_blank"
href={router.link('/{key}/{tab}/{subtab}', {
path: {
key: routingAppState.childUnderEdit?.child.name!,
tab: 'management',
subtab: 'route',
},
})}
>
{i18n.translate('xpack.streams.streamDetailRouting.view', {
defaultMessage: 'Open stream in new tab',
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>,
core
),
});
routingAppState.setChildUnderEdit(undefined);
refreshDefinition();
@ -469,7 +524,12 @@ function PreviewPanel({
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow>
<EuiText size="s">
<EuiText
size="s"
className={css`
font-weight: bold;
`}
>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiIcon type="inspect" />
{i18n.translate('xpack.streams.streamDetail.preview.header', {
@ -577,6 +637,7 @@ function PreviewPanelIllustration({
function ChildStreamList({
definition,
availableStreams,
routingAppState: {
childUnderEdit,
setChildUnderEdit,
@ -588,6 +649,7 @@ function ChildStreamList({
}: {
definition: ReadStreamDefinition;
routingAppState: ReturnType<typeof useRoutingState>;
availableStreams: string[];
}) {
return (
<EuiFlexGroup
@ -603,6 +665,7 @@ function ChildStreamList({
className={css`
height: 40px;
align-content: center;
font-weight: bold;
`}
>
{i18n.translate('xpack.streams.streamDetailRouting.rules.header', {
@ -617,7 +680,6 @@ function ChildStreamList({
overflow: auto;
`}
>
<PreviousStreamEntry definition={definition} />
<CurrentStreamEntry definition={definition} />
<EuiDragDropContext onDragEnd={onChildStreamDragEnd} onDragStart={onChildStreamDragStart}>
<EuiDroppable droppableId="routing_children_reordering" spacing="none">
@ -655,6 +717,7 @@ function ChildStreamList({
child: newChild,
});
}}
availableStreams={availableStreams}
/>
</NestedView>
)}
@ -713,49 +776,39 @@ function ChildStreamList({
}
function CurrentStreamEntry({ definition }: { definition: ReadStreamDefinition }) {
return (
<EuiFlexItem grow={false}>
<EuiPanel hasShadow={false} hasBorder paddingSize="s">
<EuiText size="s">{definition.name}</EuiText>
<EuiText size="xs" color="subdued">
{i18n.translate('xpack.streams.streamDetailRouting.currentStream', {
defaultMessage: 'Current stream',
})}
</EuiText>
</EuiPanel>
</EuiFlexItem>
);
}
function PreviousStreamEntry({ definition }: { definition: ReadStreamDefinition }) {
const router = useStreamsAppRouter();
const breadcrumbs: EuiBreadcrumb[] = definition.name.split('.').map((_part, pos, parts) => {
const parentId = parts.slice(0, pos + 1).join('.');
const isBreadcrumbsTail = parentId === definition.name;
const parentId = definition.name.split('.').slice(0, -1).join('.');
if (parentId === '') {
return null;
}
return {
text: parentId,
href: isBreadcrumbsTail
? undefined
: router.link('/{key}/{tab}/{subtab}', {
path: {
key: parentId,
tab: 'management',
subtab: 'route',
},
}),
};
});
return (
<EuiFlexItem grow={false}>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="streamsAppPreviousStreamEntryPreviousStreamButton"
href={router.link('/{key}/{tab}/{subtab}', {
path: {
key: parentId,
tab: 'management',
subtab: 'route',
},
<>
{!isRoot(definition.name) && <EuiBreadcrumbs breadcrumbs={breadcrumbs} truncate={false} />}
<EuiFlexItem grow={false}>
<EuiPanel hasShadow={false} hasBorder paddingSize="s">
<EuiText size="s">{definition.name}</EuiText>
<EuiText size="xs" color="subdued">
{i18n.translate('xpack.streams.streamDetailRouting.currentStream', {
defaultMessage: 'Current stream',
})}
>
{i18n.translate('xpack.streams.streamDetailRouting.previousStream', {
defaultMessage: '.. (Previous stream)',
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiText>
</EuiPanel>
</EuiFlexItem>
</>
);
}
@ -765,13 +818,16 @@ function RoutingStreamEntry({
onChildChange,
onEditStateChange,
edit,
availableStreams,
}: {
draggableProvided: DraggableProvided;
child: StreamChild;
onChildChange: (child: StreamChild) => void;
onEditStateChange: () => void;
edit?: boolean;
availableStreams: string[];
}) {
const children = availableStreams.filter((stream) => isDescendantOf(child.name, stream)).length;
const router = useStreamsAppRouter();
return (
<EuiPanel hasShadow={false} hasBorder paddingSize="s">
@ -788,9 +844,24 @@ function RoutingStreamEntry({
<EuiIcon type="grab" />
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="s">{child.name}</EuiText>
</EuiFlexItem>
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiLink
href={router.link('/{key}/{tab}/{subtab}', {
path: { key: child.name, tab: 'management', subtab: 'route' },
})}
data-test-subj="streamsAppRoutingStreamEntryButton"
>
<EuiText size="s">{child.name}</EuiText>
</EuiLink>
{children > 0 && (
<EuiBadge color="hollow">
{i18n.translate('xpack.streams.streamDetailRouting.numberChildren', {
defaultMessage: '{children, plural, one {# child} other {# children}}',
values: { children },
})}
</EuiBadge>
)}
</EuiFlexGroup>
</EuiFlexGroup>
</EuiFlexItem>
<EuiButtonIcon
@ -803,16 +874,6 @@ function RoutingStreamEntry({
defaultMessage: 'Edit',
})}
/>
<EuiButtonIcon
data-test-subj="streamsAppRoutingStreamEntryButton"
iconType="popout"
href={router.link('/{key}/{tab}/{subtab}', {
path: { key: child.name, tab: 'management', subtab: 'route' },
})}
aria-label={i18n.translate('xpack.streams.streamDetailRouting.goto', {
defaultMessage: 'Go to stream',
})}
/>
</EuiFlexGroup>
{child.condition && (
<ConditionEditor

View file

@ -55,6 +55,7 @@
"@kbn/unsaved-changes-prompt",
"@kbn/object-utils",
"@kbn/deeplinks-analytics",
"@kbn/dashboard-plugin"
"@kbn/dashboard-plugin",
"@kbn/react-kibana-mount"
]
}