mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[streams] show inheriting streams/inherited retention when updating lifecycle (#210055)
When updating a wired streams lifecycle, the new configuration have side effects that are not obvious. This change surfaces: - the child streams that will inherit the new configuration and thus be updated, if any - from which parent we inherit the new configuration when _resetting to default_ The change also includes a refactor of the retention input validation as a https://github.com/elastic/kibana/pull/208461 follow up -----   --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
72265c026b
commit
f947bd320d
8 changed files with 243 additions and 63 deletions
|
@ -7,5 +7,6 @@
|
|||
|
||||
export * from './type_guards';
|
||||
export * from './hierarchy';
|
||||
export * from './lifecycle';
|
||||
export * from './condition_fields';
|
||||
export * from './condition_to_query_dsl';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { WiredStreamDefinition } from '@kbn/streams-schema';
|
||||
import { WiredStreamDefinition } from '../models/ingest/base';
|
||||
import { findInheritedLifecycle, findInheritingStreams } from './lifecycle';
|
||||
|
||||
describe('Lifecycle helpers', () => {
|
|
@ -5,25 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { WiredStreamDefinition } from '../models/ingest/base';
|
||||
import {
|
||||
WiredIngestStreamEffectiveLifecycle,
|
||||
WiredStreamDefinition,
|
||||
getSegments,
|
||||
isChildOf,
|
||||
isDescendantOf,
|
||||
isInheritLifecycle,
|
||||
} from '@kbn/streams-schema';
|
||||
import { orderBy } from 'lodash';
|
||||
WiredIngestStreamEffectiveLifecycle,
|
||||
} from '../models/ingest/lifecycle';
|
||||
import { isDescendantOf, isChildOf, getSegments } from './hierarchy';
|
||||
|
||||
export function findInheritedLifecycle(
|
||||
definition: WiredStreamDefinition,
|
||||
ancestors: WiredStreamDefinition[]
|
||||
): WiredIngestStreamEffectiveLifecycle {
|
||||
const originDefinition = orderBy(
|
||||
[...ancestors, definition],
|
||||
(parent) => getSegments(parent.name).length,
|
||||
'asc'
|
||||
).findLast(({ ingest }) => !isInheritLifecycle(ingest.lifecycle));
|
||||
const originDefinition = [...ancestors, definition]
|
||||
.sort((a, b) => getSegments(a.name).length - getSegments(b.name).length)
|
||||
.findLast(({ ingest }) => !isInheritLifecycle(ingest.lifecycle));
|
||||
|
||||
if (!originDefinition) {
|
||||
throw new Error('Unable to find inherited lifecycle');
|
|
@ -35,6 +35,8 @@ import {
|
|||
isUnwiredStreamDefinition,
|
||||
isWiredStreamDefinition,
|
||||
streamDefinitionSchema,
|
||||
findInheritedLifecycle,
|
||||
findInheritingStreams,
|
||||
} from '@kbn/streams-schema';
|
||||
import { cloneDeep, keyBy, omit, orderBy } from 'lodash';
|
||||
import { AssetClient } from './assets/asset_client';
|
||||
|
@ -63,7 +65,6 @@ import { updateDataStreamsLifecycle } from './data_streams/manage_data_streams';
|
|||
import { DefinitionNotFoundError } from './errors/definition_not_found_error';
|
||||
import { MalformedStreamIdError } from './errors/malformed_stream_id_error';
|
||||
import { SecurityError } from './errors/security_error';
|
||||
import { findInheritedLifecycle, findInheritingStreams } from './helpers/lifecycle';
|
||||
import { NameTakenError } from './errors/name_taken_error';
|
||||
import { MalformedStreamError } from './errors/malformed_stream_error';
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
InheritedFieldDefinition,
|
||||
StreamGetResponse,
|
||||
WiredStreamGetResponse,
|
||||
findInheritedLifecycle,
|
||||
isGroupStreamDefinition,
|
||||
isUnwiredStreamDefinition,
|
||||
} from '@kbn/streams-schema';
|
||||
|
@ -19,7 +20,6 @@ import {
|
|||
getDataStreamLifecycle,
|
||||
getUnmanagedElasticsearchAssets,
|
||||
} from '../../../lib/streams/stream_crud';
|
||||
import { findInheritedLifecycle } from '../../../lib/streams/helpers/lifecycle';
|
||||
|
||||
export async function readStream({
|
||||
name,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
IlmLocatorParams,
|
||||
Phases,
|
||||
|
@ -16,8 +16,16 @@ import {
|
|||
IngestStreamGetResponse,
|
||||
IngestStreamLifecycle,
|
||||
StreamGetResponse,
|
||||
UnwiredStreamGetResponse,
|
||||
WiredStreamGetResponse,
|
||||
getAncestors,
|
||||
isIlmLifecycle,
|
||||
isUnwiredStreamGetResponse,
|
||||
isWiredStreamDefinition,
|
||||
isWiredStreamGetResponse,
|
||||
findInheritedLifecycle,
|
||||
findInheritingStreams,
|
||||
isDslLifecycle,
|
||||
} from '@kbn/streams-schema';
|
||||
import {
|
||||
EuiButton,
|
||||
|
@ -25,11 +33,12 @@ import {
|
|||
EuiCallOut,
|
||||
EuiContextMenuItem,
|
||||
EuiContextMenuPanel,
|
||||
EuiFieldNumber,
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHighlight,
|
||||
EuiLink,
|
||||
EuiLoadingSpinner,
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
|
@ -46,6 +55,8 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { useBoolean } from '@kbn/react-hooks';
|
||||
import useToggle from 'react-use/lib/useToggle';
|
||||
import { useStreamsAppRouter } from '../../hooks/use_streams_app_router';
|
||||
import { useWiredStreams } from '../../hooks/use_wired_streams';
|
||||
|
||||
export type LifecycleEditAction = 'none' | 'dsl' | 'ilm' | 'inherit';
|
||||
|
||||
|
@ -77,6 +88,16 @@ export function EditLifecycleModal({
|
|||
return <InheritModal {...options} />;
|
||||
}
|
||||
|
||||
const isInvalidRetention = (value: string) => {
|
||||
const num = Number(value);
|
||||
return isNaN(num) || num < 1 || num % 1 > 0;
|
||||
};
|
||||
|
||||
const parseRetentionDuration = (value: string = '') => {
|
||||
const result = /(\d+)([d|m|s|h])/.exec(value);
|
||||
return { value: result?.[1], unit: result?.[2] };
|
||||
};
|
||||
|
||||
function DslModal({ closeModal, definition, updateInProgress, updateLifecycle }: ModalOptions) {
|
||||
const timeUnits = [
|
||||
{ name: 'Days', value: 'd' },
|
||||
|
@ -85,10 +106,24 @@ function DslModal({ closeModal, definition, updateInProgress, updateLifecycle }:
|
|||
{ name: 'Seconds', value: 's' },
|
||||
];
|
||||
|
||||
const [selectedUnit, setSelectedUnit] = useState(timeUnits[0]);
|
||||
const [retentionValue, setRetentionValue] = useState(1);
|
||||
const [noRetention, toggleNoRetention] = useToggle(false);
|
||||
const existingRetention = isDslLifecycle(definition.stream.ingest.lifecycle)
|
||||
? parseRetentionDuration(definition.stream.ingest.lifecycle.dsl.data_retention)
|
||||
: undefined;
|
||||
const [selectedUnit, setSelectedUnit] = useState(
|
||||
(existingRetention && timeUnits.find((unit) => unit.value === existingRetention.unit)) ||
|
||||
timeUnits[0]
|
||||
);
|
||||
const [retentionValue, setRetentionValue] = useState(
|
||||
(existingRetention && existingRetention.value) || '1'
|
||||
);
|
||||
const [noRetention, toggleNoRetention] = useToggle(
|
||||
Boolean(existingRetention && !existingRetention.value)
|
||||
);
|
||||
const [showUnitMenu, { on: openUnitMenu, off: closeUnitMenu }] = useBoolean(false);
|
||||
const invalidRetention = useMemo(
|
||||
() => isInvalidRetention(retentionValue) && !noRetention,
|
||||
[retentionValue, noRetention]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiModal onClose={closeModal}>
|
||||
|
@ -105,20 +140,13 @@ function DslModal({ closeModal, definition, updateInProgress, updateLifecycle }:
|
|||
defaultMessage: 'Specify a custom data retention period for this stream.',
|
||||
})}
|
||||
<EuiSpacer />
|
||||
<EuiFieldNumber
|
||||
data-test-subj="streamsAppDslModalFieldNumber"
|
||||
<EuiFieldText
|
||||
data-test-subj="streamsAppDslModalDaysField"
|
||||
value={retentionValue}
|
||||
onChange={(e) => {
|
||||
const valueAsNumber = e.target.valueAsNumber;
|
||||
if (isNaN(valueAsNumber) || valueAsNumber < 1) {
|
||||
setRetentionValue(1);
|
||||
} else {
|
||||
setRetentionValue(valueAsNumber);
|
||||
}
|
||||
}}
|
||||
min={1}
|
||||
onChange={(e) => setRetentionValue(e.target.value)}
|
||||
disabled={noRetention}
|
||||
fullWidth
|
||||
isInvalid={invalidRetention}
|
||||
append={
|
||||
<EuiPopover
|
||||
isOpen={showUnitMenu}
|
||||
|
@ -155,6 +183,16 @@ function DslModal({ closeModal, definition, updateInProgress, updateLifecycle }:
|
|||
</EuiPopover>
|
||||
}
|
||||
/>
|
||||
{invalidRetention ? (
|
||||
<>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiText color="danger" size="xs">
|
||||
{i18n.translate('xpack.streams.streamDetailLifecycle.invalidRetentionValue', {
|
||||
defaultMessage: 'A positive integer is required',
|
||||
})}
|
||||
</EuiText>
|
||||
</>
|
||||
) : null}
|
||||
<EuiSpacer />
|
||||
<EuiSwitch
|
||||
label={i18n.translate('xpack.streams.streamDetailLifecycle.keepDataIndefinitely', {
|
||||
|
@ -170,10 +208,13 @@ function DslModal({ closeModal, definition, updateInProgress, updateLifecycle }:
|
|||
definition={definition}
|
||||
confirmationLabel="Save"
|
||||
closeModal={closeModal}
|
||||
confirmationIsDisabled={invalidRetention}
|
||||
onConfirm={() => {
|
||||
updateLifecycle({
|
||||
dsl: {
|
||||
data_retention: noRetention ? undefined : `${retentionValue}${selectedUnit.value}`,
|
||||
data_retention: noRetention
|
||||
? undefined
|
||||
: `${Number(retentionValue)}${selectedUnit.value}`,
|
||||
},
|
||||
});
|
||||
}}
|
||||
|
@ -341,7 +382,31 @@ function IlmModal({
|
|||
);
|
||||
}
|
||||
|
||||
function InheritModal({ definition, closeModal, updateInProgress, updateLifecycle }: ModalOptions) {
|
||||
function InheritModal({ definition, ...options }: ModalOptions) {
|
||||
if (isWiredStreamGetResponse(definition)) {
|
||||
return <InheritModalWired definition={definition} {...options} />;
|
||||
} else if (isUnwiredStreamGetResponse(definition)) {
|
||||
return <InheritModalUnwired definition={definition} {...options} />;
|
||||
}
|
||||
}
|
||||
|
||||
function InheritModalWired({
|
||||
definition,
|
||||
closeModal,
|
||||
updateInProgress,
|
||||
updateLifecycle,
|
||||
}: ModalOptions & { definition: WiredStreamGetResponse }) {
|
||||
const { wiredStreams, isLoading: wiredStreamsLoading } = useWiredStreams();
|
||||
|
||||
const parents = useMemo(() => {
|
||||
if (wiredStreamsLoading || !wiredStreams) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const ancestors = getAncestors(definition.stream.name);
|
||||
return wiredStreams.filter((stream) => ancestors.includes(stream.name));
|
||||
}, [definition, wiredStreams, wiredStreamsLoading]);
|
||||
|
||||
return (
|
||||
<EuiModal onClose={closeModal}>
|
||||
<EuiModalHeader>
|
||||
|
@ -353,16 +418,67 @@ function InheritModal({ definition, closeModal, updateInProgress, updateLifecycl
|
|||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
{isWiredStreamGetResponse(definition)
|
||||
? i18n.translate('xpack.streams.streamDetailLifecycle.defaultLifecycleWiredDesc', {
|
||||
defaultMessage:
|
||||
'All custom retention settings for this stream will be removed, resetting it to inherit data retention from its nearest parent.',
|
||||
})
|
||||
: i18n.translate('xpack.streams.streamDetailLifecycle.defaultLifecycleUnwiredDesc', {
|
||||
defaultMessage:
|
||||
'All custom retention settings for this stream will be removed, resetting it to use the configuration of the data stream.',
|
||||
})}
|
||||
<EuiSpacer />
|
||||
{i18n.translate('xpack.streams.streamDetailLifecycle.defaultLifecycleWiredDesc', {
|
||||
defaultMessage:
|
||||
'All custom retention settings for this stream will be removed, resetting it to inherit data retention from',
|
||||
})}{' '}
|
||||
{wiredStreamsLoading || !parents ? (
|
||||
<EuiLoadingSpinner size="s" />
|
||||
) : (
|
||||
<>
|
||||
<LinkToStream
|
||||
name={
|
||||
findInheritedLifecycle(
|
||||
{
|
||||
...definition.stream,
|
||||
ingest: { ...definition.stream.ingest, lifecycle: { inherit: {} } },
|
||||
},
|
||||
parents
|
||||
).from
|
||||
}
|
||||
/>
|
||||
.
|
||||
</>
|
||||
)}
|
||||
</EuiModalBody>
|
||||
|
||||
<ModalFooter
|
||||
definition={definition}
|
||||
confirmationLabel={i18n.translate(
|
||||
'xpack.streams.streamDetailLifecycle.defaultLifecycleAction',
|
||||
{
|
||||
defaultMessage: 'Set to default',
|
||||
}
|
||||
)}
|
||||
closeModal={closeModal}
|
||||
onConfirm={() => updateLifecycle({ inherit: {} })}
|
||||
updateInProgress={updateInProgress}
|
||||
/>
|
||||
</EuiModal>
|
||||
);
|
||||
}
|
||||
|
||||
function InheritModalUnwired({
|
||||
definition,
|
||||
closeModal,
|
||||
updateInProgress,
|
||||
updateLifecycle,
|
||||
}: ModalOptions & { definition: UnwiredStreamGetResponse }) {
|
||||
return (
|
||||
<EuiModal onClose={closeModal}>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
{i18n.translate('xpack.streams.streamDetailLifecycle.defaultLifecycleTitle', {
|
||||
defaultMessage: 'Set data retention to default',
|
||||
})}
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
{i18n.translate('xpack.streams.streamDetailLifecycle.defaultLifecycleUnwiredDesc', {
|
||||
defaultMessage:
|
||||
'All custom retention settings for this stream will be removed, resetting it to use the configuration of the data stream.',
|
||||
})}
|
||||
</EuiModalBody>
|
||||
|
||||
<ModalFooter
|
||||
|
@ -391,6 +507,17 @@ function ModalFooter({
|
|||
onConfirm: () => void;
|
||||
closeModal: () => void;
|
||||
}) {
|
||||
const { wiredStreams, isLoading: wiredStreamsLoading } = useWiredStreams();
|
||||
const inheritingStreams = useMemo(() => {
|
||||
if (!isWiredStreamGetResponse(definition) || wiredStreamsLoading || !wiredStreams) {
|
||||
return [];
|
||||
}
|
||||
return findInheritingStreams(
|
||||
definition.stream,
|
||||
wiredStreams.filter(isWiredStreamDefinition)
|
||||
).filter((name) => name !== definition.stream.name);
|
||||
}, [definition, wiredStreams, wiredStreamsLoading]);
|
||||
|
||||
return (
|
||||
<EuiModalFooter>
|
||||
<EuiFlexGroup direction="column">
|
||||
|
@ -413,6 +540,27 @@ function ModalFooter({
|
|||
'Data retention changes will apply to dependant streams unless they already have custom retention settings in place.',
|
||||
}
|
||||
)}
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
{wiredStreamsLoading ? (
|
||||
<EuiLoadingSpinner size="s" />
|
||||
) : inheritingStreams.length > 0 ? (
|
||||
<>
|
||||
{i18n.translate('xpack.streams.streamDetailLifecycle.inheritingChildStreams', {
|
||||
defaultMessage: 'The following child streams will be updated:',
|
||||
})}{' '}
|
||||
{inheritingStreams.map((name) => (
|
||||
<>
|
||||
{' '}
|
||||
<LinkToStream name={name} />{' '}
|
||||
</>
|
||||
))}
|
||||
.
|
||||
</>
|
||||
) : (
|
||||
'No child streams will be updated.'
|
||||
)}
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
</EuiFlexItem>
|
||||
|
@ -450,3 +598,22 @@ function ModalFooter({
|
|||
</EuiModalFooter>
|
||||
);
|
||||
}
|
||||
|
||||
function LinkToStream({ name }: { name: string }) {
|
||||
const router = useStreamsAppRouter();
|
||||
|
||||
return (
|
||||
<EuiLink
|
||||
data-test-subj="streamsAppLinkToStreamLink"
|
||||
target="_blank"
|
||||
href={router.link('/{key}/{tab}', {
|
||||
path: {
|
||||
key: name,
|
||||
tab: 'overview',
|
||||
},
|
||||
})}
|
||||
>
|
||||
[{name}]
|
||||
</EuiLink>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import React, { useMemo } from 'react';
|
|||
import { css } from '@emotion/css';
|
||||
import {
|
||||
IngestStreamGetResponse,
|
||||
isDescendantOf,
|
||||
isUnwiredStreamGetResponse,
|
||||
isWiredStreamDefinition,
|
||||
} from '@kbn/streams-schema';
|
||||
|
@ -36,6 +37,7 @@ 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', {
|
||||
|
@ -288,34 +290,18 @@ function QuickLinks({ definition }: { definition?: IngestStreamGetResponse }) {
|
|||
}
|
||||
|
||||
function ChildStreamList({ definition }: { definition?: IngestStreamGetResponse }) {
|
||||
const {
|
||||
dependencies: {
|
||||
start: {
|
||||
streams: { streamsRepositoryClient },
|
||||
},
|
||||
},
|
||||
} = useKibana();
|
||||
const router = useStreamsAppRouter();
|
||||
|
||||
const streamsListFetch = useStreamsAppFetch(
|
||||
({ signal }) => {
|
||||
return streamsRepositoryClient.fetch('GET /api/streams', {
|
||||
signal,
|
||||
});
|
||||
},
|
||||
[streamsRepositoryClient]
|
||||
);
|
||||
const { wiredStreams } = useWiredStreams();
|
||||
|
||||
const childrenStreams = useMemo(() => {
|
||||
if (!definition) {
|
||||
return [];
|
||||
}
|
||||
return streamsListFetch.value?.streams.filter(
|
||||
(d) => isWiredStreamDefinition(d) && d.name.startsWith(definition.stream.name)
|
||||
);
|
||||
}, [definition, streamsListFetch.value?.streams]);
|
||||
return wiredStreams?.filter((d) => isDescendantOf(definition.stream.name, d.name));
|
||||
}, [definition, wiredStreams]);
|
||||
|
||||
if (definition && childrenStreams?.length === 1) {
|
||||
if (definition && childrenStreams?.length === 0) {
|
||||
return (
|
||||
<EuiFlexItem grow>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="center">
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { isWiredStreamDefinition } from '@kbn/streams-schema';
|
||||
import { useKibana } from './use_kibana';
|
||||
import { useStreamsAppFetch } from './use_streams_app_fetch';
|
||||
|
||||
export const useWiredStreams = () => {
|
||||
const {
|
||||
dependencies: {
|
||||
start: {
|
||||
streams: { streamsRepositoryClient },
|
||||
},
|
||||
},
|
||||
} = useKibana();
|
||||
|
||||
const result = useStreamsAppFetch(
|
||||
async ({ signal }) => streamsRepositoryClient.fetch('GET /api/streams', { signal }),
|
||||
[streamsRepositoryClient]
|
||||
);
|
||||
|
||||
return {
|
||||
wiredStreams: result.value?.streams.filter(isWiredStreamDefinition),
|
||||
isLoading: result.loading,
|
||||
};
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue