-
Notifications
You must be signed in to change notification settings - Fork 8.5k
[One Discover] Add app menu actions for Observability projects #198987
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d619e41
c02bf21
d5e2705
8c28477
e9a679a
6cde32f
b32046d
1f0e308
6b2480d
250638f
9e271f7
9f5252f
0553ddc
8c073e2
cb44a07
e9f6c40
58eba92
73bd8ba
13646a8
b7a61fb
c22e5b3
b08d118
e333f5b
65d854d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| /* | ||
| * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side | ||
| * Public License v 1"; you may not use this file except in compliance with, at | ||
| * your election, the "Elastic License 2.0", the "GNU Affero General Public | ||
| * License v3.0 only", or the "Server Side Public License, v 1". | ||
| */ | ||
|
|
||
| import { AppMenuActionId, AppMenuActionType, AppMenuRegistry } from '@kbn/discover-utils'; | ||
| import { DATA_QUALITY_LOCATOR_ID, DataQualityLocatorParams } from '@kbn/deeplinks-observability'; | ||
| import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; | ||
| import { isOfQueryType } from '@kbn/es-query'; | ||
| import { i18n } from '@kbn/i18n'; | ||
| import { AppMenuExtensionParams } from '../../../..'; | ||
| import type { RootProfileProvider } from '../../../../profiles'; | ||
| import { ProfileProviderServices } from '../../../profile_provider_services'; | ||
|
|
||
| export const createGetAppMenu = | ||
| (services: ProfileProviderServices): RootProfileProvider['profile']['getAppMenu'] => | ||
| (prev) => | ||
| (params) => { | ||
| const prevValue = prev(params); | ||
|
|
||
| return { | ||
| appMenuRegistry: (registry) => { | ||
| // Register custom link actions | ||
| registerDatasetQualityLink(registry, services); | ||
| // Register alerts sub menu actions | ||
| registerCreateSLOAction(registry, services, params); | ||
| registerCustomThresholdRuleAction(registry, services, params); | ||
|
|
||
| return prevValue.appMenuRegistry(registry); | ||
| }, | ||
| }; | ||
| }; | ||
|
|
||
| const registerDatasetQualityLink = ( | ||
| registry: AppMenuRegistry, | ||
| { share, timefilter }: ProfileProviderServices | ||
| ) => { | ||
| const dataQualityLocator = | ||
| share?.url.locators.get<DataQualityLocatorParams>(DATA_QUALITY_LOCATOR_ID); | ||
|
|
||
| if (dataQualityLocator) { | ||
| registry.registerCustomAction({ | ||
| id: 'dataset-quality-link', | ||
| type: AppMenuActionType.custom, | ||
| controlProps: { | ||
| label: i18n.translate('discover.observabilitySolution.appMenu.datasets', { | ||
| defaultMessage: 'Data sets', | ||
| }), | ||
| testId: 'discoverAppMenuDatasetQualityLink', | ||
| onClick: ({ onFinishAction }) => { | ||
| const refresh = timefilter.getRefreshInterval(); | ||
| const { from, to } = timefilter.getTime(); | ||
|
|
||
| dataQualityLocator.navigate({ | ||
| filters: { | ||
| timeRange: { | ||
| from: from ?? 'now-24h', | ||
| to: to ?? 'now', | ||
| refresh, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| onFinishAction(); | ||
| }, | ||
| }, | ||
| }); | ||
| } | ||
| }; | ||
|
|
||
| const registerCustomThresholdRuleAction = ( | ||
tonyghiani marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| registry: AppMenuRegistry, | ||
| { data, triggersActionsUi }: ProfileProviderServices, | ||
| { dataView }: AppMenuExtensionParams | ||
| ) => { | ||
| registry.registerCustomActionUnderSubmenu(AppMenuActionId.alerts, { | ||
| id: AppMenuActionId.createRule, | ||
| type: AppMenuActionType.custom, | ||
| order: 101, | ||
| controlProps: { | ||
| label: i18n.translate('discover.observabilitySolution.appMenu.customThresholdRule', { | ||
| defaultMessage: 'Create custom threshold rule', | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi @tonyghiani, The PR changes are looking great and the code is very clean. I have some questions about "Create custom threshold rule" action.
For some reason I am see an error message when trying to create one (I am using a space with Observability set as the current Solution view).
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hey @jughosta 👋
Yes, the custom threshold rule is an observability alert that can be applied to any data stream where the user wants to control a value.
It does work with ad-hoc views, the failure you are facing is due to the fact I was not passing the data view specs, but only the (erroneously) data view pattern, which I fixed in fix(discover): use data view specs for alert rule
|
||
| }), | ||
| iconType: 'visGauge', | ||
| testId: 'discoverAppMenuCustomThresholdRule', | ||
| onClick: ({ onFinishAction }) => { | ||
| const index = dataView?.toMinimalSpec(); | ||
| const { filters, query } = data.query.getState(); | ||
|
|
||
| return triggersActionsUi.getAddRuleFlyout({ | ||
| consumer: 'logs', | ||
| ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, | ||
| canChangeTrigger: false, | ||
| initialValues: { | ||
| params: { | ||
| searchConfiguration: { | ||
| index, | ||
| query, | ||
| filter: filters, | ||
| }, | ||
| }, | ||
| }, | ||
| onClose: onFinishAction, | ||
| }); | ||
| }, | ||
| }, | ||
| }); | ||
| }; | ||
|
|
||
| const registerCreateSLOAction = ( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I reproduced it but it looks like a bug on the SLOs end, we are just embedding the creation flyout.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was unable to reproduce this outside of Discover directly in the SLOs app. It seems they might be stripping the Do you know of another area in Kibana where I can create SLOs to try to reproduce it? Mainly I'm unsure if this is a bug that affects other usages too, or if the expectation is that consumers somehow modify the filters as they do before saving.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hey Davis, I tried this on Logs Explorer too and it is affected in the same way. I stripped the property client side and it works fine now
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice, I confirmed it works now! |
||
| registry: AppMenuRegistry, | ||
| { data, discoverShared }: ProfileProviderServices, | ||
| { dataView, isEsqlMode }: AppMenuExtensionParams | ||
| ) => { | ||
| const sloFeature = discoverShared.features.registry.getById('observability-create-slo'); | ||
|
|
||
| if (sloFeature) { | ||
| registry.registerCustomActionUnderSubmenu(AppMenuActionId.alerts, { | ||
tonyghiani marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| id: 'create-slo', | ||
| type: AppMenuActionType.custom, | ||
| order: 102, | ||
| controlProps: { | ||
| label: i18n.translate('discover.observabilitySolution.appMenu.slo', { | ||
| defaultMessage: 'Create SLO', | ||
| }), | ||
| iconType: 'bell', | ||
| testId: 'discoverAppMenuCreateSlo', | ||
| onClick: ({ onFinishAction }) => { | ||
| const index = dataView?.getIndexPattern(); | ||
tonyghiani marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const timestampField = dataView?.timeFieldName; | ||
| const { filters, query: kqlQuery } = data.query.getState(); | ||
|
|
||
| const filter = isEsqlMode | ||
| ? {} | ||
| : { | ||
| kqlQuery: isOfQueryType(kqlQuery) ? kqlQuery.query : '', | ||
| filters: filters?.map(({ meta, query }) => ({ meta, query })), | ||
| }; | ||
|
|
||
| return sloFeature.createSLOFlyout({ | ||
| initialValues: { | ||
| indicator: { | ||
| type: 'sli.kql.custom', | ||
| params: { | ||
| index, | ||
| timestampField, | ||
| filter, | ||
| }, | ||
| }, | ||
| }, | ||
| onClose: onFinishAction, | ||
| }); | ||
| }, | ||
| }, | ||
| }); | ||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| /* | ||
| * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side | ||
| * Public License v 1"; you may not use this file except in compliance with, at | ||
| * your election, the "Elastic License 2.0", the "GNU Affero General Public | ||
| * License v3.0 only", or the "Server Side Public License, v 1". | ||
| */ | ||
|
|
||
| export { createGetAppMenu } from './get_app_menu'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| /* | ||
| * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side | ||
| * Public License v 1"; you may not use this file except in compliance with, at | ||
| * your election, the "Elastic License 2.0", the "GNU Affero General Public | ||
| * License v3.0 only", or the "Server Side Public License, v 1". | ||
| */ | ||
|
|
||
| export { createObservabilityRootProfileProvider } from './profile'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| /* | ||
| * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side | ||
| * Public License v 1"; you may not use this file except in compliance with, at | ||
| * your election, the "Elastic License 2.0", the "GNU Affero General Public | ||
| * License v3.0 only", or the "Server Side Public License, v 1". | ||
| */ | ||
|
|
||
| import { SolutionType } from '../../../profiles'; | ||
| import { createContextAwarenessMocks } from '../../../__mocks__'; | ||
| import { createObservabilityRootProfileProvider } from './profile'; | ||
|
|
||
| const mockServices = createContextAwarenessMocks().profileProviderServices; | ||
|
|
||
| describe('observabilityRootProfileProvider', () => { | ||
| const observabilityRootProfileProvider = createObservabilityRootProfileProvider(mockServices); | ||
| const RESOLUTION_MATCH = { | ||
| isMatch: true, | ||
| context: { solutionType: SolutionType.Observability }, | ||
| }; | ||
| const RESOLUTION_MISMATCH = { | ||
| isMatch: false, | ||
| }; | ||
|
|
||
| it('should match when the solution project is observability', () => { | ||
| expect( | ||
| observabilityRootProfileProvider.resolve({ | ||
| solutionNavId: SolutionType.Observability, | ||
| }) | ||
| ).toEqual(RESOLUTION_MATCH); | ||
| }); | ||
|
|
||
| it('should NOT match when the solution project anything but observability', () => { | ||
| expect( | ||
| observabilityRootProfileProvider.resolve({ | ||
| solutionNavId: SolutionType.Default, | ||
| }) | ||
| ).toEqual(RESOLUTION_MISMATCH); | ||
| expect( | ||
| observabilityRootProfileProvider.resolve({ | ||
| solutionNavId: SolutionType.Search, | ||
| }) | ||
| ).toEqual(RESOLUTION_MISMATCH); | ||
| expect( | ||
| observabilityRootProfileProvider.resolve({ | ||
| solutionNavId: SolutionType.Security, | ||
| }) | ||
| ).toEqual(RESOLUTION_MISMATCH); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| /* | ||
| * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side | ||
| * Public License v 1"; you may not use this file except in compliance with, at | ||
| * your election, the "Elastic License 2.0", the "GNU Affero General Public | ||
| * License v3.0 only", or the "Server Side Public License, v 1". | ||
| */ | ||
|
|
||
| import { RootProfileProvider, SolutionType } from '../../../profiles'; | ||
| import { ProfileProviderServices } from '../../profile_provider_services'; | ||
| import { createGetAppMenu } from './accessors'; | ||
|
|
||
| export const createObservabilityRootProfileProvider = ( | ||
| services: ProfileProviderServices | ||
| ): RootProfileProvider => ({ | ||
| profileId: 'observability-root-profile', | ||
| profile: { | ||
| getAppMenu: createGetAppMenu(services), | ||
| }, | ||
| resolve: (params) => { | ||
| if (params.solutionNavId === SolutionType.Observability) { | ||
| return { isMatch: true, context: { solutionType: SolutionType.Observability } }; | ||
| } | ||
|
|
||
| return { isMatch: false }; | ||
| }, | ||
| }); |




Uh oh!
There was an error while loading. Please reload this page.