(key: T, val: string) => void = (argName, val) => {
const fn = val === '' ? del : set;
diff --git a/x-pack/plugins/canvas/public/functions/filters.ts b/x-pack/plugins/canvas/public/functions/filters.ts
index a37953657e157..97049f8af57c8 100644
--- a/x-pack/plugins/canvas/public/functions/filters.ts
+++ b/x-pack/plugins/canvas/public/functions/filters.ts
@@ -27,14 +27,14 @@ function getFiltersByGroup(allFilters: string[], groups?: string[], ungrouped =
// remove all allFilters that belong to a group
return allFilters.filter((filter: string) => {
const ast = fromExpression(filter);
- const expGroups: string[] = get(ast, 'chain[0].arguments.filterGroup', []);
+ const expGroups: string[] = get(ast, 'chain[0].arguments.filterGroup', []) as string[];
return expGroups.length === 0;
});
}
return allFilters.filter((filter: string) => {
const ast = fromExpression(filter);
- const expGroups: string[] = get(ast, 'chain[0].arguments.filterGroup', []);
+ const expGroups: string[] = get(ast, 'chain[0].arguments.filterGroup', []) as string[];
return expGroups.length > 0 && expGroups.every((expGroup) => groups.includes(expGroup));
});
}
diff --git a/x-pack/plugins/canvas/public/functions/plot/get_flot_axis_config.ts b/x-pack/plugins/canvas/public/functions/plot/get_flot_axis_config.ts
index 2d58e926d28c8..ddbbb62bd1872 100644
--- a/x-pack/plugins/canvas/public/functions/plot/get_flot_axis_config.ts
+++ b/x-pack/plugins/canvas/public/functions/plot/get_flot_axis_config.ts
@@ -38,7 +38,7 @@ export const getFlotAxisConfig = (
const config: Config = { show: true };
- const axisType = get(columns, `${axis}.type`);
+ const axisType = get(columns, [axis, `type`]);
if (isAxisConfig(argValue)) {
const { position, min, max, tickSize } = argValue;
diff --git a/x-pack/plugins/canvas/public/lib/filter.ts b/x-pack/plugins/canvas/public/lib/filter.ts
index 767cf53e16f6d..fade544b2bc80 100644
--- a/x-pack/plugins/canvas/public/lib/filter.ts
+++ b/x-pack/plugins/canvas/public/lib/filter.ts
@@ -65,7 +65,7 @@ const excludeFiltersByGroups = (filters: Ast[], filterExprAst: AstFunction) => {
const groupsToExclude = filterExprAst.arguments.group ?? [];
const removeUngrouped = filterExprAst.arguments.ungrouped?.[0] ?? false;
return filters.filter((filter) => {
- const groups: string[] = get(filter, 'chain[0].arguments.filterGroup', []).filter(
+ const groups: string[] = (get(filter, 'chain[0].arguments.filterGroup', []) as string[]).filter(
(group: string) => group !== ''
);
const noNeedToExcludeByGroup = !(
@@ -89,7 +89,7 @@ const includeFiltersByGroups = (
const groupsToInclude = filterExprAst.arguments.group ?? [];
const includeOnlyUngrouped = filterExprAst.arguments.ungrouped?.[0] ?? false;
return filters.filter((filter) => {
- const groups: string[] = get(filter, 'chain[0].arguments.filterGroup', []).filter(
+ const groups: string[] = (get(filter, 'chain[0].arguments.filterGroup', []) as string[]).filter(
(group: string) => group !== ''
);
const needToIncludeByGroup =
diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__snapshots__/app.test.tsx.snap b/x-pack/plugins/canvas/shareable_runtime/components/__snapshots__/app.test.tsx.snap
index 4aa379aa194bc..447771435a1dc 100644
--- a/x-pack/plugins/canvas/shareable_runtime/components/__snapshots__/app.test.tsx.snap
+++ b/x-pack/plugins/canvas/shareable_runtime/components/__snapshots__/app.test.tsx.snap
@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[` App renders properly 1`] = `"There is a new region landmark with page level controls at the end of the document.
"`;
+exports[` App renders properly 1`] = `"There is a new region landmark with page level controls at the end of the document.
"`;
diff --git a/x-pack/plugins/cases/public/common/test_utils.tsx b/x-pack/plugins/cases/public/common/test_utils.tsx
index 0028d79019f2a..1cbf5e2a5d454 100644
--- a/x-pack/plugins/cases/public/common/test_utils.tsx
+++ b/x-pack/plugins/cases/public/common/test_utils.tsx
@@ -18,7 +18,7 @@ import { EuiButton } from '@elastic/eui';
* Convenience utility to remove text appended to links by EUI
*/
export const removeExternalLinkText = (str: string | null) =>
- str?.replace(/\(opens in a new tab or window\)/g, '');
+ str?.replace(/\(external[^)]*\)/g, '');
export async function waitForComponentToPaint(wrapper: ReactWrapper
, amount = 0) {
await act(async () => {
diff --git a/x-pack/plugins/cases/public/components/create/template.test.tsx b/x-pack/plugins/cases/public/components/create/template.test.tsx
index bde2e84d17f59..156caf6341e07 100644
--- a/x-pack/plugins/cases/public/components/create/template.test.tsx
+++ b/x-pack/plugins/cases/public/components/create/template.test.tsx
@@ -13,7 +13,8 @@ import { createAppMockRenderer } from '../../common/mock';
import { templatesConfigurationMock } from '../../containers/mock';
import { TemplateSelector } from './templates';
-describe('TemplateSelector', () => {
+// FLAKY: https://github.com/elastic/kibana/issues/193482
+describe.skip('TemplateSelector', () => {
let appMockRender: AppMockRenderer;
const onTemplateChange = jest.fn();
diff --git a/x-pack/plugins/cases/public/components/system_actions/cases/cases_params.test.tsx b/x-pack/plugins/cases/public/components/system_actions/cases/cases_params.test.tsx
index 0dc8ca9cbfb13..5e93d0b061a84 100644
--- a/x-pack/plugins/cases/public/components/system_actions/cases/cases_params.test.tsx
+++ b/x-pack/plugins/cases/public/components/system_actions/cases/cases_params.test.tsx
@@ -18,6 +18,7 @@ import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react
import { useGetAllCaseConfigurations } from '../../../containers/configure/use_get_all_case_configurations';
import { useGetAllCaseConfigurationsResponse } from '../../configure_cases/__mock__';
import { templatesConfigurationMock } from '../../../containers/mock';
+import * as utils from '../../../containers/configure/utils';
jest.mock('@kbn/alerts-ui-shared/src/common/hooks/use_alerts_data_view');
jest.mock('../../../common/lib/kibana/use_application');
@@ -29,10 +30,6 @@ const useAlertsDataViewMock = jest.mocked(useAlertsDataView);
const useApplicationMock = useApplication as jest.Mock;
const useGetAllCaseConfigurationsMock = useGetAllCaseConfigurations as jest.Mock;
-useKibanaMock.mockReturnValue({
- services: { ...createStartServicesMock(), data: { dataViews: {} } },
-} as unknown as ReturnType);
-
const actionParams = {
subAction: 'run',
subActionParams: {
@@ -98,6 +95,9 @@ describe('CasesParamsFields renders', () => {
},
});
useGetAllCaseConfigurationsMock.mockImplementation(() => useGetAllCaseConfigurationsResponse);
+ useKibanaMock.mockReturnValue({
+ services: { ...createStartServicesMock(), data: { dataViews: {} } },
+ } as unknown as ReturnType);
});
afterEach(() => {
@@ -268,6 +268,54 @@ describe('CasesParamsFields renders', () => {
expect(await screen.findByText(templatesConfigurationMock[1].name)).toBeInTheDocument();
});
+ it('renders security templates if the project is serverless security', async () => {
+ useKibanaMock.mockReturnValue({
+ services: {
+ ...createStartServicesMock(),
+ // simulate a serverless security project
+ cloud: { isServerlessEnabled: true, serverless: { projectType: 'security' } },
+ data: { dataViews: {} },
+ },
+ } as unknown as ReturnType);
+
+ const configuration = {
+ ...useGetAllCaseConfigurationsResponse.data[0],
+ templates: templatesConfigurationMock,
+ };
+ useGetAllCaseConfigurationsMock.mockImplementation(() => ({
+ ...useGetAllCaseConfigurationsResponse,
+ data: [configuration],
+ }));
+ const getConfigurationByOwnerSpy = jest
+ .spyOn(utils, 'getConfigurationByOwner')
+ .mockImplementation(() => configuration);
+
+ const observabilityOwnedRule = {
+ ...defaultProps,
+ // these two would normally produce an observability owner
+ producerId: 'observability',
+ featureId: 'observability',
+ actionParams: {
+ subAction: 'run',
+ subActionParams: {
+ ...actionParams.subActionParams,
+ templateId: templatesConfigurationMock[1].key,
+ },
+ },
+ };
+
+ render();
+
+ expect(getConfigurationByOwnerSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ // the security owner was forced
+ owner: 'securitySolution',
+ })
+ );
+
+ getConfigurationByOwnerSpy.mockRestore();
+ });
+
it('updates template correctly', async () => {
useGetAllCaseConfigurationsMock.mockReturnValueOnce({
...useGetAllCaseConfigurationsResponse,
diff --git a/x-pack/plugins/cases/public/components/system_actions/cases/cases_params.tsx b/x-pack/plugins/cases/public/components/system_actions/cases/cases_params.tsx
index 6c93b2435af8e..9fabf39db0bc4 100644
--- a/x-pack/plugins/cases/public/components/system_actions/cases/cases_params.tsx
+++ b/x-pack/plugins/cases/public/components/system_actions/cases/cases_params.tsx
@@ -39,11 +39,20 @@ export const CasesParamsFieldsComponent: React.FunctionComponent<
ActionParamsProps
> = ({ actionParams, editAction, errors, index, producerId, featureId }) => {
const {
+ cloud,
+ data: { dataViews: dataViewsService },
http,
notifications: { toasts },
- data: { dataViews: dataViewsService },
} = useKibana().services;
- const owner = getOwnerFromRuleConsumerProducer({ consumer: featureId, producer: producerId });
+
+ const owner = getOwnerFromRuleConsumerProducer({
+ consumer: featureId,
+ producer: producerId,
+ // This is a workaround for a very specific bug with the cases action in serverless security
+ // More info here: https://github.com/elastic/kibana/issues/195599
+ isServerlessSecurity:
+ cloud?.isServerlessEnabled && cloud?.serverless.projectType === 'security',
+ });
const { dataView, isLoading: loadingAlertDataViews } = useAlertsDataView({
http,
diff --git a/x-pack/plugins/cases/public/types.ts b/x-pack/plugins/cases/public/types.ts
index c857446ea042c..7f1d4863eac7e 100644
--- a/x-pack/plugins/cases/public/types.ts
+++ b/x-pack/plugins/cases/public/types.ts
@@ -30,6 +30,7 @@ import type { ContentManagementPublicStart } from '@kbn/content-management-plugi
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import type { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public';
+import type { CloudStart } from '@kbn/cloud-plugin/public';
import type { UseCasesAddToExistingCaseModal } from './components/all_cases/selector_modal/use_cases_add_to_existing_case_modal';
import type { UseCasesAddToNewCaseFlyout } from './components/create/flyout/use_cases_add_to_new_case_flyout';
import type { UseIsAddToCaseOpen } from './components/cases_context/state/use_is_add_to_case_open';
@@ -73,6 +74,7 @@ export interface CasesPublicSetupDependencies {
export interface CasesPublicStartDependencies {
apm?: ApmBase;
+ cloud?: CloudStart;
data: DataPublicPluginStart;
embeddable: EmbeddableStart;
features: FeaturesPluginStart;
diff --git a/x-pack/plugins/cloud_security_posture/common/runtime_mappings/get_safe_posture_type_runtime_mapping.ts b/x-pack/plugins/cloud_security_posture/common/runtime_mappings/get_safe_posture_type_runtime_mapping.ts
index 29c42402ad8dc..568829356cf82 100644
--- a/x-pack/plugins/cloud_security_posture/common/runtime_mappings/get_safe_posture_type_runtime_mapping.ts
+++ b/x-pack/plugins/cloud_security_posture/common/runtime_mappings/get_safe_posture_type_runtime_mapping.ts
@@ -16,14 +16,16 @@ export const getSafePostureTypeRuntimeMapping = (): MappingRuntimeFields => ({
type: 'keyword',
script: {
source: `
- def postureTypeAvailable = doc.containsKey("rule.benchmark.posture_type") &&
- !doc["rule.benchmark.posture_type"].empty;
+ def postureTypeAvailable = doc.containsKey("rule.benchmark.posture_type") && !doc["rule.benchmark.posture_type"].empty;
+ boolean isNativeCsp = doc.containsKey("data_stream.dataset") && !doc["data_stream.dataset"].empty && doc["data_stream.dataset"].value == "cloud_security_posture.findings";
- if (!postureTypeAvailable) {
- // Before 8.7 release
- emit("kspm");
- } else {
- emit(doc["rule.benchmark.posture_type"].value);
+ if (isNativeCsp) {
+ if (!postureTypeAvailable) {
+ // Before 8.7 release
+ emit("kspm");
+ } else {
+ emit(doc["rule.benchmark.posture_type"].value);
+ }
}
`,
},
diff --git a/x-pack/plugins/cloud_security_posture/public/common/navigation/constants.ts b/x-pack/plugins/cloud_security_posture/public/common/navigation/constants.ts
index 9444b7b4b1922..be2a7c75aad46 100644
--- a/x-pack/plugins/cloud_security_posture/public/common/navigation/constants.ts
+++ b/x-pack/plugins/cloud_security_posture/public/common/navigation/constants.ts
@@ -10,7 +10,7 @@ import {
KSPM_POLICY_TEMPLATE,
CLOUD_SECURITY_POSTURE_BASE_PATH,
} from '@kbn/cloud-security-posture-common';
-import { NAV_ITEMS_NAMES } from '@kbn/cloud-security-posture/constants/navigation';
+import { NAV_ITEMS_NAMES } from '@kbn/cloud-security-posture/src/constants/navigation';
import { CNVM_POLICY_TEMPLATE } from '../../../common/constants';
import type { CspBenchmarksPage, CspPage, CspPageNavigationItem } from './types';
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/rule_tab.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/rule_tab.tsx
index 2dcca4932935c..a64ac099df14a 100644
--- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/rule_tab.tsx
+++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/rule_tab.tsx
@@ -13,111 +13,115 @@ import type { CspFinding } from '@kbn/cloud-security-posture-common';
import { RulesDetectionRuleCounter } from '../../rules/rules_detection_rule_counter';
import { BenchmarkIcons, CspFlyoutMarkdown, EMPTY_VALUE, RuleNameLink } from './findings_flyout';
+const getReferenceFromRule = (rule?: CspFinding['rule']) => {
+ return rule?.reference || rule?.references;
+};
+
export const getRuleList = (
rule?: CspFinding['rule'],
ruleState = 'unmuted',
ruleFlyoutLink?: string
-) => [
- {
- title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.nameTitle', {
- defaultMessage: 'Name',
- }),
- description: rule?.name ? (
-
- ) : (
- EMPTY_VALUE
- ),
- },
- {
- title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.descriptionTitle', {
- defaultMessage: 'Description',
- }),
- description: rule?.description ? (
- {rule.description}
- ) : (
- EMPTY_VALUE
- ),
- },
- {
- title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.AlertsTitle', {
- defaultMessage: 'Alerts',
- }),
- description:
- ruleState === 'muted' ? (
-
- ) : rule?.benchmark?.name ? (
-
+) => {
+ const reference = getReferenceFromRule(rule);
+
+ return [
+ {
+ title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.nameTitle', {
+ defaultMessage: 'Name',
+ }),
+ description: rule?.name ? (
+
+ ) : (
+ EMPTY_VALUE
+ ),
+ },
+ {
+ title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.descriptionTitle', {
+ defaultMessage: 'Description',
+ }),
+ description: rule?.description ? (
+ {rule.description}
) : (
EMPTY_VALUE
),
- },
- {
- title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.tagsTitle', {
- defaultMessage: 'Tags',
- }),
- description: rule?.tags?.length ? (
- <>
- {rule.tags.map((tag) => (
- {tag}
- ))}
- >
- ) : (
- EMPTY_VALUE
- ),
- },
- {
- title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.frameworkSourcesTitle', {
- defaultMessage: 'Framework Sources',
- }),
- description:
- rule?.benchmark?.id && rule?.benchmark?.name ? (
-
+ },
+ {
+ title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.AlertsTitle', {
+ defaultMessage: 'Alerts',
+ }),
+ description:
+ ruleState === 'muted' ? (
+
+ ) : rule?.benchmark?.name ? (
+
+ ) : (
+ EMPTY_VALUE
+ ),
+ },
+ {
+ title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.tagsTitle', {
+ defaultMessage: 'Tags',
+ }),
+ description: rule?.tags?.length ? (
+ <>
+ {rule.tags.map((tag) => (
+ {tag}
+ ))}
+ >
) : (
EMPTY_VALUE
),
- },
- {
- title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.cisSectionTitle', {
- defaultMessage: 'Framework Section',
- }),
- description: rule?.section || EMPTY_VALUE,
- },
- {
- title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.profileApplicabilityTitle', {
- defaultMessage: 'Profile Applicability',
- }),
- description: rule?.profile_applicability ? (
- {rule.profile_applicability}
- ) : (
- EMPTY_VALUE
- ),
- },
- {
- title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.benchmarkTitle', {
- defaultMessage: 'Benchmark',
- }),
- description: rule?.benchmark?.name || EMPTY_VALUE,
- },
- {
- title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.auditTitle', {
- defaultMessage: 'Audit',
- }),
- description: rule?.audit ? {rule.audit} : EMPTY_VALUE,
- },
- {
- title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.referencesTitle', {
- defaultMessage: 'References',
- }),
- description: rule?.references ? (
- {rule.references}
- ) : (
- EMPTY_VALUE
- ),
- },
-];
+ },
+ {
+ title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.frameworkSourcesTitle', {
+ defaultMessage: 'Framework Sources',
+ }),
+ description:
+ rule?.benchmark?.id && rule?.benchmark?.name ? (
+
+ ) : (
+ EMPTY_VALUE
+ ),
+ },
+ {
+ title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.cisSectionTitle', {
+ defaultMessage: 'Framework Section',
+ }),
+ description: rule?.section || EMPTY_VALUE,
+ },
+ {
+ title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.profileApplicabilityTitle', {
+ defaultMessage: 'Profile Applicability',
+ }),
+ description: rule?.profile_applicability ? (
+ {rule.profile_applicability}
+ ) : (
+ EMPTY_VALUE
+ ),
+ },
+ {
+ title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.benchmarkTitle', {
+ defaultMessage: 'Benchmark',
+ }),
+ description: rule?.benchmark?.name || EMPTY_VALUE,
+ },
+ {
+ title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.auditTitle', {
+ defaultMessage: 'Audit',
+ }),
+ description: rule?.audit ? {rule.audit} : EMPTY_VALUE,
+ },
+ {
+ title: i18n.translate('xpack.csp.findings.findingsFlyout.ruleTab.referencesTitle', {
+ defaultMessage: 'References',
+ }),
+ description: reference ? {reference} : EMPTY_VALUE,
+ },
+ ];
+};
export const RuleTab = ({
data,
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_overview_tab.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_overview_tab.tsx
index ed9d7f985f357..327ce2f94759d 100644
--- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_overview_tab.tsx
+++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_overview_tab.tsx
@@ -161,7 +161,7 @@ const VulnerabilityOverviewTiles = ({ vulnerabilityRecord }: VulnerabilityTabPro
return (
- {vulnerability?.score?.base && (
+ {!!vulnerability?.score?.base && (
{
+ private capabilities$ = new Subject();
+
public setup(
core: CoreSetup,
plugins: AppPluginSetupDependencies
@@ -37,51 +40,56 @@ export class DataQualityPlugin
const { management, share } = plugins;
const useHash = core.uiSettings.get('state:storeInSessionStorage');
- management.sections.section.data.registerApp({
- id: PLUGIN_ID,
- title: PLUGIN_NAME,
- order: 2,
- keywords: [
- 'data',
- 'quality',
- 'data quality',
- 'datasets',
- 'datasets quality',
- 'data set quality',
- ],
- async mount(params: ManagementAppMountParams) {
- const [{ renderApp }, [coreStart, pluginsStartDeps, pluginStart]] = await Promise.all([
- import('./application'),
- core.getStartServices(),
- ]);
+ this.capabilities$.subscribe((capabilities) => {
+ if (!capabilities.dataQuality.show) return;
- return renderApp(coreStart, pluginsStartDeps, pluginStart, params);
- },
- hideFromSidebar: false,
- });
+ management.sections.section.data.registerApp({
+ id: PLUGIN_ID,
+ title: PLUGIN_NAME,
+ order: 2,
+ keywords: [
+ 'data',
+ 'quality',
+ 'data quality',
+ 'datasets',
+ 'datasets quality',
+ 'data set quality',
+ ],
+ async mount(params: ManagementAppMountParams) {
+ const [{ renderApp }, [coreStart, pluginsStartDeps, pluginStart]] = await Promise.all([
+ import('./application'),
+ core.getStartServices(),
+ ]);
- const managementLocator =
- share.url.locators.get(MANAGEMENT_APP_LOCATOR);
+ return renderApp(coreStart, pluginsStartDeps, pluginStart, params);
+ },
+ hideFromSidebar: false,
+ });
- if (managementLocator) {
- share.url.locators.create(
- new DatasetQualityLocatorDefinition({
- useHash,
- managementLocator,
- })
- );
- share.url.locators.create(
- new DatasetQualityDetailsLocatorDefinition({
- useHash,
- managementLocator,
- })
- );
- }
+ const managementLocator =
+ share.url.locators.get(MANAGEMENT_APP_LOCATOR);
+
+ if (managementLocator) {
+ share.url.locators.create(
+ new DatasetQualityLocatorDefinition({
+ useHash,
+ managementLocator,
+ })
+ );
+ share.url.locators.create(
+ new DatasetQualityDetailsLocatorDefinition({
+ useHash,
+ managementLocator,
+ })
+ );
+ }
+ });
return {};
}
- public start(_core: CoreStart): DataQualityPluginStart {
+ public start(core: CoreStart): DataQualityPluginStart {
+ this.capabilities$.next(core.application.capabilities);
return {};
}
diff --git a/x-pack/plugins/data_quality/server/features.ts b/x-pack/plugins/data_quality/server/features.ts
new file mode 100644
index 0000000000000..a570c78e6edbe
--- /dev/null
+++ b/x-pack/plugins/data_quality/server/features.ts
@@ -0,0 +1,77 @@
+/*
+ * 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 { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common';
+import {
+ KibanaFeatureConfig,
+ KibanaFeatureScope,
+ ElasticsearchFeatureConfig,
+} from '@kbn/features-plugin/common';
+import { PLUGIN_FEATURE_ID, PLUGIN_ID, PLUGIN_NAME } from '../common';
+
+export const KIBANA_FEATURE: KibanaFeatureConfig = {
+ id: PLUGIN_FEATURE_ID,
+ name: PLUGIN_NAME,
+ category: DEFAULT_APP_CATEGORIES.management,
+ scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security],
+ app: [PLUGIN_ID],
+ privileges: {
+ all: {
+ app: [PLUGIN_ID],
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: ['show'],
+ },
+ read: {
+ disabled: true,
+ savedObject: {
+ all: [],
+ read: [],
+ },
+ ui: ['show'],
+ },
+ },
+};
+
+export const ELASTICSEARCH_FEATURE: ElasticsearchFeatureConfig = {
+ id: PLUGIN_ID,
+ management: {
+ data: [PLUGIN_ID],
+ },
+ privileges: [
+ {
+ ui: [],
+ requiredClusterPrivileges: [],
+ requiredIndexPrivileges: {
+ ['logs-*-*']: ['read'],
+ },
+ },
+ {
+ ui: [],
+ requiredClusterPrivileges: [],
+ requiredIndexPrivileges: {
+ ['traces-*-*']: ['read'],
+ },
+ },
+ {
+ ui: [],
+ requiredClusterPrivileges: [],
+ requiredIndexPrivileges: {
+ ['metrics-*-*']: ['read'],
+ },
+ },
+ {
+ ui: [],
+ requiredClusterPrivileges: [],
+ requiredIndexPrivileges: {
+ ['synthetics-*-*']: ['read'],
+ },
+ },
+ ],
+};
diff --git a/x-pack/plugins/data_quality/server/plugin.ts b/x-pack/plugins/data_quality/server/plugin.ts
index 1b7e9cface597..93ed93917fa7a 100644
--- a/x-pack/plugins/data_quality/server/plugin.ts
+++ b/x-pack/plugins/data_quality/server/plugin.ts
@@ -6,48 +6,14 @@
*/
import { CoreSetup, Plugin } from '@kbn/core/server';
-import { PLUGIN_ID } from '../common';
import { Dependencies } from './types';
+import { ELASTICSEARCH_FEATURE, KIBANA_FEATURE } from './features';
export class DataQualityPlugin implements Plugin {
- public setup(coreSetup: CoreSetup, { features }: Dependencies) {
- features.registerElasticsearchFeature({
- id: PLUGIN_ID,
- management: {
- data: [PLUGIN_ID],
- },
- privileges: [
- {
- ui: [],
- requiredClusterPrivileges: [],
- requiredIndexPrivileges: {
- ['logs-*-*']: ['read'],
- },
- },
- {
- ui: [],
- requiredClusterPrivileges: [],
- requiredIndexPrivileges: {
- ['traces-*-*']: ['read'],
- },
- },
- {
- ui: [],
- requiredClusterPrivileges: [],
- requiredIndexPrivileges: {
- ['metrics-*-*']: ['read'],
- },
- },
- {
- ui: [],
- requiredClusterPrivileges: [],
- requiredIndexPrivileges: {
- ['synthetics-*-*']: ['read'],
- },
- },
- ],
- });
+ public setup(_coreSetup: CoreSetup, { features }: Dependencies) {
+ features.registerKibanaFeature(KIBANA_FEATURE);
+ features.registerElasticsearchFeature(ELASTICSEARCH_FEATURE);
}
public start() {}
diff --git a/x-pack/plugins/data_quality/tsconfig.json b/x-pack/plugins/data_quality/tsconfig.json
index 911c4fbfff557..a3f04f88ec7ff 100644
--- a/x-pack/plugins/data_quality/tsconfig.json
+++ b/x-pack/plugins/data_quality/tsconfig.json
@@ -28,6 +28,7 @@
"@kbn/deeplinks-management",
"@kbn/deeplinks-observability",
"@kbn/ebt-tools",
+ "@kbn/core-application-common",
],
"exclude": ["target/**/*"]
}
diff --git a/x-pack/plugins/data_usage/common/rest_types/data_streams.ts b/x-pack/plugins/data_usage/common/rest_types/data_streams.ts
index b1c02bb40854d..87af7e29eccb6 100644
--- a/x-pack/plugins/data_usage/common/rest_types/data_streams.ts
+++ b/x-pack/plugins/data_usage/common/rest_types/data_streams.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { schema } from '@kbn/config-schema';
+import { schema, TypeOf } from '@kbn/config-schema';
export const DataStreamsResponseSchema = {
body: () =>
@@ -16,3 +16,5 @@ export const DataStreamsResponseSchema = {
})
),
};
+
+export type DataStreamsResponseBodySchemaBody = TypeOf;
diff --git a/x-pack/plugins/data_usage/common/rest_types/usage_metrics.test.ts b/x-pack/plugins/data_usage/common/rest_types/usage_metrics.test.ts
index 473e64c6b03d9..e4feb438cc801 100644
--- a/x-pack/plugins/data_usage/common/rest_types/usage_metrics.test.ts
+++ b/x-pack/plugins/data_usage/common/rest_types/usage_metrics.test.ts
@@ -41,7 +41,7 @@ describe('usage_metrics schemas', () => {
).not.toThrow();
});
- it('should error if `dataStream` list is empty', () => {
+ it('should not error if `dataStream` list is empty', () => {
expect(() =>
UsageMetricsRequestSchema.validate({
from: new Date().toISOString(),
@@ -49,7 +49,7 @@ describe('usage_metrics schemas', () => {
metricTypes: ['storage_retained'],
dataStreams: [],
})
- ).toThrowError('[dataStreams]: array size is [0], but cannot be smaller than [1]');
+ ).not.toThrow();
});
it('should error if `dataStream` is given type not array', () => {
@@ -71,7 +71,7 @@ describe('usage_metrics schemas', () => {
metricTypes: ['storage_retained'],
dataStreams: ['ds_1', ' '],
})
- ).toThrow('[dataStreams]: [dataStreams] list cannot contain empty values');
+ ).toThrow('[dataStreams]: list cannot contain empty values');
});
it('should error if `metricTypes` is empty string', () => {
@@ -82,7 +82,7 @@ describe('usage_metrics schemas', () => {
dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'],
metricTypes: ' ',
})
- ).toThrow();
+ ).toThrow('[metricTypes]: could not parse array value from json input');
});
it('should error if `metricTypes` contains an empty item', () => {
@@ -93,7 +93,7 @@ describe('usage_metrics schemas', () => {
dataStreams: ['data_stream_1', 'data_stream_2', 'data_stream_3'],
metricTypes: [' ', 'storage_retained'], // First item is invalid
})
- ).toThrowError(/list cannot contain empty values/);
+ ).toThrow('list cannot contain empty values');
});
it('should error if `metricTypes` is not a valid type', () => {
@@ -116,7 +116,7 @@ describe('usage_metrics schemas', () => {
metricTypes: ['storage_retained', 'foo'],
})
).toThrow(
- '[metricTypes] must be one of storage_retained, ingest_rate, search_vcu, ingest_vcu, ml_vcu, index_latency, index_rate, search_latency, search_rate'
+ '[metricTypes]: must be one of ingest_rate, storage_retained, search_vcu, ingest_vcu, ml_vcu, index_latency, index_rate, search_latency, search_rate'
);
});
diff --git a/x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts b/x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts
index 3dceeadc198b0..40194494854fc 100644
--- a/x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts
+++ b/x-pack/plugins/data_usage/common/rest_types/usage_metrics.ts
@@ -7,9 +7,11 @@
import { schema, type TypeOf } from '@kbn/config-schema';
-const METRIC_TYPE_VALUES = [
- 'storage_retained',
- 'ingest_rate',
+// note these should be sorted alphabetically as we sort the URL params on the browser side
+// before making the request, else the cache key will be different and that would invoke a new request
+export const DEFAULT_METRIC_TYPES = ['ingest_rate', 'storage_retained'] as const;
+export const METRIC_TYPE_VALUES = [
+ ...DEFAULT_METRIC_TYPES,
'search_vcu',
'ingest_vcu',
'ml_vcu',
@@ -21,6 +23,22 @@ const METRIC_TYPE_VALUES = [
export type MetricTypes = (typeof METRIC_TYPE_VALUES)[number];
+export const isDefaultMetricType = (metricType: string) =>
+ // @ts-ignore
+ DEFAULT_METRIC_TYPES.includes(metricType);
+
+export const METRIC_TYPE_API_VALUES_TO_UI_OPTIONS_MAP = Object.freeze>({
+ storage_retained: 'Data Retained in Storage',
+ ingest_rate: 'Data Ingested',
+ search_vcu: 'Search VCU',
+ ingest_vcu: 'Ingest VCU',
+ ml_vcu: 'ML VCU',
+ index_latency: 'Index Latency',
+ index_rate: 'Index Rate',
+ search_latency: 'Search Latency',
+ search_rate: 'Search Rate',
+});
+
// type guard for MetricTypes
export const isMetricType = (type: string): type is MetricTypes =>
METRIC_TYPE_VALUES.includes(type as MetricTypes);
@@ -47,21 +65,20 @@ export const UsageMetricsRequestSchema = schema.object({
if (trimmedValues.some((v) => !v.length)) {
return '[metricTypes] list cannot contain empty values';
} else if (trimmedValues.some((v) => !isValidMetricType(v))) {
- return `[metricTypes] must be one of ${METRIC_TYPE_VALUES.join(', ')}`;
+ return `must be one of ${METRIC_TYPE_VALUES.join(', ')}`;
}
},
}),
dataStreams: schema.arrayOf(schema.string(), {
- minSize: 1,
validate: (values) => {
if (values.map((v) => v.trim()).some((v) => !v.length)) {
- return '[dataStreams] list cannot contain empty values';
+ return 'list cannot contain empty values';
}
},
}),
});
-export type UsageMetricsRequestSchemaQueryParams = TypeOf;
+export type UsageMetricsRequestBody = TypeOf;
export const UsageMetricsResponseSchema = {
body: () =>
diff --git a/x-pack/plugins/data_usage/public/app/components/data_usage_metrics.tsx b/x-pack/plugins/data_usage/public/app/components/data_usage_metrics.tsx
new file mode 100644
index 0000000000000..cc443c78562ee
--- /dev/null
+++ b/x-pack/plugins/data_usage/public/app/components/data_usage_metrics.tsx
@@ -0,0 +1,150 @@
+/*
+ * 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, { useCallback, useEffect, memo, useState } from 'react';
+import { css } from '@emotion/react';
+import { EuiFlexGroup, EuiFlexItem, EuiLoadingElastic, EuiCallOut } from '@elastic/eui';
+import { Charts } from './charts';
+import { useBreadcrumbs } from '../../utils/use_breadcrumbs';
+import { useKibanaContextForPlugin } from '../../utils/use_kibana';
+import { PLUGIN_NAME } from '../../../common';
+import { useGetDataUsageMetrics } from '../../hooks/use_get_usage_metrics';
+import { useDataUsageMetricsUrlParams } from '../hooks/use_charts_url_params';
+import { DEFAULT_DATE_RANGE_OPTIONS, useDateRangePicker } from '../hooks/use_date_picker';
+import { DEFAULT_METRIC_TYPES, UsageMetricsRequestBody } from '../../../common/rest_types';
+import { ChartFilters } from './filters/charts_filters';
+import { UX_LABELS } from '../translations';
+
+const EuiItemCss = css`
+ width: 100%;
+`;
+
+const FlexItemWithCss = memo(({ children }: { children: React.ReactNode }) => (
+ {children}
+));
+
+export const DataUsageMetrics = () => {
+ const {
+ services: { chrome, appParams },
+ } = useKibanaContextForPlugin();
+
+ const {
+ metricTypes: metricTypesFromUrl,
+ dataStreams: dataStreamsFromUrl,
+ startDate: startDateFromUrl,
+ endDate: endDateFromUrl,
+ setUrlMetricTypesFilter,
+ setUrlDateRangeFilter,
+ } = useDataUsageMetricsUrlParams();
+
+ const [metricsFilters, setMetricsFilters] = useState({
+ metricTypes: [...DEFAULT_METRIC_TYPES],
+ dataStreams: [],
+ from: DEFAULT_DATE_RANGE_OPTIONS.startDate,
+ to: DEFAULT_DATE_RANGE_OPTIONS.endDate,
+ });
+
+ useEffect(() => {
+ if (!metricTypesFromUrl) {
+ setUrlMetricTypesFilter(metricsFilters.metricTypes.join(','));
+ }
+ if (!startDateFromUrl || !endDateFromUrl) {
+ setUrlDateRangeFilter({ startDate: metricsFilters.from, endDate: metricsFilters.to });
+ }
+ }, [
+ endDateFromUrl,
+ metricTypesFromUrl,
+ metricsFilters.from,
+ metricsFilters.metricTypes,
+ metricsFilters.to,
+ setUrlDateRangeFilter,
+ setUrlMetricTypesFilter,
+ startDateFromUrl,
+ ]);
+
+ useEffect(() => {
+ setMetricsFilters((prevState) => ({
+ ...prevState,
+ metricTypes: metricTypesFromUrl?.length ? metricTypesFromUrl : prevState.metricTypes,
+ dataStreams: dataStreamsFromUrl?.length ? dataStreamsFromUrl : prevState.dataStreams,
+ }));
+ }, [metricTypesFromUrl, dataStreamsFromUrl]);
+
+ const { dateRangePickerState, onRefreshChange, onTimeChange } = useDateRangePicker();
+
+ const {
+ error,
+ data,
+ isFetching,
+ isFetched,
+ refetch: refetchDataUsageMetrics,
+ } = useGetDataUsageMetrics(
+ {
+ ...metricsFilters,
+ from: dateRangePickerState.startDate,
+ to: dateRangePickerState.endDate,
+ },
+ {
+ retry: false,
+ }
+ );
+
+ const onRefresh = useCallback(() => {
+ refetchDataUsageMetrics();
+ }, [refetchDataUsageMetrics]);
+
+ const onChangeDataStreamsFilter = useCallback(
+ (selectedDataStreams: string[]) => {
+ setMetricsFilters((prevState) => ({ ...prevState, dataStreams: selectedDataStreams }));
+ },
+ [setMetricsFilters]
+ );
+
+ const onChangeMetricTypesFilter = useCallback(
+ (selectedMetricTypes: string[]) => {
+ setMetricsFilters((prevState) => ({ ...prevState, metricTypes: selectedMetricTypes }));
+ },
+ [setMetricsFilters]
+ );
+
+ useBreadcrumbs([{ text: PLUGIN_NAME }], appParams, chrome);
+
+ return (
+
+
+
+
+ {!isFetching && error?.message && (
+
+
+
+ )}
+
+ {isFetched && data?.metrics ? (
+
+ ) : isFetching ? (
+
+ ) : null}
+
+
+ );
+};
diff --git a/x-pack/plugins/data_usage/public/app/components/filters/charts_filter.tsx b/x-pack/plugins/data_usage/public/app/components/filters/charts_filter.tsx
new file mode 100644
index 0000000000000..466bc6debae77
--- /dev/null
+++ b/x-pack/plugins/data_usage/public/app/components/filters/charts_filter.tsx
@@ -0,0 +1,238 @@
+/*
+ * 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 { orderBy } from 'lodash/fp';
+import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { EuiFlexGroup, EuiFlexItem, EuiPopoverTitle, EuiSelectable } from '@elastic/eui';
+
+import { useTestIdGenerator } from '../../../hooks/use_test_id_generator';
+import {
+ METRIC_TYPE_API_VALUES_TO_UI_OPTIONS_MAP,
+ type MetricTypes,
+} from '../../../../common/rest_types';
+
+import { ClearAllButton } from './clear_all_button';
+import { UX_LABELS } from '../../translations';
+import { ChartsFilterPopover } from './charts_filter_popover';
+import { FilterItems, FilterName, useChartsFilter } from '../../hooks';
+
+const getSearchPlaceholder = (filterName: FilterName) => {
+ if (filterName === 'dataStreams') {
+ return UX_LABELS.filterSearchPlaceholder('data streams');
+ }
+ return UX_LABELS.filterSearchPlaceholder('metric types');
+};
+
+export const ChartsFilter = memo(
+ ({
+ filterName,
+ onChangeFilterOptions,
+ 'data-test-subj': dataTestSubj,
+ }: {
+ filterName: FilterName;
+ onChangeFilterOptions?: (selectedOptions: string[]) => void;
+ 'data-test-subj'?: string;
+ }) => {
+ const getTestId = useTestIdGenerator(dataTestSubj);
+
+ const isMetricsFilter = filterName === 'metricTypes';
+ const isDataStreamsFilter = filterName === 'dataStreams';
+ // popover states and handlers
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+ const onPopoverButtonClick = useCallback(() => {
+ setIsPopoverOpen(!isPopoverOpen);
+ }, [setIsPopoverOpen, isPopoverOpen]);
+ const onClosePopover = useCallback(() => {
+ setIsPopoverOpen(false);
+ }, [setIsPopoverOpen]);
+
+ // search string state
+ const [searchString, setSearchString] = useState('');
+ const {
+ areDataStreamsSelectedOnMount,
+ isLoading,
+ items,
+ setItems,
+ hasActiveFilters,
+ numActiveFilters,
+ numFilters,
+ setAreDataStreamsSelectedOnMount,
+ setUrlDataStreamsFilter,
+ setUrlMetricTypesFilter,
+ } = useChartsFilter({
+ filterName,
+ searchString,
+ });
+
+ // track popover state to pin selected options
+ const wasPopoverOpen = useRef(isPopoverOpen);
+ useEffect(() => {
+ return () => {
+ wasPopoverOpen.current = isPopoverOpen;
+ };
+ }, [isPopoverOpen, wasPopoverOpen]);
+
+ // compute if selected dataStreams should be pinned
+ const shouldPinSelectedDataStreams = useCallback(
+ (isNotChangingOptions: boolean = true) => {
+ // case 1: when no dataStreams are selected initially
+ return (
+ isNotChangingOptions &&
+ wasPopoverOpen.current &&
+ isPopoverOpen &&
+ filterName === 'dataStreams'
+ );
+ },
+ [filterName, isPopoverOpen]
+ );
+
+ // augmented options based on the dataStreams filter
+ const sortedHostsFilterOptions = useMemo(() => {
+ if (shouldPinSelectedDataStreams() || areDataStreamsSelectedOnMount) {
+ // pin checked items to the top
+ return orderBy('checked', 'asc', items);
+ }
+ // return options as are for other filters
+ return items;
+ }, [areDataStreamsSelectedOnMount, shouldPinSelectedDataStreams, items]);
+
+ const isSearchable = useMemo(() => !isMetricsFilter, [isMetricsFilter]);
+
+ const onOptionsChange = useCallback(
+ (newOptions: FilterItems) => {
+ // update filter UI options state
+ setItems(newOptions.map((option) => option));
+
+ // compute a selected list of options
+ const selectedItems = newOptions.reduce((acc, curr) => {
+ if (curr.checked === 'on' && curr.key) {
+ acc.push(curr.key);
+ }
+ return acc;
+ }, []);
+
+ // update URL params
+ if (isMetricsFilter) {
+ setUrlMetricTypesFilter(
+ selectedItems
+ .map((item) => METRIC_TYPE_API_VALUES_TO_UI_OPTIONS_MAP[item as MetricTypes])
+ .join()
+ );
+ } else if (isDataStreamsFilter) {
+ setUrlDataStreamsFilter(selectedItems.join());
+ }
+ // reset shouldPinSelectedDataStreams, setAreDataStreamsSelectedOnMount
+ shouldPinSelectedDataStreams(false);
+ setAreDataStreamsSelectedOnMount(false);
+
+ // update overall query state
+ if (typeof onChangeFilterOptions !== 'undefined') {
+ onChangeFilterOptions(selectedItems);
+ }
+ },
+ [
+ setItems,
+ isMetricsFilter,
+ isDataStreamsFilter,
+ shouldPinSelectedDataStreams,
+ setAreDataStreamsSelectedOnMount,
+ onChangeFilterOptions,
+ setUrlMetricTypesFilter,
+ setUrlDataStreamsFilter,
+ ]
+ );
+
+ // clear all selected options
+ const onClearAll = useCallback(() => {
+ // update filter UI options state
+ setItems(
+ items.map((option) => {
+ option.checked = undefined;
+ return option;
+ })
+ );
+
+ // update URL params based on filter on page
+ if (isMetricsFilter) {
+ setUrlMetricTypesFilter('');
+ } else if (isDataStreamsFilter) {
+ setUrlDataStreamsFilter('');
+ }
+
+ if (typeof onChangeFilterOptions !== 'undefined') {
+ onChangeFilterOptions([]);
+ }
+ }, [
+ setItems,
+ items,
+ isMetricsFilter,
+ isDataStreamsFilter,
+ onChangeFilterOptions,
+ setUrlMetricTypesFilter,
+ setUrlDataStreamsFilter,
+ ]);
+
+ return (
+
+ setSearchString(searchValue.trim()),
+ }}
+ >
+ {(list, search) => {
+ return (
+
+ {isSearchable && (
+
+ {search}
+
+ )}
+ {list}
+ {!isMetricsFilter && (
+
+
+
+
+
+ )}
+
+ );
+ }}
+
+
+ );
+ }
+);
+
+ChartsFilter.displayName = 'ChartsFilter';
diff --git a/x-pack/plugins/data_usage/public/app/components/filters/charts_filter_popover.tsx b/x-pack/plugins/data_usage/public/app/components/filters/charts_filter_popover.tsx
new file mode 100644
index 0000000000000..2ed96f012c497
--- /dev/null
+++ b/x-pack/plugins/data_usage/public/app/components/filters/charts_filter_popover.tsx
@@ -0,0 +1,81 @@
+/*
+ * 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, { memo, useMemo } from 'react';
+import { EuiFilterButton, EuiPopover, useGeneratedHtmlId } from '@elastic/eui';
+import { useTestIdGenerator } from '../../../hooks/use_test_id_generator';
+import { type FilterName } from '../../hooks/use_charts_filter';
+import { FILTER_NAMES } from '../../translations';
+
+export const ChartsFilterPopover = memo(
+ ({
+ children,
+ closePopover,
+ filterName,
+ hasActiveFilters,
+ isPopoverOpen,
+ numActiveFilters,
+ numFilters,
+ onButtonClick,
+ 'data-test-subj': dataTestSubj,
+ }: {
+ children: React.ReactNode;
+ closePopover: () => void;
+ filterName: FilterName;
+ hasActiveFilters: boolean;
+ isPopoverOpen: boolean;
+ numActiveFilters: number;
+ numFilters: number;
+ onButtonClick: () => void;
+ 'data-test-subj'?: string;
+ }) => {
+ const getTestId = useTestIdGenerator(dataTestSubj);
+
+ const filterGroupPopoverId = useGeneratedHtmlId({
+ prefix: 'filterGroupPopover',
+ });
+
+ const button = useMemo(
+ () => (
+
+ {FILTER_NAMES[filterName]}
+
+ ),
+ [
+ filterName,
+ getTestId,
+ hasActiveFilters,
+ isPopoverOpen,
+ numActiveFilters,
+ numFilters,
+ onButtonClick,
+ ]
+ );
+
+ return (
+
+ {children}
+
+ );
+ }
+);
+
+ChartsFilterPopover.displayName = 'ChartsFilterPopover';
diff --git a/x-pack/plugins/data_usage/public/app/components/filters/charts_filters.tsx b/x-pack/plugins/data_usage/public/app/components/filters/charts_filters.tsx
new file mode 100644
index 0000000000000..72608f4a62c75
--- /dev/null
+++ b/x-pack/plugins/data_usage/public/app/components/filters/charts_filters.tsx
@@ -0,0 +1,93 @@
+/*
+ * 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, { memo, useCallback, useMemo } from 'react';
+import { EuiFilterGroup, EuiFlexGroup, EuiFlexItem, EuiSuperUpdateButton } from '@elastic/eui';
+import type {
+ DurationRange,
+ OnRefreshChangeProps,
+} from '@elastic/eui/src/components/date_picker/types';
+import { useTestIdGenerator } from '../../../hooks/use_test_id_generator';
+import { useGetDataUsageMetrics } from '../../../hooks/use_get_usage_metrics';
+import { DateRangePickerValues, UsageMetricsDateRangePicker } from './date_picker';
+import { ChartsFilter } from './charts_filter';
+
+interface ChartFiltersProps {
+ dateRangePickerState: DateRangePickerValues;
+ isDataLoading: boolean;
+ onChangeDataStreamsFilter: (selectedDataStreams: string[]) => void;
+ onChangeMetricTypesFilter?: (selectedMetricTypes: string[]) => void;
+ onRefresh: () => void;
+ onRefreshChange: (evt: OnRefreshChangeProps) => void;
+ onTimeChange: ({ start, end }: DurationRange) => void;
+ onClick: ReturnType['refetch'];
+ showMetricsTypesFilter?: boolean;
+ 'data-test-subj'?: string;
+}
+
+export const ChartFilters = memo(
+ ({
+ dateRangePickerState,
+ isDataLoading,
+ onClick,
+ onChangeMetricTypesFilter,
+ onChangeDataStreamsFilter,
+ onRefresh,
+ onRefreshChange,
+ onTimeChange,
+ showMetricsTypesFilter = false,
+ 'data-test-subj': dataTestSubj,
+ }) => {
+ const getTestId = useTestIdGenerator(dataTestSubj);
+
+ const filters = useMemo(() => {
+ return (
+ <>
+ {showMetricsTypesFilter && (
+
+ )}
+
+ >
+ );
+ }, [onChangeDataStreamsFilter, onChangeMetricTypesFilter, showMetricsTypesFilter]);
+
+ const onClickRefreshButton = useCallback(() => onClick(), [onClick]);
+
+ return (
+
+
+ {filters}
+
+
+
+
+
+
+
+
+ );
+ }
+);
+
+ChartFilters.displayName = 'ChartFilters';
diff --git a/x-pack/plugins/data_usage/public/app/components/filters/clear_all_button.tsx b/x-pack/plugins/data_usage/public/app/components/filters/clear_all_button.tsx
new file mode 100644
index 0000000000000..afa4c2fe72917
--- /dev/null
+++ b/x-pack/plugins/data_usage/public/app/components/filters/clear_all_button.tsx
@@ -0,0 +1,43 @@
+/*
+ * 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, { memo } from 'react';
+import { css } from '@emotion/react';
+import { euiThemeVars } from '@kbn/ui-theme';
+import { EuiButtonEmpty } from '@elastic/eui';
+import { UX_LABELS } from '../../translations';
+
+const buttonCss = css`
+ border-top: ${euiThemeVars.euiBorderThin};
+ border-radius: 0;
+`;
+export const ClearAllButton = memo(
+ ({
+ 'data-test-subj': dataTestSubj,
+ isDisabled,
+ onClick,
+ }: {
+ 'data-test-subj'?: string;
+ isDisabled: boolean;
+ onClick: () => void;
+ }) => {
+ return (
+
+ {UX_LABELS.filterClearAll}
+
+ );
+ }
+);
+
+ClearAllButton.displayName = 'ClearAllButton';
diff --git a/x-pack/plugins/data_usage/public/app/components/date_picker.tsx b/x-pack/plugins/data_usage/public/app/components/filters/date_picker.tsx
similarity index 61%
rename from x-pack/plugins/data_usage/public/app/components/date_picker.tsx
rename to x-pack/plugins/data_usage/public/app/components/filters/date_picker.tsx
index ca29acf8c96a6..4d9b280d763ce 100644
--- a/x-pack/plugins/data_usage/public/app/components/date_picker.tsx
+++ b/x-pack/plugins/data_usage/public/app/components/filters/date_picker.tsx
@@ -6,8 +6,7 @@
*/
import React, { memo, useState } from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker, useEuiTheme } from '@elastic/eui';
-import { css } from '@emotion/react';
+import { EuiSuperDatePicker } from '@elastic/eui';
import type { IUnifiedSearchPluginServices } from '@kbn/unified-search-plugin/public';
import type { EuiSuperDatePickerRecentRange } from '@elastic/eui';
import { useKibana } from '@kbn/kibana-react-plugin/public';
@@ -37,7 +36,6 @@ interface UsageMetricsDateRangePickerProps {
export const UsageMetricsDateRangePicker = memo(
({ dateRangePickerState, isDataLoading, onRefresh, onRefreshChange, onTimeChange }) => {
- const { euiTheme } = useEuiTheme();
const kibana = useKibana();
const { uiSettings } = kibana.services;
const [commonlyUsedRanges] = useState(() => {
@@ -55,32 +53,22 @@ export const UsageMetricsDateRangePicker = memo
-
-
-
-
-
-
+
);
}
);
diff --git a/x-pack/plugins/data_usage/public/app/components/page.tsx b/x-pack/plugins/data_usage/public/app/components/page.tsx
new file mode 100644
index 0000000000000..d7ff20f5e933f
--- /dev/null
+++ b/x-pack/plugins/data_usage/public/app/components/page.tsx
@@ -0,0 +1,69 @@
+/*
+ * 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 type { PropsWithChildren } from 'react';
+import React, { memo, useMemo } from 'react';
+import type { CommonProps } from '@elastic/eui';
+import {
+ EuiPageHeader,
+ EuiPageSection,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiTitle,
+ EuiSpacer,
+} from '@elastic/eui';
+
+interface DataUsagePageProps {
+ title: React.ReactNode;
+ subtitle?: React.ReactNode;
+ actions?: React.ReactNode;
+ restrictWidth?: boolean | number;
+ hasBottomBorder?: boolean;
+ hideHeader?: boolean;
+}
+
+export const DataUsagePage = memo>(
+ ({ title, subtitle, children, restrictWidth = false, hasBottomBorder = true, ...otherProps }) => {
+ const header = useMemo(() => {
+ return (
+
+
+
+ {title}
+
+
+
+ );
+ }, [, title]);
+
+ const description = useMemo(() => {
+ return subtitle ? (
+ {subtitle}
+ ) : undefined;
+ }, [subtitle]);
+
+ return (
+
+ <>
+
+
+ >
+
+ {children}
+
+
+ );
+ }
+);
+
+DataUsagePage.displayName = 'DataUsagePage';
diff --git a/x-pack/plugins/data_usage/public/app/data_usage.tsx b/x-pack/plugins/data_usage/public/app/data_usage.tsx
deleted file mode 100644
index bea9f2b511a77..0000000000000
--- a/x-pack/plugins/data_usage/public/app/data_usage.tsx
+++ /dev/null
@@ -1,146 +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, { useCallback, useEffect, useState } from 'react';
-import {
- EuiTitle,
- EuiSpacer,
- EuiFlexGroup,
- EuiFlexItem,
- EuiLoadingElastic,
- EuiPageSection,
- EuiText,
-} from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n-react';
-import { i18n } from '@kbn/i18n';
-import { UsageMetricsRequestSchemaQueryParams } from '../../common/rest_types';
-import { Charts } from './components/charts';
-import { UsageMetricsDateRangePicker } from './components/date_picker';
-import { useBreadcrumbs } from '../utils/use_breadcrumbs';
-import { useKibanaContextForPlugin } from '../utils/use_kibana';
-import { PLUGIN_NAME } from '../../common';
-import { useGetDataUsageMetrics } from '../hooks/use_get_usage_metrics';
-import { DEFAULT_DATE_RANGE_OPTIONS, useDateRangePicker } from './hooks/use_date_picker';
-import { useDataUsageMetricsUrlParams } from './hooks/use_charts_url_params';
-
-export const DataUsage = () => {
- const {
- services: { chrome, appParams },
- } = useKibanaContextForPlugin();
-
- const {
- metricTypes: metricTypesFromUrl,
- dataStreams: dataStreamsFromUrl,
- startDate: startDateFromUrl,
- endDate: endDateFromUrl,
- setUrlMetricTypesFilter,
- setUrlDateRangeFilter,
- } = useDataUsageMetricsUrlParams();
-
- const [metricsFilters, setMetricsFilters] = useState({
- metricTypes: ['storage_retained', 'ingest_rate'],
- // TODO: Replace with data streams from /data_streams api
- dataStreams: [
- '.alerts-ml.anomaly-detection-health.alerts-default',
- '.alerts-stack.alerts-default',
- ],
- from: DEFAULT_DATE_RANGE_OPTIONS.startDate,
- to: DEFAULT_DATE_RANGE_OPTIONS.endDate,
- });
-
- useEffect(() => {
- if (!metricTypesFromUrl) {
- setUrlMetricTypesFilter(metricsFilters.metricTypes.join(','));
- }
- if (!startDateFromUrl || !endDateFromUrl) {
- setUrlDateRangeFilter({ startDate: metricsFilters.from, endDate: metricsFilters.to });
- }
- }, [
- endDateFromUrl,
- metricTypesFromUrl,
- metricsFilters.from,
- metricsFilters.metricTypes,
- metricsFilters.to,
- setUrlDateRangeFilter,
- setUrlMetricTypesFilter,
- startDateFromUrl,
- ]);
-
- useEffect(() => {
- setMetricsFilters((prevState) => ({
- ...prevState,
- metricTypes: metricTypesFromUrl?.length ? metricTypesFromUrl : prevState.metricTypes,
- dataStreams: dataStreamsFromUrl?.length ? dataStreamsFromUrl : prevState.dataStreams,
- }));
- }, [metricTypesFromUrl, dataStreamsFromUrl]);
-
- const { dateRangePickerState, onRefreshChange, onTimeChange } = useDateRangePicker();
-
- const {
- error,
- data,
- isFetching,
- isFetched,
- refetch: refetchDataUsageMetrics,
- } = useGetDataUsageMetrics(
- {
- ...metricsFilters,
- from: dateRangePickerState.startDate,
- to: dateRangePickerState.endDate,
- },
- {
- retry: false,
- }
- );
-
- const onRefresh = useCallback(() => {
- refetchDataUsageMetrics();
- }, [refetchDataUsageMetrics]);
-
- useBreadcrumbs([{ text: PLUGIN_NAME }], appParams, chrome);
-
- // TODO: show a toast?
- if (!isFetching && error?.body) {
- return {error.body.message}
;
- }
-
- return (
- <>
-
-
- {i18n.translate('xpack.dataUsage.pageTitle', {
- defaultMessage: 'Data Usage',
- })}
-
-
-
-
-
-
-
-