Skip to content

Commit

Permalink
Single view in app function for rule actions variables and UI page (#…
Browse files Browse the repository at this point in the history
…148671)

Resolves: #145132.

In this PR, I'm adding a new function to the server-side rule type
definition called `viewInAppRelativeUrl`. This function returns a
relative path to view the rule in the proper application that will
provide more context. This relative path is used to build the `rule.url`
mustache variable for the actions (overriding the rule details page link
when defined) as well as a fallback for the UI's `View in App` button if
no navigation is registered on the front-end.

Follow up issues:
- #149608
- #151355

## ML to verify

1.  Create an anomaly detection rule from the ML application
2. Go to stack management rule details page
3. Click "View in App"
4. Ensure it brings you to the ML app properly.
5. Repeat step 1 to 4 in a space that isn't the default

Note: ML won't take advantage of the new functionality yet, but we plan
to help in a follow up #149608
so that ML anomaly detection rules can provide a view in app URL within
the rule action messages.

## ResponseOps to verify

1. Set `server.publicBaseUrl` to the proper value in your kibana.yml
6. Modify the [index threshold rule
type](https://github.com/elastic/kibana/blob/main/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.ts#L108-L136)
to have a `getViewInAppRelativeUrl` function returning
`/app/management/insightsAndAlerting/triggersActionsConnectors/connectors`.
7. Create an index threshold rule that always fires. Make sure to add a
a server log action that contains the `{{rule.url}}` variable.
8. Pull the printed URL from the server logs and make sure it works and
brings you to the connectors page.
9. Navigate to the rule details page, click the "View in App" button and
ensure it also brings you to the connectors page.
10. Create a Kibana space.
11. Go into that created space and repeat step 3 to 5. Ensure the URL
and View in App keep you in the same space.

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
mikecote and kibanamachine committed Feb 16, 2023
1 parent 2406ada commit ccd78c9
Show file tree
Hide file tree
Showing 25 changed files with 156 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function registerNavigation(alerting: AlertingSetup) {
alerting.registerNavigation(
ALERTING_EXAMPLE_APP_ID,
'example.people-in-space',
(rule: SanitizedRule) => `/astros/${rule.id}`
(rule: SanitizedRule) => `/app/${ALERTING_EXAMPLE_APP_ID}/astros/${rule.id}`
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function registerNavigation(alerting: AlertingSetup) {
// register default navigation
alerting.registerDefaultNavigation(
ALERTING_EXAMPLE_APP_ID,
(rule: SanitizedRule) => `/rule/${rule.id}`
(rule: SanitizedRule) => `/app/${ALERTING_EXAMPLE_APP_ID}/rule/${rule.id}`
);

registerPeopleInSpaceNavigation(alerting);
Expand Down
3 changes: 3 additions & 0 deletions x-pack/examples/alerting_example/server/alert_types/astros.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,7 @@ export const alertType: RuleType<
};
},
producer: ALERTING_EXAMPLE_APP_ID,
getViewInAppRelativeUrl({ rule }) {
return `/app/${ALERTING_EXAMPLE_APP_ID}/astros/${rule.id}`;
},
};
2 changes: 1 addition & 1 deletion x-pack/plugins/alerting/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -695,7 +695,7 @@ alerting.registerNavigation(

This tells the Alerting Framework that, given a rule of the RuleType whose ID is `my-application-id.my-unique-rule-type`, if that rule's `consumer` value (which is set when the rule is created by your plugin) is your application (whose id is `my-application-id`), then it will navigate to your application using the path `/my-unique-rule/${the id of the rule}`.

The navigation is handled using the `navigateToApp` API, meaning that the path will be automatically picked up by your `react-router-dom` **Route** component, so all you have top do is configure a Route that handles the path `/my-unique-rule/:id`.
The navigation is handled using the `navigateToUrl` API, meaning that the path will be automatically picked up by your `react-router-dom` **Route** component, so all you have top do is configure a Route that handles the path `/my-unique-rule/:id`.

You can look at the `alerting-example` plugin to see an example of using this API, which is enabled using the `--run-examples` flag when you run `yarn start`.

Expand Down
1 change: 0 additions & 1 deletion x-pack/plugins/alerting/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export * from './rule';
export * from './rules_settings';
export * from './rule_type';
export * from './rule_task_instance';
export * from './rule_navigation';
export * from './alert_instance';
export * from './alert_summary';
export * from './builtin_action_groups';
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/alerting/common/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export interface Rule<Params extends RuleTypeParams = never> {
lastRun?: RuleLastRun | null;
nextRun?: Date | null;
running?: boolean | null;
viewInAppRelativeUrl?: string;
}

export type SanitizedRule<Params extends RuleTypeParams = never> = Omit<Rule<Params>, 'apiKey'>;
Expand Down
15 changes: 0 additions & 15 deletions x-pack/plugins/alerting/common/rule_navigation.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const mockRuleType = (id: string): RuleType => ({

describe('AlertNavigationRegistry', () => {
function handler(rule: SanitizedRule) {
return {};
return '';
}

describe('has()', () => {
Expand Down Expand Up @@ -151,7 +151,7 @@ describe('AlertNavigationRegistry', () => {
const registry = new AlertNavigationRegistry();

function indexThresholdHandler(rule: SanitizedRule) {
return {};
return '';
}

const indexThresholdRuleType = mockRuleType('indexThreshold');
Expand All @@ -163,7 +163,7 @@ describe('AlertNavigationRegistry', () => {
const registry = new AlertNavigationRegistry();

function defaultHandler(rule: SanitizedRule) {
return {};
return '';
}

registry.registerDefault('siem', defaultHandler);
Expand All @@ -173,10 +173,10 @@ describe('AlertNavigationRegistry', () => {
test('returns default handlers by consumer when there are other rule type handler', () => {
const registry = new AlertNavigationRegistry();

registry.register('siem', mockRuleType('indexThreshold').id, () => ({}));
registry.register('siem', mockRuleType('indexThreshold').id, () => '');

function defaultHandler(rule: SanitizedRule) {
return {};
return '';
}

registry.registerDefault('siem', defaultHandler);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
* 2.0.
*/

import { JsonObject } from '@kbn/utility-types';
import { SanitizedRule } from '../../common';

/**
Expand All @@ -17,4 +16,4 @@ import { SanitizedRule } from '../../common';
* originally registered to {@link PluginSetupContract.registerNavigation}.
*
*/
export type AlertNavigationHandler = (rule: SanitizedRule) => JsonObject | string;
export type AlertNavigationHandler = (rule: SanitizedRule) => string;
2 changes: 2 additions & 0 deletions x-pack/plugins/alerting/public/lib/common_transformations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export function transformRule(input: ApiRule): Rule {
next_run: nextRun,
last_run: lastRun,
monitoring: monitoring,
view_in_app_relative_url: viewInAppRelativeUrl,
...rest
} = input;

Expand All @@ -135,6 +136,7 @@ export function transformRule(input: ApiRule): Rule {
executionStatus: transformExecutionStatus(executionStatusAPI),
actions: actionsAPI ? actionsAPI.map((action) => transformAction(action)) : [],
scheduledTaskId,
...(viewInAppRelativeUrl ? { viewInAppRelativeUrl } : {}),
...(nextRun ? { nextRun: new Date(nextRun) } : {}),
...(monitoring ? { monitoring: transformMonitoring(monitoring) } : {}),
...(lastRun ? { lastRun: transformLastRun(lastRun) } : {}),
Expand Down
35 changes: 35 additions & 0 deletions x-pack/plugins/alerting/public/plugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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 { AlertingPublicPlugin } from './plugin';
import { coreMock } from '@kbn/core/public/mocks';

jest.mock('./alert_api', () => ({
loadRule: jest.fn(),
loadRuleType: jest.fn(),
}));

describe('Alerting Public Plugin', () => {
describe('start()', () => {
it(`should fallback to the viewInAppRelativeUrl part of the rule object if navigation isn't registered`, async () => {
const { loadRule, loadRuleType } = jest.requireMock('./alert_api');
loadRule.mockResolvedValue({
alertTypeId: 'foo',
consumer: 'abc',
viewInAppRelativeUrl: '/my/custom/path',
});
loadRuleType.mockResolvedValue({});

const plugin = new AlertingPublicPlugin();
plugin.setup();
const pluginStart = plugin.start(coreMock.createStart());

const navigationPath = await pluginStart.getNavigation('123');
expect(navigationPath).toEqual('/my/custom/path');
});
});
});
19 changes: 13 additions & 6 deletions x-pack/plugins/alerting/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
* 2.0.
*/

import { CoreSetup, Plugin, CoreStart } from '@kbn/core/public';
import { Plugin, CoreStart } from '@kbn/core/public';

import { AlertNavigationRegistry, AlertNavigationHandler } from './alert_navigation_registry';
import { loadRule, loadRuleType } from './alert_api';
import { Rule, RuleNavigation } from '../common';
import { Rule } from '../common';

export interface PluginSetupContract {
/**
Expand All @@ -26,6 +26,8 @@ export interface PluginSetupContract {
* @param handler The navigation handler should return either a relative URL, or a state object. This information can be used,
* in conjunction with the consumer id, to navigate the user to a custom URL to view a rule's details.
* @throws an error if the given applicationId and ruleType combination has already been registered.
*
* @deprecated use "getViewInAppRelativeUrl" on the server side rule type instead.
*/
registerNavigation: (
applicationId: string,
Expand All @@ -42,16 +44,18 @@ export interface PluginSetupContract {
* @param applicationId The application id that the user should be navigated to, to view a particular rule in a custom way.
* @param handler The navigation handler should return either a relative URL, or a state object. This information can be used,
* in conjunction with the consumer id, to navigate the user to a custom URL to view a rule's details.
*
* @deprecated use "getViewInAppRelativeUrl" on the server side rule type instead.
*/
registerDefaultNavigation: (applicationId: string, handler: AlertNavigationHandler) => void;
}
export interface PluginStartContract {
getNavigation: (ruleId: Rule['id']) => Promise<RuleNavigation | undefined>;
getNavigation: (ruleId: Rule['id']) => Promise<string | undefined>;
}

export class AlertingPublicPlugin implements Plugin<PluginSetupContract, PluginStartContract> {
private alertNavigationRegistry?: AlertNavigationRegistry;
public setup(core: CoreSetup) {
public setup() {
this.alertNavigationRegistry = new AlertNavigationRegistry();

const registerNavigation = async (
Expand Down Expand Up @@ -89,8 +93,11 @@ export class AlertingPublicPlugin implements Plugin<PluginSetupContract, PluginS

if (this.alertNavigationRegistry!.has(rule.consumer, ruleType)) {
const navigationHandler = this.alertNavigationRegistry!.get(rule.consumer, ruleType);
const state = navigationHandler(rule);
return typeof state === 'string' ? { path: state } : { state };
return navigationHandler(rule);
}

if (rule.viewInAppRelativeUrl) {
return rule.viewInAppRelativeUrl;
}
},
};
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/alerting/server/routes/get_rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const rewriteBodyRes: RewriteResponseCase<SanitizedRule<RuleTypeParams>> = ({
isSnoozedUntil,
lastRun,
nextRun,
viewInAppRelativeUrl,
...rest
}) => ({
...rest,
Expand Down Expand Up @@ -74,6 +75,7 @@ const rewriteBodyRes: RewriteResponseCase<SanitizedRule<RuleTypeParams>> = ({
})),
...(lastRun ? { last_run: rewriteRuleLastRun(lastRun) } : {}),
...(nextRun ? { next_run: nextRun } : {}),
...(viewInAppRelativeUrl ? { view_in_app_relative_url: viewInAppRelativeUrl } : {}),
});

interface BuildGetRulesRouteParams {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export { buildKueryNodeFilter } from './build_kuery_node_filter';
export { generateAPIKeyName } from './generate_api_key_name';
export * from './mapped_params_utils';
export { apiKeyAsAlertAttributes } from './api_key_as_alert_attributes';
export { calculateIsSnoozedUntil } from './calculate_is_snoozed_until';
export * from './inject_references';
export { parseDate } from './parse_date';
export { includeFieldsRequiredForAuthentication } from './include_fields_required_for_authentication';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ import {
RuleWithLegacyId,
PartialRuleWithLegacyId,
} from '../../types';
import { ruleExecutionStatusFromRaw, convertMonitoringFromRawAndVerify } from '../../lib';
import {
ruleExecutionStatusFromRaw,
convertMonitoringFromRawAndVerify,
getRuleSnoozeEndTime,
} from '../../lib';
import { UntypedNormalizedRuleType } from '../../rule_type_registry';
import { getActiveScheduledSnoozes } from '../../lib/is_rule_snoozed';
import {
calculateIsSnoozedUntil,
injectReferencesIntoActions,
injectReferencesIntoParams,
} from '../common';
import { injectReferencesIntoActions, injectReferencesIntoParams } from '../common';
import { RulesClientContext } from '../types';

export interface GetAlertFromRawParams {
Expand Down Expand Up @@ -98,20 +98,20 @@ export function getPartialRuleFromRaw<Params extends RuleTypeParams>(
...s,
rRule: {
...s.rRule,
dtstart: new Date(s.rRule.dtstart),
...(s.rRule.until ? { until: new Date(s.rRule.until) } : {}),
dtstart: new Date(s.rRule.dtstart).toISOString(),
...(s.rRule.until ? { until: new Date(s.rRule.until).toISOString() } : {}),
},
}));
const includeSnoozeSchedule =
snoozeSchedule !== undefined && !isEmpty(snoozeSchedule) && !excludeFromPublicApi;
const isSnoozedUntil = includeSnoozeSchedule
? calculateIsSnoozedUntil({
? getRuleSnoozeEndTime({
muteAll: partialRawRule.muteAll ?? false,
snoozeSchedule,
})
: null;
const includeMonitoring = monitoring && !excludeFromPublicApi;
const rule = {
const rule: PartialRule<Params> = {
id,
notifyWhen,
...omit(partialRawRule, excludeFromPublicApi ? [...context.fieldsToExcludeFromPublicApi] : ''),
Expand Down Expand Up @@ -152,7 +152,23 @@ export function getPartialRuleFromRaw<Params extends RuleTypeParams>(
: {}),
};

return includeLegacyId
? ({ ...rule, legacyId } as PartialRuleWithLegacyId<Params>)
: (rule as PartialRule<Params>);
// Need the `rule` object to build a URL
if (!excludeFromPublicApi) {
const viewInAppRelativeUrl =
ruleType.getViewInAppRelativeUrl &&
ruleType.getViewInAppRelativeUrl({ rule: rule as Rule<Params> });
if (viewInAppRelativeUrl) {
rule.viewInAppRelativeUrl = viewInAppRelativeUrl;
}
}

if (includeLegacyId) {
const result: PartialRuleWithLegacyId<Params> = {
...rule,
legacyId,
};
return result;
}

return rule;
}
Original file line number Diff line number Diff line change
Expand Up @@ -1472,5 +1472,38 @@ describe('Execution Handler', () => {
]
`);
});

it('sets the rule.url to the value from getViewInAppRelativeUrl when the rule type has it defined', async () => {
const execParams = {
...defaultExecutionParams,
rule: ruleWithUrl,
taskRunnerContext: {
...defaultExecutionParams.taskRunnerContext,
kibanaBaseUrl: 'http://localhost:12345',
},
ruleType: {
...ruleType,
getViewInAppRelativeUrl() {
return '/app/management/some/other/place';
},
},
};

const executionHandler = new ExecutionHandler(generateExecutionParams(execParams));
await executionHandler.run(generateAlert({ id: 1 }));

expect(injectActionParamsMock.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"actionParams": Object {
"val": "rule url: http://localhost:12345/s/test1/app/management/some/other/place",
},
"actionTypeId": "test",
"ruleId": "1",
"spaceId": "test1",
},
]
`);
});
});
});
Loading

0 comments on commit ccd78c9

Please sign in to comment.