123580 o11 rules page (#124132)

* testing find rules api & load observability rules

* add rules option in observability sidebar

* make rules menu option configurable

* create o11y rules page

* update manage rules link

* rules page

* filter o11y rule types only(temp solution)

* remove unused stuff

* Add documentation link

* fix typescript errors

* add selection to EuiBasicTable

* add rest columns in the table

* toggle popover

* add actions column

* temp

* add breadcrumbs to rules page

* add pagination to rules page

* add onChange handler in rules pagination

* add create rule button

* add icon type to the create rule button

* Show number of rules

* add auto refresh button

* use correct rule management link in apm based on feature flag

* use correct rule management link in infra and observability alerts page based on on feature flag

* centralize useRulesLink logic inside the new useRulesLinkCreator observability hook

* useRulesLink in uptime

* useRulesLink in apm

* mock observability useLinks function in uptime tests

* temporarily remove the create rule button

* remove unused console statement

* remove unused button

* fix uptime failing tests

* remove useContextForPlugin and rename hook to create_use_rules_link

* remove useKibanaForContext from uptime and use useKibana instead

* remove unused imports

* add a todo comment in the loadRules export

* use await import for createUseRulesLink and declare core and setup as async

* fix typescript error

* Revert "fix typescript error"

This reverts commit c5a67d5d56.

* Revert "use await import for createUseRulesLink and declare core and setup as async"

This reverts commit 627a6265cf.

* experiment with page bundle size

* fix for useEffect

* use async import for loadRules

* experiments

* Revert "experiments"

This reverts commit 8b389dbf6a.

* increase page bundle size limit

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
mgiota 2022-03-08 19:32:19 +01:00 committed by GitHub
parent b5e194532d
commit 2fa485a29b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 357 additions and 45 deletions

View file

@ -41,7 +41,7 @@ pageLoadAssetSize:
monitoring: 80000
navigation: 37269
newsfeed: 42228
observability: 89709
observability: 95000
painlessLab: 179748
remoteClusters: 51327
rollup: 97204

View file

@ -16,6 +16,7 @@ import React, { useState } from 'react';
import { IBasePath } from '../../../../../../../src/core/public';
import { AlertType } from '../../../../common/alert_types';
import { AlertingFlyout } from '../../alerting/alerting_flyout';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
const alertLabel = i18n.translate('xpack.apm.home.alertsMenu.alerts', {
defaultMessage: 'Alerts and rules',
@ -63,7 +64,9 @@ export function AlertingPopoverAndFlyout({
}: Props) {
const [popoverOpen, setPopoverOpen] = useState(false);
const [alertType, setAlertType] = useState<AlertType | null>(null);
const {
plugins: { observability },
} = useApmPluginContext();
const button = (
<EuiHeaderLink
color="text"
@ -103,9 +106,7 @@ export function AlertingPopoverAndFlyout({
'xpack.apm.home.alertsMenu.viewActiveAlerts',
{ defaultMessage: 'Manage rules' }
),
href: basePath.prepend(
'/app/management/insightsAndAlerting/triggersActions/alerts'
),
href: observability.useRulesLink().href,
icon: 'tableOfContents',
},
]

View file

@ -17,14 +17,17 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { PrefilledInventoryAlertFlyout } from '../../inventory/components/alert_flyout';
import { PrefilledThresholdAlertFlyout } from '../../metric_threshold/components/alert_flyout';
import { useRulesLink } from '../../../../../observability/public';
import { InfraClientStartDeps } from '../../../types';
type VisibleFlyoutType = 'inventory' | 'threshold' | null;
export const MetricsAlertDropdown = () => {
const [popoverOpen, setPopoverOpen] = useState(false);
const [visibleFlyoutType, setVisibleFlyoutType] = useState<VisibleFlyoutType>(null);
const uiCapabilities = useKibana().services.application?.capabilities;
const {
services: { observability },
} = useKibana<InfraClientStartDeps>();
const canCreateAlerts = useMemo(
() => Boolean(uiCapabilities?.infrastructure?.save),
[uiCapabilities]
@ -83,7 +86,7 @@ export const MetricsAlertDropdown = () => {
[setVisibleFlyoutType, closePopover]
);
const manageRulesLinkProps = useRulesLink();
const manageRulesLinkProps = observability.useRulesLink();
const manageAlertsMenuItem = useMemo(
() => ({

View file

@ -8,10 +8,14 @@
import { EuiContextMenuItem } from '@elastic/eui';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { useRulesLink } from '../../../../../observability/public';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { InfraClientStartDeps } from '../../../types';
export const ManageAlertsContextMenuItem = () => {
const manageRulesLinkProps = useRulesLink();
const {
services: { observability },
} = useKibana<InfraClientStartDeps>();
const manageRulesLinkProps = observability.useRulesLink();
return (
<EuiContextMenuItem icon="tableOfContents" key="manageLink" {...manageRulesLinkProps}>
<FormattedMessage id="xpack.infra.alerting.manageAlerts" defaultMessage="Manage rules" />

View file

@ -10,7 +10,6 @@ import { i18n } from '@kbn/i18n';
import { EuiPopover, EuiContextMenuItem, EuiContextMenuPanel, EuiHeaderLink } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { AlertFlyout } from './alert_flyout';
import { useRulesLink } from '../../../../../observability/public';
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
const readOnlyUserTooltipContent = i18n.translate(
@ -31,13 +30,14 @@ export const AlertDropdown = () => {
const {
services: {
application: { capabilities },
observability,
},
} = useKibanaContextForPlugin();
const canCreateAlerts = capabilities?.logs?.save ?? false;
const [popoverOpen, setPopoverOpen] = useState(false);
const [flyoutVisible, setFlyoutVisible] = useState(false);
const manageRulesLinkProps = useRulesLink({
const manageRulesLinkProps = observability.useRulesLink({
hrefOnly: true,
});

View file

@ -58,6 +58,7 @@ describe('renderApp', () => {
alertingExperience: { enabled: true },
cases: { enabled: true },
overviewNext: { enabled: false },
rules: { enabled: false },
},
};

View file

@ -47,6 +47,7 @@ describe('APMSection', () => {
alertingExperience: { enabled: true },
cases: { enabled: true },
overviewNext: { enabled: false },
rules: { enabled: false },
},
},
observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(),

View file

@ -47,6 +47,7 @@ describe('UXSection', () => {
alertingExperience: { enabled: true },
cases: { enabled: true },
overviewNext: { enabled: false },
rules: { enabled: false },
},
},
plugins: {

View file

@ -0,0 +1,22 @@
/*
* 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 { Options, useLinkProps } from './use_link_props';
export function createUseRulesLink(isNewRuleManagementEnabled = false) {
return function (options: Options = {}) {
const linkProps = isNewRuleManagementEnabled
? {
app: 'observability',
pathname: '/rules',
}
: {
app: 'management',
pathname: '/insightsAndAlerting/triggersActions/alerts',
};
return useLinkProps(linkProps, options);
};
}

View file

@ -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 { useLinkProps, Options, LinkProps } from './use_link_props';
export function useRulesLink(options?: Options): LinkProps {
const manageRulesLinkProps = useLinkProps(
{
app: 'management',
pathname: '/insightsAndAlerting/triggersActions/alerts',
},
options
);
return manageRulesLinkProps;
}

View file

@ -29,6 +29,7 @@ describe('useTimeRange', () => {
alertingExperience: { enabled: true },
cases: { enabled: true },
overviewNext: { enabled: false },
rules: { enabled: false },
},
},
plugins: {
@ -78,6 +79,7 @@ describe('useTimeRange', () => {
alertingExperience: { enabled: true },
cases: { enabled: true },
overviewNext: { enabled: false },
rules: { enabled: false },
},
},
plugins: {

View file

@ -34,6 +34,7 @@ export { uptimeOverviewLocatorID } from '../common';
export interface ConfigSchema {
unsafe: {
alertingExperience: { enabled: boolean };
rules: { enabled: boolean };
cases: { enabled: boolean };
overviewNext: { enabled: boolean };
};
@ -82,7 +83,7 @@ export * from './typings';
export { useChartTheme } from './hooks/use_chart_theme';
export { useBreadcrumbs } from './hooks/use_breadcrumbs';
export { useTheme } from './hooks/use_theme';
export { useRulesLink } from './hooks/use_rules_link';
export { createUseRulesLink } from './hooks/create_use_rules_link';
export { useLinkProps, shouldHandleLinkEvent } from './hooks/use_link_props';
export type { LinkDescriptor } from './hooks/use_link_props';

View file

@ -45,6 +45,7 @@ interface RuleStatsState {
muted: number;
error: number;
}
export interface TopAlert {
fields: ParsedTechnicalFields & ParsedExperimentalFields;
start: number;
@ -69,7 +70,7 @@ const ALERT_STATUS_REGEX = new RegExp(
);
function AlertsPage() {
const { core, plugins, ObservabilityPageTemplate } = usePluginContext();
const { core, plugins, ObservabilityPageTemplate, config } = usePluginContext();
const [alertFilterStatus, setAlertFilterStatus] = useState('' as AlertStatusFilterButton);
const { prepend } = core.http.basePath;
const refetch = useRef<() => void>();
@ -137,9 +138,9 @@ function AlertsPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// In a future milestone we'll have a page dedicated to rule management in
// observability. For now link to the settings page.
const manageRulesHref = prepend('/app/management/insightsAndAlerting/triggersActions/alerts');
const manageRulesHref = config.unsafe.rules
? prepend('/app/observability/rules')
: prepend('/insightsAndAlerting/triggersActions/alerts');
const dynamicIndexPatternsAsyncState = useAsync(async (): Promise<DataViewBase[]> => {
if (indexNames.length === 0) {

View file

@ -80,6 +80,7 @@ const withCore = makeDecorator({
alertingExperience: { enabled: true },
cases: { enabled: true },
overviewNext: { enabled: false },
rules: { enabled: false },
},
},
core: options as CoreStart,

View file

@ -0,0 +1,261 @@
/*
* 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, { useState, useEffect, useCallback } from 'react';
import moment from 'moment';
import {
EuiBasicTable,
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiText,
EuiBadge,
EuiPopover,
EuiContextMenuPanel,
EuiContextMenuItem,
EuiHorizontalRule,
EuiAutoRefreshButton,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { usePluginContext } from '../../hooks/use_plugin_context';
import { useBreadcrumbs } from '../../hooks/use_breadcrumbs';
import { useKibana } from '../../utils/kibana_react';
const DEFAULT_SEARCH_PAGE_SIZE: number = 25;
interface RuleState {
data: [];
totalItemsCount: number;
}
interface Pagination {
index: number;
size: number;
}
export function RulesPage() {
const { core, ObservabilityPageTemplate } = usePluginContext();
const { docLinks } = useKibana().services;
const {
http,
notifications: { toasts },
} = core;
const [rules, setRules] = useState<RuleState>({ data: [], totalItemsCount: 0 });
const [page, setPage] = useState<Pagination>({ index: 0, size: DEFAULT_SEARCH_PAGE_SIZE });
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
async function loadObservabilityRules() {
const { loadRules } = await import('../../../../triggers_actions_ui/public');
try {
const response = await loadRules({
http,
page: { index: 0, size: DEFAULT_SEARCH_PAGE_SIZE },
typesFilter: [
'xpack.uptime.alerts.monitorStatus',
'xpack.uptime.alerts.tls',
'xpack.uptime.alerts.tlsCertificate',
'xpack.uptime.alerts.durationAnomaly',
'apm.error_rate',
'apm.transaction_error_rate',
'apm.transaction_duration',
'apm.transaction_duration_anomaly',
'metrics.alert.inventory.threshold',
'metrics.alert.threshold',
'logs.alert.document.count',
],
});
setRules({
data: response.data as any,
totalItemsCount: response.total,
});
} catch (_e) {
toasts.addDanger({
title: i18n.translate('xpack.observability.rules.loadError', {
defaultMessage: 'Unable to load rules',
}),
});
}
}
enum RuleStatus {
enabled = 'enabled',
disabled = 'disabled',
}
const statuses = Object.values(RuleStatus);
const togglePopover = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]);
const popOverButton = (
<EuiBadge
iconType="arrowDown"
iconSide="right"
onClick={togglePopover}
onClickAriaLabel="Change status"
>
Enabled
</EuiBadge>
);
const panelItems = statuses.map((status) => (
<EuiContextMenuItem>
<EuiBadge>{status}</EuiBadge>
</EuiContextMenuItem>
));
useEffect(() => {
loadObservabilityRules();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useBreadcrumbs([
{
text: i18n.translate('xpack.observability.breadcrumbs.rulesLinkText', {
defaultMessage: 'Rules',
}),
},
]);
const rulesTableColumns = [
{
field: 'name',
name: i18n.translate('xpack.observability.rules.rulesTable.columns.nameTitle', {
defaultMessage: 'Rule Name',
}),
},
{
field: 'executionStatus.lastExecutionDate',
name: i18n.translate('xpack.observability.rules.rulesTable.columns.lastRunTitle', {
defaultMessage: 'Last run',
}),
render: (date: Date) => {
if (date) {
return (
<>
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="xs">
{moment(date).fromNow()}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
}
},
},
{
field: 'executionStatus.status',
name: i18n.translate('xpack.observability.rules.rulesTable.columns.lastResponseTitle', {
defaultMessage: 'Last response',
}),
},
{
field: 'enabled',
name: i18n.translate('xpack.observability.rules.rulesTable.columns.statusTitle', {
defaultMessage: 'Status',
}),
render: (_enabled: boolean) => {
return (
<EuiPopover
button={popOverButton}
anchorPosition="downLeft"
isOpen={isPopoverOpen}
panelPaddingSize="none"
>
<EuiContextMenuPanel items={panelItems} />
</EuiPopover>
);
},
},
{
field: '*',
name: i18n.translate('xpack.observability.rules.rulesTable.columns.actionsTitle', {
defaultMessage: 'Actions',
}),
actions: [
{
name: 'Edit',
isPrimary: true,
description: 'Edit this rule',
icon: 'pencil',
type: 'icon',
onClick: () => {},
'data-test-subj': 'action-edit',
},
],
},
];
return (
<ObservabilityPageTemplate
pageHeader={{
pageTitle: (
<>{i18n.translate('xpack.observability.rulesTitle', { defaultMessage: 'Rules' })} </>
),
rightSideItems: [
<EuiButtonEmpty
href={docLinks.links.alerting.guide}
target="_blank"
iconType="help"
data-test-subj="documentationLink"
>
<FormattedMessage
id="xpack.observability.rules.docsLinkText"
defaultMessage="Documentation"
/>
</EuiButtonEmpty>,
],
}}
>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiText size="s" color="subdued" data-test-subj="totalAlertsCount">
<FormattedMessage
id="xpack.observability.rules.totalItemsCountDescription"
defaultMessage="Showing: {pageSize} of {totalItemCount} Rules"
values={{
totalItemCount: rules.totalItemsCount,
pageSize: rules.data.length,
}}
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiAutoRefreshButton
isPaused={false}
refreshInterval={3000}
onRefreshChange={() => {}}
shortHand
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule margin="xs" />
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
<EuiBasicTable
items={rules.data}
hasActions={true}
columns={rulesTableColumns}
isSelectable={true}
pagination={{
pageIndex: page.index,
pageSize: page.size,
totalItemCount: rules.totalItemsCount,
}}
onChange={({ page: changedPage }: { page: Pagination }) => {
setPage(changedPage);
}}
selection={{
selectable: () => true,
onSelectionChange: (selectedItems) => {},
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
</ObservabilityPageTemplate>
);
}

View file

@ -46,6 +46,7 @@ import { createNavigationRegistry, NavigationEntry } from './services/navigation
import { updateGlobalNavigation } from './update_global_navigation';
import { getExploratoryViewEmbeddable } from './components/shared/exploratory_view/embeddable';
import { createExploratoryViewUrl } from './components/shared/exploratory_view/configurations/utils';
import { createUseRulesLink } from './hooks/create_use_rules_link';
export type ObservabilityPublicSetup = ReturnType<Plugin['setup']>;
@ -92,11 +93,20 @@ export class Plugin
path: '/alerts',
navLinkStatus: AppNavLinkStatus.hidden,
},
{
id: 'rules',
title: i18n.translate('xpack.observability.rulesLinkTitle', {
defaultMessage: 'Rules',
}),
order: 8002,
path: '/rules',
navLinkStatus: AppNavLinkStatus.hidden,
},
getCasesDeepLinks({
basePath: casesPath,
extend: {
[CasesDeepLinkId.cases]: {
order: 8002,
order: 8003,
navLinkStatus: AppNavLinkStatus.hidden,
},
[CasesDeepLinkId.casesCreate]: {
@ -242,6 +252,7 @@ export class Plugin
navigation: {
registerSections: this.navigationRegistry.registerSections,
},
useRulesLink: createUseRulesLink(config.unsafe.rules.enabled),
};
}
@ -270,6 +281,7 @@ export class Plugin
},
createExploratoryViewUrl,
ExploratoryViewEmbeddable: getExploratoryViewEmbeddable(coreStart, pluginsStart),
useRulesLink: createUseRulesLink(config.unsafe.rules.enabled),
};
}
}

View file

@ -15,6 +15,7 @@ import { LandingPage } from '../pages/landing';
import { OverviewPage } from '../pages/overview';
import { jsonRt } from './json_rt';
import { ObservabilityExploratoryView } from '../components/shared/exploratory_view/obsv_exploratory_view';
import { RulesPage } from '../pages/rules';
export type RouteParams<T extends keyof typeof routes> = DecodeParams<typeof routes[T]['params']>;
@ -87,4 +88,11 @@ export const routes = {
},
exact: true,
},
'/rules': {
handler: () => {
return <RulesPage />;
},
params: {},
exact: true,
},
};

View file

@ -48,6 +48,14 @@ export function updateGlobalNavigation({
? AppNavLinkStatus.visible
: AppNavLinkStatus.hidden,
};
case 'rules':
return {
...link,
navLinkStatus:
config.unsafe.rules.enabled && someVisible
? AppNavLinkStatus.visible
: AppNavLinkStatus.hidden,
};
default:
return link;
}

View file

@ -39,6 +39,7 @@ const config = {
alertingExperience: { enabled: true },
cases: { enabled: true },
overviewNext: { enabled: false },
rules: { enabled: false },
},
};

View file

@ -33,6 +33,7 @@ export const config: PluginConfigDescriptor = {
}),
unsafe: schema.object({
alertingExperience: schema.object({ enabled: schema.boolean({ defaultValue: true }) }),
rules: schema.object({ enabled: schema.boolean({ defaultValue: false }) }),
cases: schema.object({ enabled: schema.boolean({ defaultValue: true }) }),
overviewNext: schema.object({ enabled: schema.boolean({ defaultValue: false }) }),
}),

View file

@ -45,6 +45,8 @@ export function plugin() {
export { Plugin };
export * from './plugin';
// TODO remove this import when we expose the Rules tables as a component
export { loadRules } from './application/lib/rule_api/rules';
export { loadRuleAggregations } from './application/lib/rule_api/aggregate';
export { loadActionTypes } from './application/lib/action_connector_api/connector_types';

View file

@ -18,6 +18,8 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { CLIENT_ALERT_TYPES } from '../../../../common/constants/alerts';
import { ClientPluginsStart } from '../../../../public/apps/plugin';
import { ToggleFlyoutTranslations } from './translations';
import { ToggleAlertFlyoutButtonProps } from './alerts_containers';
@ -43,7 +45,10 @@ export const ToggleAlertFlyoutButtonComponent: React.FC<Props> = ({
}) => {
const [isOpen, setIsOpen] = useState<boolean>(false);
const kibana = useKibana();
const {
services: { observability },
} = useKibana<ClientPluginsStart>();
const manageRulesUrl = observability.useRulesLink();
const hasUptimeWrite = kibana.services.application?.capabilities.uptime?.save ?? false;
const monitorStatusAlertContextMenuItem: EuiContextMenuPanelItemDescriptor = {
@ -70,12 +75,7 @@ export const ToggleAlertFlyoutButtonComponent: React.FC<Props> = ({
'aria-label': ToggleFlyoutTranslations.navigateToAlertingUIAriaLabel,
'data-test-subj': 'xpack.uptime.navigateToAlertingUi',
name: (
<EuiLink
color="text"
href={kibana.services?.application?.getUrlForApp(
'management/insightsAndAlerting/triggersActions/alerts'
)}
>
<EuiLink color="text" href={manageRulesUrl.href}>
<FormattedMessage
id="xpack.uptime.navigateToAlertingButton.content"
defaultMessage="Manage rules"

View file

@ -134,6 +134,7 @@ export const mockCore: () => Partial<CoreStart> = () => {
storage: createMockStore(),
data: dataPluginMock.createStartContract(),
observability: {
useRulesLink: () => ({ href: 'newRuleLink' }),
navigation: {
// @ts-ignore
PageTemplate: EuiPageTemplate,