mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [🌊 Streams: Permission handling (#217353)](https://github.com/elastic/kibana/pull/217353) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Joe Reuter","email":"johannes.reuter@elastic.co"},"sourceCommit":{"committedDate":"2025-04-08T13:42:29Z","message":"🌊 Streams: Permission handling (#217353)\n\nCurrently, the streams UI doesn't deal well with partial permissions.\nThis PR improves that. As a lot of things come together in streams, we\ncould do even better, but I think it's OK to draw a line somewhere.\n\nThe logic is now as follows:\nWhen reading a stream, the privileges of the current user are returned\nalong with the stream itself. These are grouped like this:\n```\ninterface IngestStreamPrivileges {\n // User can change everything about the stream\n manage: boolean;\n // User can read stats (like size in bytes) about the stream\n monitor: boolean;\n // User can change the retention policy of the stream\n lifecycle: boolean;\n // User can simulate changes to the processing or the mapping of the stream\n simulate: boolean;\n}\n```\n\nThis is part of the definition response and is passed around to the\ncomponents and disabled buttons and similar in the places where this is\nnecessary.\n\nThe \"advanced\" tab is only shown when full `manage` permissions are\npresent - there constellations of permissions that would allow some\naccess but not all (e.g. having `read_pipelines` but not\n`manage_index_templates`), but these should be rather rare and not worth\nthe additional effort.\n\n## Conditions\n\nIn the following places privileges are checked:\n* Overview\n * Without `monitor`, the overall stats are not shown\n* Enrichment\n * Without `manage`, you can't save changes\n * Without `simulate`, the UI is readonly\n* Partitioning\n * Without `manage`, you can't save changes\n * Without `simulate`, the UI is readonly\n* Schema editor\n * Without `manage`, the UI is readonly\n* Retention\n * Without `monitor`, the ingest stats are not shown\n* Without `lifecycle`, the retention can't be changed and ILM breakdown\nis not rendered\n* Advanced\n * Without `manage`, the tab is hidden completely\n\n## Drive-by fix\n\nI noticed that we still register the app header action menu which adds\nan empty bar on serverless, removed that code.\n\n## Testing\n\nCheck\nhttps://github.com/elastic/kibana/pull/217353/files#diff-d8f33d7021058bf90cbeea908bf399da2af50d8b8bfac8a07f160ddc0cdff12bR747\nfor which Elasticsearch level privileges you need for different\npermutations. Then set up a role and a user and log in as that user.\n\nAlso test the different pre-defined roles on serverless.","sha":"fd374463f74caac17b07120c34d2fc6c8e5e2754","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:obs-ux-logs","backport:version","Feature:Streams","v9.1.0","v8.19.0"],"title":"🌊 Streams: Permission handling","number":217353,"url":"https://github.com/elastic/kibana/pull/217353","mergeCommit":{"message":"🌊 Streams: Permission handling (#217353)\n\nCurrently, the streams UI doesn't deal well with partial permissions.\nThis PR improves that. As a lot of things come together in streams, we\ncould do even better, but I think it's OK to draw a line somewhere.\n\nThe logic is now as follows:\nWhen reading a stream, the privileges of the current user are returned\nalong with the stream itself. These are grouped like this:\n```\ninterface IngestStreamPrivileges {\n // User can change everything about the stream\n manage: boolean;\n // User can read stats (like size in bytes) about the stream\n monitor: boolean;\n // User can change the retention policy of the stream\n lifecycle: boolean;\n // User can simulate changes to the processing or the mapping of the stream\n simulate: boolean;\n}\n```\n\nThis is part of the definition response and is passed around to the\ncomponents and disabled buttons and similar in the places where this is\nnecessary.\n\nThe \"advanced\" tab is only shown when full `manage` permissions are\npresent - there constellations of permissions that would allow some\naccess but not all (e.g. having `read_pipelines` but not\n`manage_index_templates`), but these should be rather rare and not worth\nthe additional effort.\n\n## Conditions\n\nIn the following places privileges are checked:\n* Overview\n * Without `monitor`, the overall stats are not shown\n* Enrichment\n * Without `manage`, you can't save changes\n * Without `simulate`, the UI is readonly\n* Partitioning\n * Without `manage`, you can't save changes\n * Without `simulate`, the UI is readonly\n* Schema editor\n * Without `manage`, the UI is readonly\n* Retention\n * Without `monitor`, the ingest stats are not shown\n* Without `lifecycle`, the retention can't be changed and ILM breakdown\nis not rendered\n* Advanced\n * Without `manage`, the tab is hidden completely\n\n## Drive-by fix\n\nI noticed that we still register the app header action menu which adds\nan empty bar on serverless, removed that code.\n\n## Testing\n\nCheck\nhttps://github.com/elastic/kibana/pull/217353/files#diff-d8f33d7021058bf90cbeea908bf399da2af50d8b8bfac8a07f160ddc0cdff12bR747\nfor which Elasticsearch level privileges you need for different\npermutations. Then set up a role and a user and log in as that user.\n\nAlso test the different pre-defined roles on serverless.","sha":"fd374463f74caac17b07120c34d2fc6c8e5e2754"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/217353","number":217353,"mergeCommit":{"message":"🌊 Streams: Permission handling (#217353)\n\nCurrently, the streams UI doesn't deal well with partial permissions.\nThis PR improves that. As a lot of things come together in streams, we\ncould do even better, but I think it's OK to draw a line somewhere.\n\nThe logic is now as follows:\nWhen reading a stream, the privileges of the current user are returned\nalong with the stream itself. These are grouped like this:\n```\ninterface IngestStreamPrivileges {\n // User can change everything about the stream\n manage: boolean;\n // User can read stats (like size in bytes) about the stream\n monitor: boolean;\n // User can change the retention policy of the stream\n lifecycle: boolean;\n // User can simulate changes to the processing or the mapping of the stream\n simulate: boolean;\n}\n```\n\nThis is part of the definition response and is passed around to the\ncomponents and disabled buttons and similar in the places where this is\nnecessary.\n\nThe \"advanced\" tab is only shown when full `manage` permissions are\npresent - there constellations of permissions that would allow some\naccess but not all (e.g. having `read_pipelines` but not\n`manage_index_templates`), but these should be rather rare and not worth\nthe additional effort.\n\n## Conditions\n\nIn the following places privileges are checked:\n* Overview\n * Without `monitor`, the overall stats are not shown\n* Enrichment\n * Without `manage`, you can't save changes\n * Without `simulate`, the UI is readonly\n* Partitioning\n * Without `manage`, you can't save changes\n * Without `simulate`, the UI is readonly\n* Schema editor\n * Without `manage`, the UI is readonly\n* Retention\n * Without `monitor`, the ingest stats are not shown\n* Without `lifecycle`, the retention can't be changed and ILM breakdown\nis not rendered\n* Advanced\n * Without `manage`, the tab is hidden completely\n\n## Drive-by fix\n\nI noticed that we still register the app header action menu which adds\nan empty bar on serverless, removed that code.\n\n## Testing\n\nCheck\nhttps://github.com/elastic/kibana/pull/217353/files#diff-d8f33d7021058bf90cbeea908bf399da2af50d8b8bfac8a07f160ddc0cdff12bR747\nfor which Elasticsearch level privileges you need for different\npermutations. Then set up a role and a user and log in as that user.\n\nAlso test the different pre-defined roles on serverless.","sha":"fd374463f74caac17b07120c34d2fc6c8e5e2754"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT-->
This commit is contained in:
parent
8bdecd6641
commit
eba8935713
22 changed files with 384 additions and 235 deletions
BIN
.swn
Normal file
BIN
.swn
Normal file
Binary file not shown.
|
@ -11,4 +11,10 @@ export const ingestStream = {
|
|||
name: 'logs.nginx',
|
||||
elasticsearch_assets: [],
|
||||
stream: ingestStreamConfig,
|
||||
privileges: {
|
||||
manage: true,
|
||||
monitor: true,
|
||||
lifecycle: true,
|
||||
simulate: true,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -70,6 +70,17 @@ const ingestUpsertRequestSchema: z.Schema<IngestUpsertRequest> = 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<IngestStreamUpsertRequest> = z.u
|
|||
unwiredStreamUpsertRequestSchema,
|
||||
]);
|
||||
|
||||
const ingestStreamPrivilegesSchema: z.Schema<IngestStreamPrivileges> = z.object({
|
||||
manage: z.boolean(),
|
||||
monitor: z.boolean(),
|
||||
lifecycle: z.boolean(),
|
||||
simulate: z.boolean(),
|
||||
});
|
||||
|
||||
const wiredStreamGetResponseSchema: z.Schema<WiredStreamGetResponse> = z.intersection(
|
||||
streamGetResponseSchemaBase,
|
||||
z.object({
|
||||
stream: wiredStreamDefinitionSchema,
|
||||
inherited_fields: inheritedFieldDefinitionSchema,
|
||||
effective_lifecycle: wiredIngestStreamEffectiveLifecycleSchema,
|
||||
privileges: ingestStreamPrivilegesSchema,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -137,6 +158,7 @@ const unwiredStreamGetResponseSchema: z.Schema<UnwiredStreamGetResponse> = z.int
|
|||
elasticsearch_assets: z.optional(elasticsearchAssetsSchema),
|
||||
data_stream_exists: z.boolean(),
|
||||
effective_lifecycle: unwiredIngestStreamEffectiveLifecycleSchema,
|
||||
privileges: ingestStreamPrivilegesSchema,
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -713,6 +713,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.
|
||||
|
|
|
@ -49,7 +49,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) {
|
||||
|
@ -57,17 +57,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,
|
||||
|
@ -78,6 +81,7 @@ export async function readStream({
|
|||
const body: WiredStreamGetResponse = {
|
||||
stream: streamDefinition,
|
||||
dashboards,
|
||||
privileges,
|
||||
effective_lifecycle: findInheritedLifecycle(streamDefinition, ancestors),
|
||||
inherited_fields: getInheritedFieldsFromAncestors(ancestors),
|
||||
};
|
||||
|
|
|
@ -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({
|
|||
<BreadcrumbsContextProvider>
|
||||
<RouteRenderer />
|
||||
</BreadcrumbsContextProvider>
|
||||
<StreamsAppHeaderActionMenu appMountParameters={appMountParameters} />
|
||||
</RouterProvider>
|
||||
</TimeFilterProvider>
|
||||
</RedirectAppLinks>
|
||||
</StreamsAppContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function StreamsAppHeaderActionMenu({
|
||||
appMountParameters,
|
||||
}: {
|
||||
appMountParameters: AppMountParameters;
|
||||
}) {
|
||||
const { setHeaderActionMenu, theme$ } = appMountParameters;
|
||||
|
||||
return (
|
||||
<HeaderMenuPortal setHeaderActionMenu={setHeaderActionMenu} theme$={theme$}>
|
||||
<EuiFlexGroup responsive={false} gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<></>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</HeaderMenuPortal>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
<EuiButton
|
||||
data-test-subj="streamsAppManagementBottomBarButton"
|
||||
disabled={disabled}
|
||||
color="primary"
|
||||
fill
|
||||
size="s"
|
||||
iconType="check"
|
||||
onClick={onConfirm}
|
||||
isLoading={isLoading}
|
||||
<EuiToolTip
|
||||
content={
|
||||
insufficientPrivileges
|
||||
? i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.bottomBar.onlySimulate',
|
||||
{
|
||||
defaultMessage: "You don't have sufficient privileges to save changes.",
|
||||
}
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{confirmButtonText}
|
||||
</EuiButton>
|
||||
<EuiButton
|
||||
data-test-subj="streamsAppManagementBottomBarButton"
|
||||
disabled={disabled || insufficientPrivileges}
|
||||
color="primary"
|
||||
fill
|
||||
size="s"
|
||||
iconType="check"
|
||||
onClick={onConfirm}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{confirmButtonText}
|
||||
</EuiButton>
|
||||
</EuiToolTip>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
</EuiSplitPanel.Inner>
|
||||
</EuiSplitPanel.Outer>
|
||||
|
@ -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(() => {
|
|||
<SortableList onDragItem={handlerItemDrag}>
|
||||
{processorsRefs.map((processorRef, idx) => (
|
||||
<DraggableProcessorListItem
|
||||
disableDrag={!definition.privileges.manage}
|
||||
key={processorRef.id}
|
||||
idx={idx}
|
||||
processorRef={processorRef}
|
||||
|
@ -230,7 +236,7 @@ const ProcessorsEditor = React.memo(() => {
|
|||
))}
|
||||
</SortableList>
|
||||
)}
|
||||
<AddProcessorPanel />
|
||||
{definition.privileges.simulate && <AddProcessorPanel />}
|
||||
</EuiPanel>
|
||||
<EuiPanel paddingSize="m" hasShadow={false} grow={false}>
|
||||
{!isEmpty(errors.ignoredFields) && (
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -11,13 +11,15 @@ import { EditProcessorPanel, type EditProcessorPanelProps } from './processors';
|
|||
|
||||
export const DraggableProcessorListItem = ({
|
||||
idx,
|
||||
disableDrag,
|
||||
...props
|
||||
}: EditProcessorPanelProps & { idx: number }) => (
|
||||
}: EditProcessorPanelProps & { idx: number; disableDrag: boolean }) => (
|
||||
<EuiDraggable
|
||||
index={idx}
|
||||
spacing="m"
|
||||
draggableId={props.processorRef.id}
|
||||
hasInteractiveChildren
|
||||
isDragDisabled={disableDrag}
|
||||
css={{
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
|
|
|
@ -195,18 +195,20 @@ export function StreamDetailLifecycle({
|
|||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="m">
|
||||
<EuiFlexItem grow={2}>
|
||||
<EuiPanel grow={true} hasShadow={false} hasBorder paddingSize="s">
|
||||
<IngestionRate
|
||||
definition={definition}
|
||||
refreshStats={refreshStats}
|
||||
isLoadingStats={isLoadingStats}
|
||||
stats={stats}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
{definition.privileges.monitor && (
|
||||
<EuiFlexItem grow={2}>
|
||||
<EuiPanel grow={true} hasShadow={false} hasBorder paddingSize="s">
|
||||
<IngestionRate
|
||||
definition={definition}
|
||||
refreshStats={refreshStats}
|
||||
isLoadingStats={isLoadingStats}
|
||||
stats={stats}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
{isIlmLifecycle(definition.effective_lifecycle) ? (
|
||||
{definition.privileges.lifecycle && isIlmLifecycle(definition.effective_lifecycle) ? (
|
||||
<EuiFlexItem grow={3}>
|
||||
<EuiPanel grow={true} hasShadow={false} hasBorder paddingSize="s">
|
||||
<IlmSummary definition={definition} lifecycle={definition.effective_lifecycle} />
|
||||
|
|
|
@ -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 : (
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiButton
|
||||
data-test-subj="streamsAppRetentionMetadataEditDataRetentionButton"
|
||||
size="s"
|
||||
fullWidth
|
||||
onClick={toggleMenu}
|
||||
<EuiToolTip
|
||||
content={
|
||||
!definition.privileges.lifecycle
|
||||
? i18n.translate(
|
||||
'xpack.streams.entityDetailViewWithoutParams.editDataRetention.insufficientPrivileges',
|
||||
{
|
||||
defaultMessage: "You don't have sufficient privileges to change retention.",
|
||||
}
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{i18n.translate('xpack.streams.entityDetailViewWithoutParams.editDataRetention', {
|
||||
defaultMessage: 'Edit data retention',
|
||||
})}
|
||||
</EuiButton>
|
||||
<EuiButton
|
||||
data-test-subj="streamsAppRetentionMetadataEditDataRetentionButton"
|
||||
size="s"
|
||||
fullWidth
|
||||
onClick={toggleMenu}
|
||||
disabled={!definition.privileges.lifecycle}
|
||||
>
|
||||
{i18n.translate('xpack.streams.entityDetailViewWithoutParams.editDataRetention', {
|
||||
defaultMessage: 'Edit data retention',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiToolTip>
|
||||
}
|
||||
isOpen={isMenuOpen}
|
||||
closePopover={closeMenu}
|
||||
|
@ -178,15 +194,20 @@ export function RetentionMetadata({
|
|||
'Approximate average (stream total size divided by the number of days since creation).',
|
||||
})}
|
||||
value={
|
||||
statsError ? (
|
||||
'-'
|
||||
) : isLoadingStats || !stats ? (
|
||||
<EuiLoadingSpinner size="s" />
|
||||
) : stats.bytesPerDay ? (
|
||||
formatIngestionRate(stats.bytesPerDay)
|
||||
) : (
|
||||
'-'
|
||||
)
|
||||
<PrivilegesWarningIconWrapper
|
||||
hasPrivileges={definition.privileges.monitor}
|
||||
title="ingestionRate"
|
||||
>
|
||||
{statsError ? (
|
||||
'-'
|
||||
) : isLoadingStats || !stats ? (
|
||||
<EuiLoadingSpinner size="s" />
|
||||
) : stats.bytesPerDay ? (
|
||||
formatIngestionRate(stats.bytesPerDay)
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</PrivilegesWarningIconWrapper>
|
||||
}
|
||||
/>
|
||||
<EuiHorizontalRule margin="s" />
|
||||
|
@ -195,13 +216,18 @@ export function RetentionMetadata({
|
|||
defaultMessage: 'Total doc count',
|
||||
})}
|
||||
value={
|
||||
statsError ? (
|
||||
'-'
|
||||
) : isLoadingStats || !stats ? (
|
||||
<EuiLoadingSpinner size="s" />
|
||||
) : (
|
||||
formatNumber(stats.totalDocs, '0,0')
|
||||
)
|
||||
<PrivilegesWarningIconWrapper
|
||||
hasPrivileges={definition.privileges.monitor}
|
||||
title="totalDocCount"
|
||||
>
|
||||
{statsError ? (
|
||||
'-'
|
||||
) : isLoadingStats || !stats ? (
|
||||
<EuiLoadingSpinner size="s" />
|
||||
) : (
|
||||
formatNumber(stats.totalDocs, '0,0')
|
||||
)}
|
||||
</PrivilegesWarningIconWrapper>
|
||||
}
|
||||
/>
|
||||
</EuiPanel>
|
||||
|
|
|
@ -75,14 +75,19 @@ export function ClassicStreamDetailManagement({
|
|||
};
|
||||
}
|
||||
|
||||
tabs.advanced = {
|
||||
content: (
|
||||
<UnmanagedElasticsearchAssets definition={definition} refreshDefinition={refreshDefinition} />
|
||||
),
|
||||
label: i18n.translate('xpack.streams.streamDetailView.advancedTab', {
|
||||
defaultMessage: 'Advanced',
|
||||
}),
|
||||
};
|
||||
if (definition.privileges.manage) {
|
||||
tabs.advanced = {
|
||||
content: (
|
||||
<UnmanagedElasticsearchAssets
|
||||
definition={definition}
|
||||
refreshDefinition={refreshDefinition}
|
||||
/>
|
||||
),
|
||||
label: i18n.translate('xpack.streams.streamDetailView.advancedTab', {
|
||||
defaultMessage: 'Advanced',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (!isValidManagementSubTab(subtab)) {
|
||||
return (
|
||||
|
|
|
@ -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',
|
||||
})}
|
||||
</EuiText>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
iconType="plus"
|
||||
size="s"
|
||||
data-test-subj="streamsAppStreamDetailRoutingAddRuleButton"
|
||||
onClick={() => {
|
||||
selectChildUnderEdit({
|
||||
isNew: true,
|
||||
child: {
|
||||
destination: `${definition.stream.name}.child`,
|
||||
if: cloneDeep(EMPTY_EQUALS_CONDITION),
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{i18n.translate('xpack.streams.streamDetailRouting.addRule', {
|
||||
defaultMessage: 'Create child stream',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
{(definition.privileges.simulate || definition.privileges.manage) && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={
|
||||
definition.privileges.simulate
|
||||
? i18n.translate('xpack.streams.streamDetailRouting.rules.onlySimulate', {
|
||||
defaultMessage:
|
||||
"You don't have sufficient privileges to create new streams, only simulate.",
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<EuiButton
|
||||
iconType="plus"
|
||||
size="s"
|
||||
data-test-subj="streamsAppStreamDetailRoutingAddRuleButton"
|
||||
onClick={() => {
|
||||
selectChildUnderEdit({
|
||||
isNew: true,
|
||||
child: {
|
||||
destination: `${definition.stream.name}.child`,
|
||||
if: cloneDeep(EMPTY_EQUALS_CONDITION),
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{i18n.translate('xpack.streams.streamDetailRouting.addRule', {
|
||||
defaultMessage: 'Create child stream',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexGroup
|
||||
|
@ -98,6 +112,7 @@ export function ChildStreamList({ availableStreams }: { availableStreams: string
|
|||
<EuiDraggable
|
||||
key={child.destination}
|
||||
index={i}
|
||||
isDragDisabled={!definition.privileges.manage}
|
||||
draggableId={child.destination}
|
||||
hasInteractiveChildren={true}
|
||||
customDragHandle={true}
|
||||
|
@ -111,7 +126,9 @@ export function ChildStreamList({ availableStreams }: { availableStreams: string
|
|||
>
|
||||
<RoutingStreamEntry
|
||||
draggableProvided={provided}
|
||||
disableEditButton={hasChildStreamsOrderChanged}
|
||||
disableEditButton={
|
||||
hasChildStreamsOrderChanged || !definition.privileges.manage
|
||||
}
|
||||
child={
|
||||
!childUnderEdit?.isNew &&
|
||||
child.destination === childUnderEdit?.child.destination
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiButton, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiButton, EuiFlexItem, EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import { IngestUpsertRequest } from '@kbn/streams-schema';
|
||||
|
@ -169,7 +169,7 @@ export function ControlBar() {
|
|||
<EuiButtonEmpty
|
||||
color="danger"
|
||||
size="s"
|
||||
disabled={routingAppState.saveInProgress}
|
||||
disabled={routingAppState.saveInProgress || !definition.privileges.manage}
|
||||
data-test-subj="streamsAppRoutingStreamEntryRemoveButton"
|
||||
onClick={() => {
|
||||
routingAppState.setShowDeleteModal(true);
|
||||
|
@ -194,19 +194,30 @@ export function ControlBar() {
|
|||
defaultMessage: 'Cancel',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
<EuiButton
|
||||
isLoading={routingAppState.saveInProgress}
|
||||
onClick={saveOrUpdateChildren}
|
||||
data-test-subj="streamsAppStreamDetailRoutingSaveButton"
|
||||
<EuiToolTip
|
||||
content={
|
||||
!definition.privileges.manage
|
||||
? i18n.translate('xpack.streams.streamDetailRouting.onlySimulate', {
|
||||
defaultMessage: "You don't have sufficient privileges to save changes.",
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{routingAppState.childUnderEdit && routingAppState.childUnderEdit.isNew
|
||||
? i18n.translate('xpack.streams.streamDetailRouting.add', {
|
||||
defaultMessage: 'Save',
|
||||
})
|
||||
: i18n.translate('xpack.streams.streamDetailRouting.change', {
|
||||
defaultMessage: 'Change routing',
|
||||
})}
|
||||
</EuiButton>
|
||||
<EuiButton
|
||||
isLoading={routingAppState.saveInProgress}
|
||||
disabled={routingAppState.saveInProgress || !definition.privileges.manage}
|
||||
onClick={saveOrUpdateChildren}
|
||||
data-test-subj="streamsAppStreamDetailRoutingSaveButton"
|
||||
>
|
||||
{routingAppState.childUnderEdit && routingAppState.childUnderEdit.isNew
|
||||
? i18n.translate('xpack.streams.streamDetailRouting.add', {
|
||||
defaultMessage: 'Save',
|
||||
})
|
||||
: i18n.translate('xpack.streams.streamDetailRouting.change', {
|
||||
defaultMessage: 'Change routing',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiToolTip>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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(
|
||||
<HeaderMenuPortal setHeaderActionMenu={setHeaderActionMenu} theme$={theme$}>
|
||||
test
|
||||
</HeaderMenuPortal>
|
||||
);
|
||||
|
||||
unmount();
|
||||
|
||||
expect(setHeaderActionMenu).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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(<OutPortal node={portalNode} />, { theme$ });
|
||||
return mount(element);
|
||||
});
|
||||
|
||||
return () => {
|
||||
portalNode.unmount();
|
||||
setHeaderActionMenu(undefined);
|
||||
};
|
||||
}, [portalNode, setHeaderActionMenu, theme$]);
|
||||
|
||||
return <InPortal node={portalNode}>{children}</InPortal>;
|
||||
}
|
|
@ -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 (
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
<HeaderMenuPortalLazy {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
|
@ -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' ? (
|
||||
<EuiPopover
|
||||
css={popoverCss}
|
||||
attachToAnchor={true}
|
||||
anchorPosition="downCenter"
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
data-test-subj={`streamsInsufficientPrivileges-${title}`}
|
||||
aria-label={insufficientPrivilegesText}
|
||||
title={insufficientPrivilegesText}
|
||||
iconType="warning"
|
||||
color={iconColor}
|
||||
onClick={handleButtonClick}
|
||||
/>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={togglePopover}
|
||||
>
|
||||
{insufficientPrivilegesText}
|
||||
</EuiPopover>
|
||||
) : (
|
||||
<EuiToolTip content={insufficientPrivilegesText}>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiIcon
|
||||
data-test-subj={`streamsInsufficientPrivileges-${title}`}
|
||||
aria-label={insufficientPrivilegesText}
|
||||
title={insufficientPrivilegesText}
|
||||
type="warning"
|
||||
color={iconColor}
|
||||
/>
|
||||
{children}
|
||||
</EuiFlexGroup>
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
|
@ -50,23 +50,25 @@ export function ChildStreamList({ definition }: { definition?: IngestStreamGetRe
|
|||
'Create sub streams to split out data with different retention policies, schemas, and more.',
|
||||
})}
|
||||
</EuiText>
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
<EuiButton
|
||||
data-test-subj="streamsAppChildStreamListCreateChildStreamButton"
|
||||
iconType="plusInCircle"
|
||||
href={router.link('/{key}/{tab}/{subtab}', {
|
||||
path: {
|
||||
key: definition.stream.name,
|
||||
tab: 'management',
|
||||
subtab: 'route',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{i18n.translate('xpack.streams.entityDetailOverview.createChildStream', {
|
||||
defaultMessage: 'Create child stream',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexGroup>
|
||||
{definition.privileges.manage && (
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
<EuiButton
|
||||
data-test-subj="streamsAppChildStreamListCreateChildStreamButton"
|
||||
iconType="plusInCircle"
|
||||
href={router.link('/{key}/{tab}/{subtab}', {
|
||||
path: {
|
||||
key: definition.stream.name,
|
||||
tab: 'management',
|
||||
subtab: 'route',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{i18n.translate('xpack.streams.entityDetailOverview.createChildStream', {
|
||||
defaultMessage: 'Create child stream',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -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) {
|
|||
<StatItem
|
||||
label={documentCountLabel}
|
||||
value={
|
||||
dataStreamStats ? formatNumber(dataStreamStats.totalDocs || 0, 'decimal0') : '-'
|
||||
<PrivilegesWarningIconWrapper
|
||||
hasPrivileges={definition.privileges.monitor}
|
||||
title="totalDocCount"
|
||||
>
|
||||
{dataStreamStats ? formatNumber(dataStreamStats.totalDocs || 0, 'decimal0') : '-'}
|
||||
</PrivilegesWarningIconWrapper>
|
||||
}
|
||||
/>
|
||||
<StatItem
|
||||
label={storageSizeLabel}
|
||||
value={
|
||||
dataStreamStats && dataStreamStats.sizeBytes
|
||||
? formatBytes(dataStreamStats.sizeBytes)
|
||||
: '-'
|
||||
<PrivilegesWarningIconWrapper
|
||||
hasPrivileges={definition.privileges.monitor}
|
||||
title="sizeBytes"
|
||||
>
|
||||
{dataStreamStats && dataStreamStats.sizeBytes
|
||||
? formatBytes(dataStreamStats.sizeBytes)
|
||||
: '-'}
|
||||
</PrivilegesWarningIconWrapper>
|
||||
}
|
||||
withBorder
|
||||
/>
|
||||
|
@ -152,7 +163,14 @@ export function StreamStatsPanel({ definition }: StreamStatsPanelProps) {
|
|||
</>
|
||||
}
|
||||
value={
|
||||
dataStreamStats ? formatIngestionRate(dataStreamStats.bytesPerDay || 0, true) : '-'
|
||||
<PrivilegesWarningIconWrapper
|
||||
hasPrivileges={definition.privileges.monitor}
|
||||
title="ingestionRate"
|
||||
>
|
||||
{dataStreamStats
|
||||
? formatIngestionRate(dataStreamStats.bytesPerDay || 0, true)
|
||||
: '-'}
|
||||
</PrivilegesWarningIconWrapper>
|
||||
}
|
||||
withBorder
|
||||
/>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue