Skip to content

Commit 5efefc1

Browse files
[Streams] Refine Suggestions Before Save (#241439)
## Summary This PR allows user to edit a given partition suggestion before saving it. Cell actions can also be used when editing an AI suggestions same as for the manually created ones. ## 🎥 Demo https://github.com/user-attachments/assets/9b65a095-d1cc-478e-b0e1-6584945b7e74
1 parent 4a8b6a1 commit 5efefc1

File tree

19 files changed

+543
-93
lines changed

19 files changed

+543
-93
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React from 'react';
9+
import { EuiBadge } from '@elastic/eui';
10+
import { i18n } from '@kbn/i18n';
11+
12+
export function DisabledBadge() {
13+
return (
14+
<EuiBadge color="subdued">
15+
{i18n.translate('xpack.streams.streamDetailRouting.disabled', {
16+
defaultMessage: 'Disabled',
17+
})}
18+
</EuiBadge>
19+
);
20+
}

x-pack/platform/plugins/shared/streams_app/public/components/data_management/shared/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ export { PreviewFlyout } from './preview_flyout';
88
export type { DataTableRecordWithIndex } from './preview_flyout';
99
export { MemoPreviewTable } from './preview_table';
1010
export { ConditionPanel, ConditionDisplay, EditableConditionPanel } from './condition_display';
11+
export { VerticalRule } from './vertical_rule';
12+
export { DisabledBadge } from './disabled_badge';
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React from 'react';
9+
import styled from '@emotion/styled';
10+
import { useEuiTheme } from '@elastic/eui';
11+
12+
export function VerticalRule() {
13+
const { euiTheme } = useEuiTheme();
14+
15+
const CentralizedContainer = styled.div`
16+
display: flex;
17+
align-items: center;
18+
padding: 0 ${euiTheme.size.xs};
19+
`;
20+
21+
const Border = styled.div`
22+
height: 20px;
23+
border-right: ${euiTheme.border.thin};
24+
`;
25+
26+
return (
27+
<CentralizedContainer>
28+
<Border />
29+
</CentralizedContainer>
30+
);
31+
}

x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_routing/child_stream_list.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export function ChildStreamList({ availableStreams }: { availableStreams: string
6868
previewSuggestion,
6969
acceptSuggestion,
7070
rejectSuggestion,
71+
updateSuggestion,
7172
} = useReviewSuggestionsForm();
7273

7374
const { currentRuleId, definition, routing } = routingSnapshot.context;
@@ -234,6 +235,7 @@ export function ChildStreamList({ availableStreams }: { availableStreams: string
234235
rejectSuggestion={rejectSuggestion}
235236
resetForm={resetForm}
236237
suggestions={suggestions}
238+
updateSuggestion={updateSuggestion}
237239
/>
238240
)
239241
) : null}

x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_routing/control_bars.tsx

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,51 @@ export const EditRoutingRuleControls = ({
7979
);
8080
};
8181

82+
export const EditSuggestedRuleControls = ({
83+
onSave,
84+
onAccept,
85+
nameError,
86+
conditionError,
87+
}: {
88+
onSave?: () => void;
89+
onAccept: () => void;
90+
nameError?: string;
91+
conditionError?: string;
92+
}) => {
93+
const routingSnapshot = useStreamsRoutingSelector((snapshot) => snapshot);
94+
const { cancelChanges } = useStreamRoutingEvents();
95+
96+
const canSave = routingSnapshot.can({ type: 'suggestion.saveSuggestion' });
97+
const hasPrivileges = routingSnapshot.context.definition.privileges.manage;
98+
99+
const hasValidationErrors = !!nameError || !!conditionError;
100+
const isUpdateDisabled = hasValidationErrors || !canSave;
101+
102+
const handleAccept = () => {
103+
if (onSave) {
104+
onSave();
105+
}
106+
onAccept();
107+
};
108+
109+
return (
110+
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center" wrap>
111+
<EuiFlexItem grow={false}>
112+
<CancelButton onClick={cancelChanges} />
113+
</EuiFlexItem>
114+
<EuiFlexItem grow={false}>
115+
<PrivilegesTooltip hasPrivileges={hasPrivileges}>
116+
<UpdateAndAcceptButton
117+
isLoading={false}
118+
isDisabled={isUpdateDisabled}
119+
onClick={handleAccept}
120+
/>
121+
</PrivilegesTooltip>
122+
</EuiFlexItem>
123+
</EuiFlexGroup>
124+
);
125+
};
126+
82127
const RemoveButton = ({
83128
isDisabled,
84129
onDelete,
@@ -125,8 +170,21 @@ const SaveButton = (props: EuiButtonPropsForButton) => (
125170

126171
const UpdateButton = (props: EuiButtonPropsForButton) => (
127172
<EuiButton data-test-subj="streamsAppStreamDetailRoutingUpdateButton" size="s" fill {...props}>
128-
{i18n.translate('xpack.streams.streamDetailRouting.change', {
129-
defaultMessage: 'Change routing',
173+
{i18n.translate('xpack.streams.streamDetailRouting.update', {
174+
defaultMessage: 'Update',
175+
})}
176+
</EuiButton>
177+
);
178+
179+
const UpdateAndAcceptButton = (props: EuiButtonPropsForButton) => (
180+
<EuiButton
181+
data-test-subj="streamsAppStreamDetailRoutingUpdateAndAcceptButton"
182+
size="s"
183+
fill
184+
{...props}
185+
>
186+
{i18n.translate('xpack.streams.streamDetailRouting.updateAndAccept', {
187+
defaultMessage: 'Update & Accept',
130188
})}
131189
</EuiButton>
132190
);

x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_routing/idle_routing_stream_entry.tsx

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,30 +24,10 @@ import { i18n } from '@kbn/i18n';
2424
import { isDescendantOf, isRoutingEnabled } from '@kbn/streams-schema';
2525
import { css } from '@emotion/css';
2626
import { css as cssReact } from '@emotion/react';
27-
import styled from '@emotion/styled';
2827
import { useStreamsAppRouter } from '../../../hooks/use_streams_app_router';
29-
import { ConditionPanel } from '../shared';
28+
import { ConditionPanel, VerticalRule } from '../shared';
3029
import type { RoutingDefinitionWithUIAttributes } from './types';
31-
32-
function VerticalRule() {
33-
const { euiTheme } = useEuiTheme();
34-
const CentralizedContainer = styled.div`
35-
display: flex;
36-
align-items: center;
37-
padding: 0 ${euiTheme.size.xs};
38-
`;
39-
40-
const Border = styled.div`
41-
height: 20px;
42-
border-right: ${euiTheme.border.thin};
43-
`;
44-
45-
return (
46-
<CentralizedContainer>
47-
<Border />
48-
</CentralizedContainer>
49-
);
50-
}
30+
import { DisabledBadge } from '../shared';
5131

5232
export function IdleRoutingStreamEntry({
5333
availableStreams,
@@ -143,11 +123,7 @@ export function IdleRoutingStreamEntry({
143123
>
144124
{!isRoutingEnabled(routingRule.status) && (
145125
<>
146-
<EuiBadge color="subdued">
147-
{i18n.translate('xpack.streams.streamDetailRouting.disabled', {
148-
defaultMessage: 'Disabled',
149-
})}
150-
</EuiBadge>
126+
<DisabledBadge />
151127
<VerticalRule />
152128
</>
153129
)}

x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_routing/index.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
EuiFlexItem,
1212
EuiPanel,
1313
EuiResizableContainer,
14-
useIsWithinBreakpoints,
1514
} from '@elastic/eui';
1615
import { css } from '@emotion/css';
1716
import type { Streams } from '@kbn/streams-schema';
@@ -99,15 +98,15 @@ export function StreamDetailRoutingImpl() {
9998
useUnsavedChangesPrompt({
10099
hasUnsavedChanges:
101100
routingSnapshot.can({ type: 'routingRule.save' }) ||
102-
routingSnapshot.can({ type: 'routingRule.fork' }),
101+
routingSnapshot.can({ type: 'routingRule.fork' }) ||
102+
routingSnapshot.can({ type: 'suggestion.saveSuggestion' }),
103103
history: appParams.history,
104104
http: core.http,
105105
navigateToUrl: core.application.navigateToUrl,
106106
openConfirm: core.overlays.openConfirm,
107107
});
108108

109109
const availableStreams = streamsListFetch.value?.streams.map((stream) => stream.name) ?? [];
110-
const isVerticalLayout = useIsWithinBreakpoints(['xs', 's']);
111110

112111
return (
113112
<EuiFlexItem
@@ -133,7 +132,7 @@ export function StreamDetailRoutingImpl() {
133132
`}
134133
paddingSize="none"
135134
>
136-
<EuiResizableContainer direction={isVerticalLayout ? 'vertical' : 'horizontal'}>
135+
<EuiResizableContainer>
137136
{(EuiResizablePanel, EuiResizableButton) => (
138137
<>
139138
<EuiResizablePanel

x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_routing/preview_panel.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ export function PreviewPanel() {
5353
content = <EditingPanel />;
5454
} else if (
5555
routingSnapshot.matches({ ready: 'creatingNewRule' }) ||
56-
routingSnapshot.matches({ ready: 'reviewSuggestedRule' })
56+
routingSnapshot.matches({ ready: 'reviewSuggestedRule' }) ||
57+
routingSnapshot.matches({ ready: 'editingSuggestedRule' })
5758
) {
5859
content = <SamplePreviewPanel enableActions />;
5960
}

x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_routing/review_suggestions_form/create_stream_confirmation_modal.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,11 @@ export function CreateStreamConfirmationModal({
8484
forkStream({
8585
destination: partition.name,
8686
where: partition.condition,
87-
}).then(() => onSuccess())
87+
}).then((result) => {
88+
if (result.success) {
89+
onSuccess();
90+
}
91+
})
8892
}
8993
fill
9094
>

x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_routing/review_suggestions_form/review_suggestions_form.tsx

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type {
2020
import {
2121
useStreamSamplesSelector,
2222
useStreamsRoutingSelector,
23+
useStreamRoutingEvents,
2324
} from '../state_management/stream_routing_state_machine';
2425
import { CreateStreamConfirmationModal } from './create_stream_confirmation_modal';
2526
import type { AIFeatures } from '../../../../hooks/use_ai_features';
@@ -32,6 +33,7 @@ export interface ReviewSuggestionsFormProps
3233
| 'previewSuggestion'
3334
| 'acceptSuggestion'
3435
| 'rejectSuggestion'
36+
| 'updateSuggestion'
3537
> {
3638
suggestions: PartitionSuggestion[];
3739
onRegenerate: (connectorId: string) => void;
@@ -48,29 +50,55 @@ export function ReviewSuggestionsForm({
4850
previewSuggestion,
4951
acceptSuggestion,
5052
rejectSuggestion,
53+
updateSuggestion,
5154
onRegenerate,
5255
}: ReviewSuggestionsFormProps) {
5356
const ruleUnderReview = useStreamsRoutingSelector((snapshot) =>
5457
snapshot.matches({ ready: 'reviewSuggestedRule' }) ? snapshot.context.suggestedRuleId : null
5558
);
59+
const editingSuggestion = useStreamsRoutingSelector((snapshot) =>
60+
snapshot.matches({ ready: 'editingSuggestedRule' }) ? snapshot.context.editedSuggestion : null
61+
);
62+
63+
// For the confirmation modal, use edited suggestion if available, otherwise find by name
64+
const partitionForModal =
65+
editingSuggestion || suggestions.find(({ name }) => name === ruleUnderReview)!;
66+
5667
const selectedPreviewName = useStreamSamplesSelector(
5768
({ context }) =>
5869
context.selectedPreview &&
5970
context.selectedPreview.type === 'suggestion' &&
6071
context.selectedPreview.name
6172
);
6273

74+
const { editSuggestion } = useStreamRoutingEvents();
75+
const routingSnapshot = useStreamsRoutingSelector((snapshot) => snapshot);
76+
77+
const handleSave = () => {
78+
const currentEditingIndex = routingSnapshot.context.editingSuggestionIndex;
79+
const currentEditedSuggestion = routingSnapshot.context.editedSuggestion;
80+
81+
if (currentEditingIndex !== null && currentEditedSuggestion) {
82+
updateSuggestion(currentEditingIndex, currentEditedSuggestion);
83+
}
84+
};
85+
6386
return (
6487
<>
65-
{ruleUnderReview && (
88+
{ruleUnderReview && partitionForModal && (
6689
<CreateStreamConfirmationModal
67-
partition={suggestions.find(({ name }) => name === ruleUnderReview)!}
68-
onSuccess={() =>
69-
acceptSuggestion(suggestions.findIndex(({ name }) => name === ruleUnderReview)!)
70-
}
90+
partition={partitionForModal}
91+
onSuccess={() => {
92+
acceptSuggestion(
93+
editingSuggestion
94+
? routingSnapshot.context.editingSuggestionIndex!
95+
: suggestions.findIndex(({ name }) => name === ruleUnderReview)!
96+
);
97+
}}
7198
/>
7299
)}
73100
<EuiCallOut
101+
iconType="sparkles"
74102
title={i18n.translate(
75103
'xpack.streams.reviewSuggestionsForm.euiCallOut.reviewPartitioningSuggestionsLabel',
76104
{ defaultMessage: 'Review partitioning suggestions' }
@@ -95,8 +123,11 @@ export function ReviewSuggestionsForm({
95123
<SuggestedStreamPanel
96124
definition={definition}
97125
partition={partition}
126+
index={index}
98127
onPreview={(toggle) => previewSuggestion(index, toggle)}
99128
onDismiss={() => rejectSuggestion(index, selectedPreviewName === partition.name)}
129+
onEdit={editSuggestion}
130+
onSave={handleSave}
100131
/>
101132
<EuiSpacer size="s" />
102133
</NestedView>

0 commit comments

Comments
 (0)