diff --git a/.swn b/.swn new file mode 100644 index 0000000000000..4df88bc7d564d Binary files /dev/null and b/.swn differ diff --git a/x-pack/platform/packages/shared/kbn-streams-schema/src/fixtures/ingest_stream.ts b/x-pack/platform/packages/shared/kbn-streams-schema/src/fixtures/ingest_stream.ts index dfd1b8eb9d33e..b71c3ba081be5 100644 --- a/x-pack/platform/packages/shared/kbn-streams-schema/src/fixtures/ingest_stream.ts +++ b/x-pack/platform/packages/shared/kbn-streams-schema/src/fixtures/ingest_stream.ts @@ -11,4 +11,10 @@ export const ingestStream = { name: 'logs.nginx', elasticsearch_assets: [], stream: ingestStreamConfig, + privileges: { + manage: true, + monitor: true, + lifecycle: true, + simulate: true, + }, }; diff --git a/x-pack/platform/packages/shared/kbn-streams-schema/src/models/ingest/api.ts b/x-pack/platform/packages/shared/kbn-streams-schema/src/models/ingest/api.ts index 3f659e2cd7e03..75625faaf5e07 100644 --- a/x-pack/platform/packages/shared/kbn-streams-schema/src/models/ingest/api.ts +++ b/x-pack/platform/packages/shared/kbn-streams-schema/src/models/ingest/api.ts @@ -70,6 +70,17 @@ const ingestUpsertRequestSchema: z.Schema = z.union([ unwiredIngestUpsertRequestSchema, ]); +interface IngestStreamPrivileges { + // User can change everything about the stream + manage: boolean; + // User can read stats (like size in bytes) about the stream + monitor: boolean; + // User can change the retention policy of the stream + lifecycle: boolean; + // User can simulate changes to the processing or the mapping of the stream + simulate: boolean; +} + /** * Stream get response */ @@ -77,6 +88,7 @@ interface WiredStreamGetResponse extends StreamGetResponseBase { stream: WiredStreamDefinition; inherited_fields: InheritedFieldDefinition; effective_lifecycle: WiredIngestStreamEffectiveLifecycle; + privileges: IngestStreamPrivileges; } interface UnwiredStreamGetResponse extends StreamGetResponseBase { @@ -84,6 +96,7 @@ interface UnwiredStreamGetResponse extends StreamGetResponseBase { elasticsearch_assets?: ElasticsearchAssets; data_stream_exists: boolean; effective_lifecycle: UnwiredIngestStreamEffectiveLifecycle; + privileges: IngestStreamPrivileges; } type IngestStreamGetResponse = WiredStreamGetResponse | UnwiredStreamGetResponse; @@ -121,12 +134,20 @@ const ingestStreamUpsertRequestSchema: z.Schema = z.u unwiredStreamUpsertRequestSchema, ]); +const ingestStreamPrivilegesSchema: z.Schema = z.object({ + manage: z.boolean(), + monitor: z.boolean(), + lifecycle: z.boolean(), + simulate: z.boolean(), +}); + const wiredStreamGetResponseSchema: z.Schema = z.intersection( streamGetResponseSchemaBase, z.object({ stream: wiredStreamDefinitionSchema, inherited_fields: inheritedFieldDefinitionSchema, effective_lifecycle: wiredIngestStreamEffectiveLifecycleSchema, + privileges: ingestStreamPrivilegesSchema, }) ); @@ -137,6 +158,7 @@ const unwiredStreamGetResponseSchema: z.Schema = z.int elasticsearch_assets: z.optional(elasticsearchAssetsSchema), data_stream_exists: z.boolean(), effective_lifecycle: unwiredIngestStreamEffectiveLifecycleSchema, + privileges: ingestStreamPrivilegesSchema, }) ); diff --git a/x-pack/platform/plugins/shared/streams/server/lib/streams/client.ts b/x-pack/platform/plugins/shared/streams/server/lib/streams/client.ts index 8d9064d1e1d84..e13fbbdfed2bd 100644 --- a/x-pack/platform/plugins/shared/streams/server/lib/streams/client.ts +++ b/x-pack/platform/plugins/shared/streams/server/lib/streams/client.ts @@ -739,6 +739,47 @@ export class StreamsClient { }); } + /** + * Checks whether the user has the required privileges to manage the stream. + * Managing a stream means updating the stream properties. It does not + * include the dashboard links. + */ + async getPrivileges(name: string) { + const privileges = + await this.dependencies.scopedClusterClient.asCurrentUser.security.hasPrivileges({ + cluster: [ + 'manage_index_templates', + 'manage_ingest_pipelines', + 'manage_pipeline', + 'read_pipeline', + ], + index: [ + { + names: [name], + privileges: [ + 'read', + 'write', + 'create', + 'manage', + 'monitor', + 'manage_data_stream_lifecycle', + 'manage_ilm', + ], + }, + ], + }); + + return { + manage: + Object.values(privileges.cluster).every((privilege) => privilege === true) && + Object.values(privileges.index[name]).every((privilege) => privilege === true), + monitor: privileges.index[name].monitor, + lifecycle: + privileges.index[name].manage_data_stream_lifecycle && privileges.index[name].manage_ilm, + simulate: privileges.cluster.read_pipeline && privileges.index[name].create, + }; + } + /** * Creates an on-the-fly ingest stream definition * from a concrete data stream. diff --git a/x-pack/platform/plugins/shared/streams/server/routes/streams/crud/read_stream.ts b/x-pack/platform/plugins/shared/streams/server/routes/streams/crud/read_stream.ts index 7b63c48ba1f8e..9bf9c475846fe 100644 --- a/x-pack/platform/plugins/shared/streams/server/routes/streams/crud/read_stream.ts +++ b/x-pack/platform/plugins/shared/streams/server/routes/streams/crud/read_stream.ts @@ -59,7 +59,7 @@ export async function readStream({ } // These queries are only relavant for IngestStreams - const [ancestors, dataStream] = await Promise.all([ + const [ancestors, dataStream, privileges] = await Promise.all([ streamsClient.getAncestors(name), streamsClient.getDataStream(name).catch((e) => { if (e.statusCode === 404) { @@ -67,17 +67,20 @@ export async function readStream({ } throw e; }), + streamsClient.getPrivileges(name), ]); if (isUnwiredStreamDefinition(streamDefinition)) { return { stream: streamDefinition, - elasticsearch_assets: dataStream - ? await getUnmanagedElasticsearchAssets({ - dataStream, - scopedClusterClient, - }) - : undefined, + privileges, + elasticsearch_assets: + dataStream && privileges.manage + ? await getUnmanagedElasticsearchAssets({ + dataStream, + scopedClusterClient, + }) + : undefined, data_stream_exists: !!dataStream, effective_lifecycle: getDataStreamLifecycle(dataStream), dashboards, @@ -89,6 +92,7 @@ export async function readStream({ const body: WiredStreamGetResponse = { stream: streamDefinition, dashboards, + privileges, queries, effective_lifecycle: findInheritedLifecycle(streamDefinition, ancestors), inherited_fields: getInheritedFieldsFromAncestors(ancestors), diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/app_root/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/app_root/index.tsx index 75c7bb8f6b2eb..3b5bd509346c0 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/app_root/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/app_root/index.tsx @@ -13,12 +13,10 @@ import { RouteRenderer, RouterProvider, } from '@kbn/typed-react-router-config'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { StreamsAppContextProvider } from '../streams_app_context_provider'; import { streamsAppRouter } from '../../routes/config'; import { StreamsAppStartDependencies } from '../../types'; import { StreamsAppServices } from '../../services/types'; -import { HeaderMenuPortal } from '../header_menu'; import { TimeFilterProvider } from '../../hooks/use_timefilter'; export function AppRoot({ @@ -53,28 +51,9 @@ export function AppRoot({ - ); } - -export function StreamsAppHeaderActionMenu({ - appMountParameters, -}: { - appMountParameters: AppMountParameters; -}) { - const { setHeaderActionMenu, theme$ } = appMountParameters; - - return ( - - - - <> - - - - ); -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/management_bottom_bar/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/management_bottom_bar/index.tsx index b484c4f720f1a..b4f54d0aab289 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/management_bottom_bar/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/management_bottom_bar/index.tsx @@ -6,13 +6,14 @@ */ import React from 'react'; -import { EuiButton, EuiButtonEmpty, EuiFlexGroup } from '@elastic/eui'; +import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useDiscardConfirm } from '../../../hooks/use_discard_confirm'; interface ManagementBottomBarProps { confirmButtonText?: string; disabled?: boolean; + insufficientPrivileges?: boolean; isLoading?: boolean; onCancel: () => void; onConfirm: () => void; @@ -22,6 +23,7 @@ export function ManagementBottomBar({ confirmButtonText = defaultConfirmButtonText, disabled = false, isLoading = false, + insufficientPrivileges = false, onCancel, onConfirm, }: ManagementBottomBarProps) { @@ -46,18 +48,31 @@ export function ManagementBottomBar({ defaultMessage: 'Cancel changes', })} - - {confirmButtonText} - + + {confirmButtonText} + + ); } diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/page_content.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/page_content.tsx index b3602d370926a..75ffbdce9c964 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/page_content.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/page_content.tsx @@ -71,6 +71,9 @@ export function StreamDetailEnrichmentContentImpl() { const { resetChanges, saveChanges } = useStreamEnrichmentEvents(); const hasChanges = useStreamsEnrichmentSelector((state) => state.can({ type: 'stream.update' })); + const canManage = useStreamsEnrichmentSelector( + (state) => state.context.definition.privileges.manage + ); const isSavingChanges = useStreamsEnrichmentSelector((state) => state.matches({ ready: { stream: 'updating' } }) ); @@ -124,6 +127,7 @@ export function StreamDetailEnrichmentContentImpl() { onConfirm={saveChanges} isLoading={isSavingChanges} disabled={!hasChanges} + insufficientPrivileges={!canManage} /> @@ -134,6 +138,7 @@ const ProcessorsEditor = React.memo(() => { const { euiTheme } = useEuiTheme(); const { reorderProcessors } = useStreamEnrichmentEvents(); + const definition = useStreamsEnrichmentSelector((state) => state.context.definition); const processorsRefs = useStreamsEnrichmentSelector((state) => state.context.processorsRefs.filter((processorRef) => @@ -222,6 +227,7 @@ const ProcessorsEditor = React.memo(() => { {processorsRefs.map((processorRef, idx) => ( { ))} )} - + {definition.privileges.simulate && } {!isEmpty(errors.ignoredFields) && ( diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/index.tsx index e502c543bbf15..58b3618085598 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors/index.tsx @@ -207,6 +207,7 @@ export interface EditProcessorPanelProps { export function EditProcessorPanel({ processorRef, processorMetrics }: EditProcessorPanelProps) { const { euiTheme } = useEuiTheme(); const state = useSelector(processorRef, (s) => s); + const canEdit = useStreamsEnrichmentSelector((s) => s.context.definition.privileges.manage); const previousProcessor = state.context.previousProcessor; const processor = state.context.processor; @@ -343,6 +344,7 @@ export function EditProcessorPanel({ processorRef, processorMetrics }: EditProce data-test-subj="streamsAppEditProcessorPanelButton" onClick={handleOpen} iconType="pencil" + disabled={!canEdit} color="text" size="xs" aria-label={i18n.translate( diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors_list.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors_list.tsx index 65ed89070436e..2294448058d38 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors_list.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_enrichment/processors_list.tsx @@ -11,13 +11,15 @@ import { EditProcessorPanel, type EditProcessorPanelProps } from './processors'; export const DraggableProcessorListItem = ({ idx, + disableDrag, ...props -}: EditProcessorPanelProps & { idx: number }) => ( +}: EditProcessorPanelProps & { idx: number; disableDrag: boolean }) => ( - - - - - + {definition.privileges.monitor && ( + + + + + + )} - {isIlmLifecycle(definition.effective_lifecycle) ? ( + {definition.privileges.lifecycle && isIlmLifecycle(definition.effective_lifecycle) ? ( diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_lifecycle/metadata.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_lifecycle/metadata.tsx index fda5f1f5c54a6..d49619d858b5e 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_lifecycle/metadata.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_lifecycle/metadata.tsx @@ -29,6 +29,7 @@ import { EuiPanel, EuiPopover, EuiText, + EuiToolTip, formatNumber, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -37,6 +38,7 @@ import { IlmLink } from './ilm_link'; import { useStreamsAppRouter } from '../../../hooks/use_streams_app_router'; import { DataStreamStats } from './hooks/use_data_stream_stats'; import { formatIngestionRate } from './helpers/format_bytes'; +import { PrivilegesWarningIconWrapper } from '../../insufficient_privileges/insufficient_privileges'; export function RetentionMetadata({ definition, @@ -61,16 +63,30 @@ export function RetentionMetadata({ lifecycleActions.length === 0 ? null : ( - {i18n.translate('xpack.streams.entityDetailViewWithoutParams.editDataRetention', { - defaultMessage: 'Edit data retention', - })} - + + {i18n.translate('xpack.streams.entityDetailViewWithoutParams.editDataRetention', { + defaultMessage: 'Edit data retention', + })} + + } isOpen={isMenuOpen} closePopover={closeMenu} @@ -178,15 +194,20 @@ export function RetentionMetadata({ 'Estimated average (stream total size divided by the number of days since creation).', })} value={ - statsError ? ( - '-' - ) : isLoadingStats || !stats ? ( - - ) : stats.bytesPerDay ? ( - formatIngestionRate(stats.bytesPerDay) - ) : ( - '-' - ) + + {statsError ? ( + '-' + ) : isLoadingStats || !stats ? ( + + ) : stats.bytesPerDay ? ( + formatIngestionRate(stats.bytesPerDay) + ) : ( + '-' + )} + } /> @@ -195,13 +216,18 @@ export function RetentionMetadata({ defaultMessage: 'Total doc count', })} value={ - statsError ? ( - '-' - ) : isLoadingStats || !stats ? ( - - ) : ( - formatNumber(stats.totalDocs, '0,0') - ) + + {statsError ? ( + '-' + ) : isLoadingStats || !stats ? ( + + ) : ( + formatNumber(stats.totalDocs, '0,0') + )} + } /> diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_management/classic.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_management/classic.tsx index 5529ed79c37c6..2d08851b18511 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_management/classic.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_management/classic.tsx @@ -75,14 +75,19 @@ export function ClassicStreamDetailManagement({ }; } - tabs.advanced = { - content: ( - - ), - label: i18n.translate('xpack.streams.streamDetailView.advancedTab', { - defaultMessage: 'Advanced', - }), - }; + if (definition.privileges.manage) { + tabs.advanced = { + content: ( + + ), + label: i18n.translate('xpack.streams.streamDetailView.advancedTab', { + defaultMessage: 'Advanced', + }), + }; + } if (!isValidManagementSubTab(subtab)) { return ( diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_routing/child_stream_list.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_routing/child_stream_list.tsx index 70b504f1eb4ac..85c9257784ab3 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_routing/child_stream_list.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_routing/child_stream_list.tsx @@ -13,6 +13,7 @@ import { EuiDroppable, EuiDraggable, EuiButton, + EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { css } from '@emotion/css'; @@ -60,26 +61,39 @@ export function ChildStreamList({ availableStreams }: { availableStreams: string defaultMessage: 'Routing rules', })} - - { - selectChildUnderEdit({ - isNew: true, - child: { - destination: `${definition.stream.name}.child`, - if: cloneDeep(EMPTY_EQUALS_CONDITION), - }, - }); - }} - > - {i18n.translate('xpack.streams.streamDetailRouting.addRule', { - defaultMessage: 'Create child stream', - })} - - + {(definition.privileges.simulate || definition.privileges.manage) && ( + + + { + selectChildUnderEdit({ + isNew: true, + child: { + destination: `${definition.stream.name}.child`, + if: cloneDeep(EMPTY_EQUALS_CONDITION), + }, + }); + }} + > + {i18n.translate('xpack.streams.streamDetailRouting.addRule', { + defaultMessage: 'Create child stream', + })} + + + + )} { routingAppState.setShowDeleteModal(true); @@ -194,19 +194,30 @@ export function ControlBar() { defaultMessage: 'Cancel', })} - - {routingAppState.childUnderEdit && routingAppState.childUnderEdit.isNew - ? i18n.translate('xpack.streams.streamDetailRouting.add', { - defaultMessage: 'Save', - }) - : i18n.translate('xpack.streams.streamDetailRouting.change', { - defaultMessage: 'Change routing', - })} - + + {routingAppState.childUnderEdit && routingAppState.childUnderEdit.isNew + ? i18n.translate('xpack.streams.streamDetailRouting.add', { + defaultMessage: 'Save', + }) + : i18n.translate('xpack.streams.streamDetailRouting.change', { + defaultMessage: 'Change routing', + })} + + ); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_schema_editor/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_schema_editor/index.tsx index 1ae584fec92e3..9ca0cbb525c93 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_schema_editor/index.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/data_management/stream_detail_schema_editor/index.tsx @@ -34,7 +34,7 @@ export const StreamDetailSchemaEditor = ({ definition, refreshDefinition }: Sche onRefreshData={refreshFields} withControls withFieldSimulation - withTableActions={!isRootStreamDefinition(definition.stream)} + withTableActions={!isRootStreamDefinition(definition.stream) && definition.privileges.manage} /> ); }; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/header_menu/header_menu_portal.test.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/header_menu/header_menu_portal.test.tsx deleted file mode 100644 index 055c974dcf6db..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/header_menu/header_menu_portal.test.tsx +++ /dev/null @@ -1,30 +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 { render } from '@testing-library/react'; -import React from 'react'; -import HeaderMenuPortal from './header_menu_portal'; -import { themeServiceMock } from '@kbn/core/public/mocks'; - -describe('HeaderMenuPortal', () => { - describe('when unmounted', () => { - it('calls setHeaderActionMenu with undefined', () => { - const setHeaderActionMenu = jest.fn(); - const theme$ = themeServiceMock.createTheme$(); - - const { unmount } = render( - - test - - ); - - unmount(); - - expect(setHeaderActionMenu).toHaveBeenCalledWith(undefined); - }); - }); -}); diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/header_menu/header_menu_portal.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/header_menu/header_menu_portal.tsx deleted file mode 100644 index b05f0ac0d2011..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/header_menu/header_menu_portal.tsx +++ /dev/null @@ -1,41 +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 React, { useEffect, useMemo, ReactNode } from 'react'; -import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal'; -// FIXME use import { toMountPoint } from '@kbn/react-kibana-mount'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; -import { AppMountParameters } from '@kbn/core/public'; - -export interface HeaderMenuPortalProps { - children: ReactNode; - setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; - theme$: AppMountParameters['theme$']; -} - -// eslint-disable-next-line import/no-default-export -export default function HeaderMenuPortal({ - children, - setHeaderActionMenu, - theme$, -}: HeaderMenuPortalProps) { - const portalNode = useMemo(() => createHtmlPortalNode(), []); - - useEffect(() => { - setHeaderActionMenu((element) => { - const mount = toMountPoint(, { theme$ }); - return mount(element); - }); - - return () => { - portalNode.unmount(); - setHeaderActionMenu(undefined); - }; - }, [portalNode, setHeaderActionMenu, theme$]); - - return {children}; -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/header_menu/index.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/header_menu/index.tsx deleted file mode 100644 index 90987a98e5d33..0000000000000 --- a/x-pack/platform/plugins/shared/streams_app/public/components/header_menu/index.tsx +++ /dev/null @@ -1,20 +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 React, { lazy, Suspense } from 'react'; -import { EuiLoadingSpinner } from '@elastic/eui'; -import type { HeaderMenuPortalProps } from './header_menu_portal'; - -const HeaderMenuPortalLazy = lazy(() => import('./header_menu_portal')); - -export function HeaderMenuPortal(props: HeaderMenuPortalProps) { - return ( - }> - - - ); -} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/insufficient_privileges/insufficient_privileges.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/insufficient_privileges/insufficient_privileges.tsx new file mode 100644 index 0000000000000..b01817c7e05c1 --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/components/insufficient_privileges/insufficient_privileges.tsx @@ -0,0 +1,82 @@ +/* + * 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 React from 'react'; +import useToggle from 'react-use/lib/useToggle'; +import { i18n } from '@kbn/i18n'; +import { + EuiButtonIcon, + EuiButtonIconProps, + EuiPopover, + EuiToolTip, + EuiIcon, + EuiFlexGroup, + EuiButtonIconPropsForButton, +} from '@elastic/eui'; + +const insufficientPrivilegesText = i18n.translate('xpack.streams.insufficientPrivilegesMessage', { + defaultMessage: "You don't have sufficient privileges to access this information.", +}); + +export const PrivilegesWarningIconWrapper = ({ + hasPrivileges, + title, + mode = 'popover', + iconColor = 'warning', + popoverCss, + children, +}: { + hasPrivileges: boolean; + title: string; + mode?: 'tooltip' | 'popover'; + iconColor?: EuiButtonIconPropsForButton['color']; + popoverCss?: EuiButtonIconProps['css']; + children: React.ReactNode; +}) => { + const [isPopoverOpen, togglePopover] = useToggle(false); + + const handleButtonClick = togglePopover; + + if (hasPrivileges) { + return <>{children}; + } + + return mode === 'popover' ? ( + + } + isOpen={isPopoverOpen} + closePopover={togglePopover} + > + {insufficientPrivilegesText} + + ) : ( + + + + {children} + + + ); +}; diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_overview/child_stream_list.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_overview/child_stream_list.tsx index c269aa652fc18..81a286673d1cb 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_overview/child_stream_list.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_overview/child_stream_list.tsx @@ -50,23 +50,25 @@ export function ChildStreamList({ definition }: { definition?: IngestStreamGetRe 'Create sub streams to split out data with different retention policies, schemas, and more.', })} - - - {i18n.translate('xpack.streams.entityDetailOverview.createChildStream', { - defaultMessage: 'Create child stream', - })} - - + {definition.privileges.manage && ( + + + {i18n.translate('xpack.streams.entityDetailOverview.createChildStream', { + defaultMessage: 'Create child stream', + })} + + + )} diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_overview/components/stream_stats_panel.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_overview/components/stream_stats_panel.tsx index dfe3fa8fc45c7..218c50da45c33 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_overview/components/stream_stats_panel.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/stream_detail_overview/components/stream_stats_panel.tsx @@ -24,6 +24,7 @@ import { formatIngestionRate, } from '../../data_management/stream_detail_lifecycle/helpers/format_bytes'; import { useDataStreamStats } from '../../data_management/stream_detail_lifecycle/hooks/use_data_stream_stats'; +import { PrivilegesWarningIconWrapper } from '../../insufficient_privileges/insufficient_privileges'; interface StreamStatsPanelProps { definition: IngestStreamGetResponse; @@ -123,15 +124,25 @@ export function StreamStatsPanel({ definition }: StreamStatsPanelProps) { + {dataStreamStats ? formatNumber(dataStreamStats.totalDocs || 0, 'decimal0') : '-'} + } /> + {dataStreamStats && dataStreamStats.sizeBytes + ? formatBytes(dataStreamStats.sizeBytes) + : '-'} + } withBorder /> @@ -152,7 +163,14 @@ export function StreamStatsPanel({ definition }: StreamStatsPanelProps) { } value={ - dataStreamStats ? formatIngestionRate(dataStreamStats.bytesPerDay || 0, true) : '-' + + {dataStreamStats + ? formatIngestionRate(dataStreamStats.bytesPerDay || 0, true) + : '-'} + } withBorder />