Skip to content

Commit

Permalink
Promote solutions view in Discover top nav (elastic#208320)
Browse files Browse the repository at this point in the history
Resolves elastic#204971

## Summary

Displays badge in Discover top nav to promote solutions view.

## Screenshot

<img width="1426" alt="Screenshot 2025-01-27 at 09 13 38"
src="https://github.com/user-attachments/assets/0eab9a10-19e1-4b68-9f22-2f74989c0dac"
/>
  • Loading branch information
thomheymann authored Jan 28, 2025
1 parent 04676b2 commit 74ec59f
Show file tree
Hide file tree
Showing 5 changed files with 263 additions and 2 deletions.
4 changes: 2 additions & 2 deletions src/platform/plugins/shared/discover/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"observabilityAIAssistant",
"aiops",
"fieldsMetadata",
"logsDataAccess",
"logsDataAccess"
],
"requiredBundles": [
"kibanaUtils",
Expand All @@ -60,4 +60,4 @@
"common"
]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock'
import { savedSearchMock } from '../../../../__mocks__/saved_search';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { spacesPluginMock } from '@kbn/spaces-plugin/public/mocks';

const stateContainer = getDiscoverStateMock({ isTimeBased: true });
const discoverServiceMock = createDiscoverServicesMock();
Expand Down Expand Up @@ -128,4 +129,27 @@ describe('getTopNavBadges()', function () {
});
expect(topNavBadges).toMatchInlineSnapshot(`Array []`);
});

