[RAM][Flapping] Make rules settings link with flapping settings shareable (#149564)

## Summary
Resolves: https://github.com/elastic/kibana/issues/148760

Makes the rules setting link that opens up the flapping settings modal
shareable from the `triggers_action_ui` plugin (`getRulesSettingsLink`).

Also adds storybook entires for this component (`rulesSettingsLink`). 

To view locally, run `yarn storybook triggers_actions_ui`

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Xavier Mouligneau <xavier.mouligneau@elastic.co>
This commit is contained in:
Jiawei Wu 2023-01-30 13:59:19 -08:00 committed by GitHub
parent e2e58635a3
commit 7608dfb023
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 288 additions and 39 deletions

View file

@ -27,6 +27,7 @@ import { RuleEventLogListSandbox } from './components/rule_event_log_list_sandbo
import { RuleStatusDropdownSandbox } from './components/rule_status_dropdown_sandbox';
import { RuleStatusFilterSandbox } from './components/rule_status_filter_sandbox';
import { AlertsTableSandbox } from './components/alerts_table_sandbox';
import { RulesSettingsLinkSandbox } from './components/rules_settings_link_sandbox';
export interface TriggersActionsUiExampleComponentParams {
http: CoreStart['http'];
@ -124,6 +125,14 @@ const TriggersActionsUiExampleApp = ({
</Page>
)}
/>
<Route
path="/rules_settings_link"
render={() => (
<Page title="Rules Settings Link">
<RulesSettingsLinkSandbox triggersActionsUi={triggersActionsUi} />
</Page>
)}
/>
</EuiPage>
</Router>
);

View file

@ -0,0 +1,17 @@
/*
* 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 { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public';
interface SandboxProps {
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
}
export const RulesSettingsLinkSandbox = ({ triggersActionsUi }: SandboxProps) => {
return <div style={{ flex: 1 }}>{triggersActionsUi.getRulesSettingsLink()}</div>;
};

View file

@ -64,6 +64,11 @@ export const Sidebar = () => {
name: 'Alert Table',
onClick: () => history.push('/alerts_table'),
},
{
id: 'rules settings link',
name: 'Rules Settings Link',
onClick: () => history.push('/rules_settings_link'),
},
],
},
]}

View file

@ -0,0 +1,31 @@
/*
* 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 { action } from '@storybook/addon-actions';
import { of } from 'rxjs';
import type { ApplicationStart } from '@kbn/core/public';
import { getDefaultCapabilities } from './capabilities';
export const getDefaultServicesApplication = (
override?: Partial<ApplicationStart>
): ApplicationStart => {
const applications = new Map();
return {
currentAppId$: of('fleet'),
navigateToUrl: async (url: string) => {
action(`Navigate to: ${url}`);
},
navigateToApp: async (app: string) => {
action(`Navigate to: ${app}`);
},
getUrlForApp: (url: string) => url,
capabilities: getDefaultCapabilities(),
applications$: of(applications),
...override,
};
};

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { Capabilities } from '@kbn/core-capabilities-common';
export const getDefaultCapabilities = (override?: Partial<Capabilities>): Capabilities => {
return {
actions: {
show: true,
save: true,
execute: true,
delete: true,
},
catalogue: {},
management: {},
navLinks: {},
fleet: {
read: true,
all: true,
},
fleetv2: {
read: true,
all: true,
},
...override,
};
};

View file

@ -275,6 +275,22 @@ const paginatedEventLogListGetResponse = (path: string) => {
return baseEventLogListGetResponse(path);
};
const rulesSettingsGetResponse = (path: string) => {
if (path.endsWith('/settings/_flapping')) {
return {
enabled: true,
lookBackWindow: 20,
statusChangeThreshold: 4,
};
}
};
const rulesSettingsIds = [
'app-rulessettingslink--with-all-permission',
'app-rulessettingslink--with-read-permission',
'app-rulessettingslink--with-no-permission',
];
export const getHttp = (context: Parameters<DecoratorFn>[1]) => {
return {
get: (async (path: string, options: HttpFetchOptions) => {
@ -297,9 +313,13 @@ export const getHttp = (context: Parameters<DecoratorFn>[1]) => {
if (id === 'app-ruleeventloglist--with-paginated-events') {
return paginatedEventLogListGetResponse(path);
}
if (rulesSettingsIds.includes(id)) {
return rulesSettingsGetResponse(path);
}
}) as HttpHandler,
post: (async (path: string, options: HttpFetchOptions) => {
action('POST')(path, options);
return Promise.resolve();
}) as HttpHandler,
} as unknown as HttpStart;
};

View file

@ -11,7 +11,7 @@ import { action } from '@storybook/addon-actions';
import { DecoratorFn } from '@storybook/react';
import { EMPTY, of } from 'rxjs';
import { I18nProvider } from '@kbn/i18n-react';
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { KibanaThemeProvider, KibanaServices } from '@kbn/kibana-react-plugin/public';
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
import type { NotificationsStart, ApplicationStart } from '@kbn/core/public';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
@ -20,9 +20,12 @@ import { ExperimentalFeaturesService } from '../public/common/experimental_featu
import { getHttp } from './context/http';
import { getRuleTypeRegistry } from './context/rule_type_registry';
import { getActionTypeRegistry } from './context/action_type_registry';
import { getDefaultServicesApplication } from './context/application';
interface StorybookContextDecoratorProps {
context: Parameters<DecoratorFn>[1];
servicesApplicationOverride?: Partial<ApplicationStart>;
servicesOverride?: Partial<KibanaServices>;
}
const queryClient = new QueryClient();
@ -45,41 +48,8 @@ const notifications: NotificationsStart = {
},
};
const applications = new Map();
const application: ApplicationStart = {
currentAppId$: of('fleet'),
navigateToUrl: async (url: string) => {
action(`Navigate to: ${url}`);
},
navigateToApp: async (app: string) => {
action(`Navigate to: ${app}`);
},
getUrlForApp: (url: string) => url,
capabilities: {
actions: {
show: true,
save: true,
execute: true,
delete: true,
},
catalogue: {},
management: {},
navLinks: {},
fleet: {
read: true,
all: true,
},
fleetv2: {
read: true,
all: true,
},
},
applications$: of(applications),
};
export const StorybookContextDecorator: React.FC<StorybookContextDecoratorProps> = (props) => {
const { children, context } = props;
const { children, context, servicesApplicationOverride, servicesOverride } = props;
const { globals } = context;
const { euiTheme } = globals;
@ -113,10 +83,11 @@ export const StorybookContextDecorator: React.FC<StorybookContextDecoratorProps>
}
},
},
application,
application: getDefaultServicesApplication(servicesApplicationOverride),
http: getHttp(context),
actionTypeRegistry: getActionTypeRegistry(),
ruleTypeRegistry: getRuleTypeRegistry(),
...servicesOverride,
}}
>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>

View file

@ -0,0 +1,94 @@
/*
* 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, { ComponentProps } from 'react';
import { Meta, Story } from '@storybook/react';
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import { RulesSettingsLink } from './rules_settings_link';
import { StorybookContextDecorator } from '../../../../.storybook/decorator';
import { getDefaultCapabilities } from '../../../../.storybook/context/capabilities';
type Args = ComponentProps<typeof RulesSettingsLink>;
export default {
title: 'app/RulesSettingsLink',
component: RulesSettingsLink,
} as Meta<Args>;
const Template: Story<Args> = () => {
return <RulesSettingsLink />;
};
export const withAllPermission = Template.bind({});
withAllPermission.decorators = [
(StoryComponent, context) => (
<StorybookContextDecorator
context={context}
servicesApplicationOverride={{
capabilities: getDefaultCapabilities({
rulesSettings: {
show: true,
save: true,
readFlappingSettingsUI: true,
writeFlappingSettingsUI: true,
},
}),
}}
>
<StoryComponent />
</StorybookContextDecorator>
),
];
export const withReadPermission = Template.bind({});
withReadPermission.decorators = [
(StoryComponent, context) => (
<StorybookContextDecorator
context={context}
servicesApplicationOverride={{
capabilities: getDefaultCapabilities({
rulesSettings: {
show: true,
save: false,
readFlappingSettingsUI: true,
writeFlappingSettingsUI: false,
},
}),
}}
>
<StoryComponent />
</StorybookContextDecorator>
),
];
export const withNoPermission = Template.bind({});
withNoPermission.decorators = [
(StoryComponent, context) => (
<StorybookContextDecorator
context={context}
servicesApplicationOverride={{
capabilities: getDefaultCapabilities({
rulesSettings: {
show: false,
save: false,
readFlappingSettingsUI: false,
writeFlappingSettingsUI: false,
},
}),
}}
>
<EuiCallOut title="No Permissions">
When the user does not have capabilities to view rules settings, the entire link is hidden
</EuiCallOut>
<EuiSpacer />
<StoryComponent />
</StorybookContextDecorator>
),
];

View file

@ -39,3 +39,6 @@ export const RulesSettingsLink = () => {
</>
);
};
// eslint-disable-next-line import/no-default-export
export { RulesSettingsLink as default };

View file

@ -0,0 +1,26 @@
/*
* 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 { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { EuiLoadingSpinner } from '@elastic/eui';
const queryClient = new QueryClient();
const RulesSettingsLinkLazy: React.FC = lazy(
() => import('../application/components/rules_setting/rules_settings_link')
);
export const getRulesSettingsLinkLazy = () => {
return (
<QueryClientProvider client={queryClient}>
<Suspense fallback={<EuiLoadingSpinner />}>
<RulesSettingsLinkLazy />
</Suspense>
</QueryClientProvider>
);
};

View file

@ -46,6 +46,7 @@ import { getAlertSummaryWidgetLazy } from './common/get_rule_alerts_summary';
import { getRuleDefinitionLazy } from './common/get_rule_definition';
import { getRuleStatusPanelLazy } from './common/get_rule_status_panel';
import { getRuleSnoozeModalLazy } from './common/get_rule_snooze_modal';
import { getRulesSettingsLinkLazy } from './common/get_rules_settings_link';
function createStartMock(): TriggersAndActionsUIPublicPluginStart {
const actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
@ -133,6 +134,9 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart {
getRuleSnoozeModal: (props) => {
return getRuleSnoozeModalLazy(props);
},
getRulesSettingsLink: () => {
return getRulesSettingsLinkLazy();
},
};
}

View file

@ -83,6 +83,7 @@ import { AlertSummaryWidgetProps } from './application/sections/rule_details/com
import { getAlertSummaryWidgetLazy } from './common/get_rule_alerts_summary';
import { RuleSnoozeModalProps } from './application/sections/rules_list/components/rule_snooze_modal';
import { getRuleSnoozeModalLazy } from './common/get_rule_snooze_modal';
import { getRulesSettingsLinkLazy } from './common/get_rules_settings_link';
export interface TriggersAndActionsUIPublicPluginSetup {
actionTypeRegistry: TypeRegistry<ActionTypeModel>;
@ -130,6 +131,7 @@ export interface TriggersAndActionsUIPublicPluginStart {
getRuleStatusPanel: (props: RuleStatusPanelProps) => ReactElement<RuleStatusPanelProps>;
getAlertSummaryWidget: (props: AlertSummaryWidgetProps) => ReactElement<AlertSummaryWidgetProps>;
getRuleSnoozeModal: (props: RuleSnoozeModalProps) => ReactElement<RuleSnoozeModalProps>;
getRulesSettingsLink: () => ReactElement;
}
interface PluginsSetup {
@ -433,6 +435,9 @@ export class Plugin
getRuleSnoozeModal: (props: RuleSnoozeModalProps) => {
return getRuleSnoozeModalLazy(props);
},
getRulesSettingsLink: () => {
return getRulesSettingsLinkLazy();
},
};
}

View file

@ -43,6 +43,7 @@
"@kbn/core-doc-links-browser",
"@kbn/ui-theme",
"@kbn/datemath",
"@kbn/core-capabilities-common",
],
"exclude": [
"target/**/*",

View file

@ -7,7 +7,7 @@
import { FtrProviderContext } from '../../../../test/functional/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default ({ loadTestFile, getService }: FtrProviderContext) => {
export default ({ loadTestFile }: FtrProviderContext) => {
describe('Triggers Actions UI Example', function () {
loadTestFile(require.resolve('./rule_status_dropdown'));
loadTestFile(require.resolve('./rule_tag_filter'));
@ -16,5 +16,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => {
loadTestFile(require.resolve('./rule_event_log_list'));
loadTestFile(require.resolve('./rules_list'));
loadTestFile(require.resolve('./alerts_table'));
loadTestFile(require.resolve('./rules_settings_link'));
});
};

View file

@ -23,7 +23,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts');
});
it('shoud load from shareable lazy loader', async () => {
it('should load from shareable lazy loader', async () => {
await testSubjects.find('ruleTagFilter');
const exists = await testSubjects.exists('ruleTagFilter');
expect(exists).to.be(true);

View file

@ -23,7 +23,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts');
});
it('shoud load from shareable lazy loader', async () => {
it('should load from shareable lazy loader', async () => {
await testSubjects.find('rulesList');
const exists = await testSubjects.exists('rulesList');
expect(exists).to.be(true);

View file

@ -0,0 +1,32 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../test/functional/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default ({ getPageObjects, getService }: FtrProviderContext) => {
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['common']);
describe('Rules Settings Link', () => {
before(async () => {
await PageObjects.common.navigateToApp('triggersActionsUiExample/rules_settings_link');
});
it('should load from shareable lazy loader', async () => {
const exists = await testSubjects.exists('rulesSettingsLink');
expect(exists).to.be(true);
});
it('should be able to open the modal', async () => {
await testSubjects.click('rulesSettingsLink');
await testSubjects.waitForDeleted('centerJustifiedSpinner');
await testSubjects.existOrFail('rulesSettingsModal');
});
});
};