describe('solutions view badge', () => {
const discoverServiceWithSpacesMock = createDiscoverServicesMock();
discoverServiceWithSpacesMock.capabilities.discover.save = true;
discoverServiceWithSpacesMock.spaces = spacesPluginMock.createStartContract();

test('should return the solutions view badge when spaces is enabled', () => {
const topNavBadges = getTopNavBadges({
hasUnsavedChanges: false,
services: discoverServiceWithSpacesMock,
stateContainer,
topNavCustomization: undefined,
});
expect(topNavBadges).toMatchInlineSnapshot(`
Array [
Object {
"badgeText": "Check out context-aware Discover",
"renderCustomBadge": [Function],
},
]
`);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import React from 'react';
import type { TopNavMenuBadgeProps } from '@kbn/navigation-plugin/public';
import { getTopNavUnsavedChangesBadge } from '@kbn/unsaved-changes-badge';
import { getManagedContentBadge } from '@kbn/managed-content-badge';
Expand All @@ -16,6 +17,7 @@ import { DiscoverStateContainer } from '../../state_management/discover_state';
import type { TopNavCustomization } from '../../../../customizations';
import { onSaveSearch } from './on_save_search';
import { DiscoverServices } from '../../../../build_services';
import { SolutionsViewBadge } from './solutions_view_badge';

/**
* Helper function to build the top nav badges
Expand Down Expand Up @@ -44,6 +46,15 @@ export const getTopNavBadges = ({

const isManaged = stateContainer.savedSearchState.getState().managed;

if (services.spaces) {
entries.push({
badgeText: i18n.translate('discover.topNav.solutionViewTitle', {
defaultMessage: 'Check out context-aware Discover',
}),
renderCustomBadge: ({ badgeText }) => <SolutionsViewBadge badgeText={badgeText} />,
});
}

if (hasUnsavedChanges && !defaultBadges?.unsavedChangesBadge?.disabled) {
entries.push(
getTopNavUnsavedChangesBadge({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* 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 React from 'react';
import { of } from 'rxjs';
import { SolutionsViewBadge } from './solutions_view_badge';
import { render } from '@testing-library/react';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';

jest.mock('../../../../hooks/use_discover_services');

const useDiscoverServicesMock = useDiscoverServices as jest.Mock;

describe('SolutionsViewBadge', () => {
test('renders badge', async () => {
useDiscoverServicesMock.mockReturnValue({
spaces: {
getActiveSpace$: jest.fn().mockReturnValue(
of({
id: 'default',
solution: 'classic',
})
),
isSolutionViewEnabled: true,
},
docLinks: { ELASTIC_WEBSITE_URL: 'https://www.elastic.co' },
addBasePath: (path: string) => path,
capabilities: { spaces: { manage: true } },
});

const { container, findByTitle, findByRole } = render(
<IntlProvider locale="en">
<SolutionsViewBadge badgeText="Toggle popover" />
</IntlProvider>
);
expect(container).not.toBeEmptyDOMElement();

const button = await findByTitle('Toggle popover');
await button.click();
const dialog = await findByRole('dialog');
expect(dialog).not.toBeEmptyDOMElement();
});

test('does not render badge when active space is already configured to use a solution view other than "classic"', async () => {
useDiscoverServicesMock.mockReturnValue({
spaces: {
getActiveSpace$: jest.fn().mockReturnValue(
of({
id: 'default',
solution: 'oblt',
})
),
isSolutionViewEnabled: true,
},
docLinks: { ELASTIC_WEBSITE_URL: 'https://www.elastic.co' },
addBasePath: (path: string) => path,
capabilities: { spaces: { manage: true } },
});

const { container } = render(
<IntlProvider locale="en">
<SolutionsViewBadge badgeText="Toggle popover" />
</IntlProvider>
);
expect(container).toBeEmptyDOMElement();
});

test('does not render badge when spaces is disabled (no active space available)', async () => {
useDiscoverServicesMock.mockReturnValue({
spaces: {
getActiveSpace$: jest.fn().mockReturnValue(of(undefined)),
isSolutionViewEnabled: true,
},
docLinks: { ELASTIC_WEBSITE_URL: 'https://www.elastic.co' },
addBasePath: (path: string) => path,
capabilities: { spaces: { manage: true } },
});

const { container } = render(
<IntlProvider locale="en">
<SolutionsViewBadge badgeText="Toggle popover" />
</IntlProvider>
);
expect(container).toBeEmptyDOMElement();
});

test('does not render badge when solution visibility feature is disabled', async () => {
useDiscoverServicesMock.mockReturnValue({
spaces: {
getActiveSpace$: jest.fn().mockReturnValue(
of({
id: 'default',
solution: 'classic',
})
),
isSolutionViewEnabled: false,
},
docLinks: { ELASTIC_WEBSITE_URL: 'https://www.elastic.co' },
addBasePath: (path: string) => path,
capabilities: { spaces: { manage: true } },
});

const { container } = render(
<IntlProvider locale="en">
<SolutionsViewBadge badgeText="Toggle popover" />
</IntlProvider>
);
expect(container).toBeEmptyDOMElement();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* 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 React, { useState, useMemo, type FunctionComponent } from 'react';
import { EuiBadge, EuiPopover, EuiPopoverFooter, EuiText, EuiLink } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import useObservable from 'react-use/lib/useObservable';
import { of } from 'rxjs';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';

export const SolutionsViewBadge: FunctionComponent<{ badgeText: string }> = ({ badgeText }) => {
const services = useDiscoverServices();
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const activeSpace$ = useMemo(
() => services.spaces?.getActiveSpace$() ?? of(undefined),
[services.spaces]
);
const activeSpace = useObservable(activeSpace$);
const canManageSpaces = services.capabilities.spaces?.manage === true;

// Do not render this component if one of the following conditions is met:
// 1. Solution visibility feature is disabled
// 2. Spaces is disabled (No active space available)
// 3. Active space is already configured to use a solution view other than "classic".
if (
!services.spaces?.isSolutionViewEnabled ||
!activeSpace ||
(activeSpace.solution && activeSpace.solution !== 'classic')
) {
return null;
}

const onClickAriaLabel = i18n.translate(
'discover.topNav.solutionsViewBadge.clickToLearnMoreAriaLabel',
{
defaultMessage: 'Click to learn more about the “solution view”',
}
);

return (
<EuiPopover
button={
<EuiBadge
color="hollow"
iconType="questionInCircle"
iconSide="right"
onClick={() => setIsPopoverOpen((value) => !value)}
onClickAriaLabel={onClickAriaLabel}
iconOnClick={() => setIsPopoverOpen((value) => !value)}
iconOnClickAriaLabel={onClickAriaLabel}
>
{badgeText}
</EuiBadge>
}
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(false)}
panelStyle={{ maxWidth: 300 }}
>
{canManageSpaces ? (
<>
<EuiText size="s">
<p>
<FormattedMessage
id="discover.topNav.solutionsViewBadge.canManageSpacesDescription"
defaultMessage="We improved Discover so your view adapts to what you're exploring. Choose Observability or Security as your “solution view” in your space settings."
/>
</p>
</EuiText>
<EuiPopoverFooter>
<EuiLink
href={services.addBasePath(`/app/management/kibana/spaces/edit/${activeSpace.id}`)}
target="_blank"
>
<FormattedMessage
id="discover.topNav.solutionsViewBadge.spaceSettingsLink"
defaultMessage="Space settings"
/>
</EuiLink>
</EuiPopoverFooter>
</>
) : (
<>
<FormattedMessage
id="discover.topNav.solutionsViewBadge.cannotManageSpacesDescription"
defaultMessage="We enhanced Discover to adapt seamlessly to what you're exploring. Select Observability or Security as the “solution view” — ask your admin to set it in the space settings."
/>
<EuiPopoverFooter>
<EuiLink
href={`${services.docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/8.16/whats-new.html#_contextual_data_presentation`} // Hardcoded to 8.16 since release notes for other versions will not include the linked feature
target="_blank"
>
<FormattedMessage
id="discover.topNav.solutionsViewBadge.releaseNotesLink"
defaultMessage="Check out the release notes"
/>
</EuiLink>
</EuiPopoverFooter>
</>
)}
</EuiPopover>
);
};

0 comments on commit 74ec59f

Please sign in to comment.