Skip to content

Commit 8780676

Browse files
committed
[SharedUX] Make solution switch callout dismissible
1 parent 74fcc69 commit 8780676

9 files changed

Lines changed: 135 additions & 35 deletions

File tree

src/platform/packages/shared/shared-ux/page/solution_nav/src/solution_nav.tsx

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import {
3333
EuiPageSidebar,
3434
useEuiMinBreakpoint,
3535
euiCanAnimate,
36-
EuiHorizontalRule,
3736
} from '@elastic/eui';
3837
import { FormattedMessage } from '@kbn/i18n-react';
3938
import { i18n } from '@kbn/i18n';
@@ -84,10 +83,6 @@ export type SolutionNavProps = Omit<EuiSideNavProps<{}>, 'children' | 'items' |
8483
* Hidden when the nav is collapsed.
8584
*/
8685
footer?: React.ReactNode;
87-
/**
88-
* Whether the nav has pinned bottom items.
89-
*/
90-
hasPinnedBottomNavItems?: boolean;
9186
};
9287

9388
const FLYOUT_SIZE = 248;
@@ -119,7 +114,6 @@ export const SolutionNav: FC<SolutionNavProps> = ({
119114
onCollapse,
120115
canBeCollapsed = true,
121116
footer,
122-
hasPinnedBottomNavItems = false,
123117
...rest
124118
}) => {
125119
const { euiTheme } = useEuiTheme();
@@ -263,16 +257,6 @@ export const SolutionNav: FC<SolutionNavProps> = ({
263257

264258
const footerContent = footer && (
265259
<div css={styles.solutionNavFooter} data-test-subj="solutionNavFooter">
266-
<EuiHorizontalRule
267-
margin="m"
268-
css={
269-
hasPinnedBottomNavItems
270-
? css`
271-
margin-block-start: ${euiTheme.size.xs};
272-
`
273-
: undefined
274-
}
275-
/>
276260
{footer}
277261
</div>
278262
);

x-pack/platform/plugins/shared/spaces/public/solution_view_switch/components/modal/solution_selector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const SolutionSelector = ({ selectedSolution, onSolutionChange }: Solutio
3434
return (
3535
<EuiFormRow
3636
label={i18n.translate('xpack.spaces.solutionViewSwitch.modal.solutionLabel', {
37-
defaultMessage: 'Switch to solution view',
37+
defaultMessage: 'Select solution view',
3838
})}
3939
fullWidth
4040
>

x-pack/platform/plugins/shared/spaces/public/solution_view_switch/components/solution_view_switch_callout.test.tsx

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { featuresPluginMock } from '@kbn/features-plugin/public/mocks';
1414
import { renderWithI18n } from '@kbn/test-jest-helpers';
1515

1616
import { getSolutionViewSwitchCalloutComponent } from './solution_view_switch_callout';
17+
import { addSpaceIdToPath, ENTER_SPACE_PATH } from '../../../common';
1718
import type { PluginsStart } from '../../plugin';
1819
import { spacesManagerMock } from '../../spaces_manager/mocks';
1920
import { SOLUTION_VIEW_SWITCH_STORAGE_KEY_PREFIX } from '../constants';
@@ -42,19 +43,27 @@ jest.mock('./modal', () => ({
4243

4344
describe('SolutionViewSwitchCallout', () => {
4445
const originalLocation = window.location;
46+
let hrefSpy: jest.Mock;
4547

4648
beforeEach(() => {
49+
hrefSpy = jest.fn();
4750
Object.defineProperty(window, 'location', {
4851
configurable: true,
52+
writable: true,
4953
value: { ...originalLocation, reload: jest.fn() },
5054
});
55+
Object.defineProperty(window.location, 'href', {
56+
configurable: true,
57+
set: hrefSpy,
58+
});
5159
});
5260

5361
afterEach(() => {
5462
Object.defineProperty(window, 'location', {
5563
configurable: true,
5664
value: originalLocation,
5765
});
66+
localStorage.clear();
5867
jest.restoreAllMocks();
5968
});
6069

@@ -96,7 +105,7 @@ describe('SolutionViewSwitchCallout', () => {
96105
return { user, coreStart, spacesManager, setItemSpy, getUrlForAppSpy };
97106
};
98107

99-
test('updates space, sets localStorage and reloads page on success', async () => {
108+
test('updates space, sets localStorage and navigates to space home page on success', async () => {
100109
const { user, spacesManager, setItemSpy, getUrlForAppSpy } = await setup();
101110

102111
expect(getUrlForAppSpy).toHaveBeenCalledWith('management', { path: 'kibana/spaces' });
@@ -118,7 +127,7 @@ describe('SolutionViewSwitchCallout', () => {
118127
`${SOLUTION_VIEW_SWITCH_STORAGE_KEY_PREFIX}:default`,
119128
'true'
120129
);
121-
expect(window.location.reload).toHaveBeenCalled();
130+
expect(hrefSpy).toHaveBeenCalledWith(addSpaceIdToPath('', 'default', ENTER_SPACE_PATH));
122131
});
123132

124133
test('shows error toast on update failure', async () => {
@@ -137,4 +146,36 @@ describe('SolutionViewSwitchCallout', () => {
137146
);
138147
});
139148
});
149+
150+
test('does not render when previously dismissed', async () => {
151+
jest
152+
.spyOn(Storage.prototype, 'getItem')
153+
.mockImplementation((key) =>
154+
key === `${SOLUTION_VIEW_SWITCH_STORAGE_KEY_PREFIX}.dismissed` ? 'true' : null
155+
);
156+
157+
await setup();
158+
159+
expect(screen.queryByTestId('solutionViewSwitchCalloutDismissButton')).not.toBeInTheDocument();
160+
});
161+
162+
test('renders when not previously dismissed', async () => {
163+
await setup();
164+
165+
expect(screen.getByTestId('solutionViewSwitchCalloutDismissButton')).toBeInTheDocument();
166+
});
167+
168+
test('dismiss button hides callout and sets localStorage flag', async () => {
169+
const { user, setItemSpy } = await setup();
170+
171+
expect(screen.getByTestId('solutionViewSwitchCalloutDismissButton')).toBeInTheDocument();
172+
173+
await user.click(screen.getByTestId('solutionViewSwitchCalloutDismissButton'));
174+
175+
expect(screen.queryByTestId('solutionViewSwitchCalloutDismissButton')).not.toBeInTheDocument();
176+
expect(setItemSpy).toHaveBeenCalledWith(
177+
`${SOLUTION_VIEW_SWITCH_STORAGE_KEY_PREFIX}.dismissed`,
178+
'true'
179+
);
180+
});
140181
});

x-pack/platform/plugins/shared/spaces/public/solution_view_switch/components/solution_view_switch_callout.tsx

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
* 2.0.
66
*/
77

8-
import React from 'react';
8+
import React, { useState } from 'react';
99

1010
import type { StartServicesAccessor } from '@kbn/core/public';
1111
import { i18n } from '@kbn/i18n';
1212

13+
import { addSpaceIdToPath, ENTER_SPACE_PATH } from '../../../common';
1314
import type { PluginsStart } from '../../plugin';
1415
import type { SpacesManager } from '../../spaces_manager';
1516
import { SOLUTION_VIEW_SWITCH_STORAGE_KEY_PREFIX } from '../constants';
@@ -18,6 +19,24 @@ import type {
1819
SolutionViewSwitchCalloutProps,
1920
} from '../types';
2021

22+
const SOLUTION_VIEW_SWITCH_STORAGE_KEY_DISMISSED = `${SOLUTION_VIEW_SWITCH_STORAGE_KEY_PREFIX}.dismissed`;
23+
24+
const getIsDismissed = () => {
25+
try {
26+
return localStorage.getItem(SOLUTION_VIEW_SWITCH_STORAGE_KEY_DISMISSED) === 'true';
27+
} catch {
28+
return false;
29+
}
30+
};
31+
32+
const setIsDismissed = () => {
33+
try {
34+
localStorage.setItem(SOLUTION_VIEW_SWITCH_STORAGE_KEY_DISMISSED, 'true');
35+
} catch {
36+
// Ignore storage errors
37+
}
38+
};
39+
2140
export interface GetSolutionViewSwitchCalloutOptions {
2241
spacesManager: SpacesManager;
2342
getStartServices: StartServicesAccessor<PluginsStart>;
@@ -27,7 +46,7 @@ export const getSolutionViewSwitchCalloutComponent = async ({
2746
spacesManager,
2847
getStartServices,
2948
}: GetSolutionViewSwitchCalloutOptions): Promise<React.FC<SolutionViewSwitchCalloutProps>> => {
30-
const [{ application, notifications }] = await getStartServices();
49+
const [{ application, http, notifications }] = await getStartServices();
3150
const manageSpacesUrl = application.getUrlForApp('management', { path: 'kibana/spaces' });
3251

3352
const showError: SolutionViewSwitchCalloutInternalProps['showError'] = (error) => {
@@ -54,20 +73,35 @@ export const getSolutionViewSwitchCalloutComponent = async ({
5473
} catch {
5574
// Ignore storage errors
5675
}
57-
window.location.reload();
76+
77+
window.location.href = addSpaceIdToPath(
78+
http.basePath.serverBasePath,
79+
spaceId,
80+
ENTER_SPACE_PATH
81+
);
5882
};
5983

6084
const { SolutionViewSwitchCalloutInternal } = await import(
6185
'./solution_view_switch_callout_internal'
6286
);
6387

6488
return (props: SolutionViewSwitchCalloutProps) => {
89+
const [shouldShow, setShouldShow] = useState<boolean>(() => !getIsDismissed());
90+
91+
const onDismiss: SolutionViewSwitchCalloutInternalProps['onDismiss'] = () => {
92+
setIsDismissed();
93+
setShouldShow(false);
94+
};
95+
96+
if (!shouldShow) return null;
97+
6598
return (
6699
<SolutionViewSwitchCalloutInternal
67100
{...props}
68101
manageSpacesUrl={manageSpacesUrl}
69102
updateSpace={updateSpace}
70103
showError={showError}
104+
onDismiss={onDismiss}
71105
/>
72106
);
73107
};

x-pack/platform/plugins/shared/spaces/public/solution_view_switch/components/solution_view_switch_callout_internal.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ describe('SolutionViewSwitchCalloutInternal', () => {
4949
manageSpacesUrl="app/management/kibana/spaces"
5050
updateSpace={updateSpace}
5151
showError={showError}
52+
onDismiss={jest.fn()}
5253
/>
5354
);
5455

x-pack/platform/plugins/shared/spaces/public/solution_view_switch/components/solution_view_switch_callout_internal.tsx

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,17 @@
55
* 2.0.
66
*/
77

8-
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui';
8+
import {
9+
EuiButton,
10+
EuiButtonIcon,
11+
EuiFlexGroup,
12+
EuiFlexItem,
13+
EuiHorizontalRule,
14+
EuiSpacer,
15+
EuiText,
16+
useEuiTheme,
17+
} from '@elastic/eui';
18+
import { css } from '@emotion/react';
919
import React, { useState } from 'react';
1020

1121
import { i18n } from '@kbn/i18n';
@@ -23,10 +33,14 @@ export const SolutionViewSwitchCalloutInternal = ({
2333
manageSpacesUrl,
2434
updateSpace,
2535
showError,
36+
onDismiss,
2637
}: SolutionViewSwitchCalloutProps & SolutionViewSwitchCalloutInternalProps) => {
38+
const { euiTheme } = useEuiTheme();
2739
const [isModalOpen, setIsModalOpen] = useState(false);
2840
const [isLoading, setIsLoading] = useState(false);
2941

42+
const narrowTopMargin = currentSolution === 'security';
43+
3044
const handleSwitch = async (selectedSolution: SupportedSolutionView) => {
3145
setIsLoading(true);
3246
try {
@@ -40,20 +54,49 @@ export const SolutionViewSwitchCalloutInternal = ({
4054

4155
return (
4256
<>
57+
<EuiHorizontalRule
58+
margin="m"
59+
css={
60+
narrowTopMargin
61+
? css`
62+
margin-block-start: ${euiTheme.size.xs};
63+
`
64+
: undefined
65+
}
66+
/>
4367
<EuiFlexGroup direction="column" gutterSize="m">
4468
<EuiFlexItem grow={false}>
45-
<EuiText size="s">
46-
<strong>
47-
{i18n.translate('xpack.spaces.solutionViewSwitch.callout.title', {
48-
defaultMessage: 'New navigation available',
49-
})}
50-
</strong>
51-
</EuiText>
69+
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center" gutterSize="none">
70+
<EuiFlexItem grow={false}>
71+
<EuiText size="s">
72+
<strong>
73+
{i18n.translate('xpack.spaces.solutionViewSwitch.callout.title', {
74+
defaultMessage: 'New navigation available',
75+
})}
76+
</strong>
77+
</EuiText>
78+
</EuiFlexItem>
79+
<EuiFlexItem grow={false}>
80+
<EuiButtonIcon
81+
data-test-subj="solutionViewSwitchCalloutDismissButton"
82+
iconType="cross"
83+
size="xs"
84+
color="text"
85+
aria-label={i18n.translate(
86+
'xpack.spaces.solutionViewSwitch.callout.dismissButton',
87+
{
88+
defaultMessage: 'Dismiss',
89+
}
90+
)}
91+
onClick={onDismiss}
92+
/>
93+
</EuiFlexItem>
94+
</EuiFlexGroup>
5295
<EuiSpacer size="s" />
5396
<EuiText size="s">
5497
{i18n.translate('xpack.spaces.solutionViewSwitch.callout.description', {
5598
defaultMessage:
56-
'A simplified left nav built for {solutionName} with easier access to analytics and management.',
99+
'Switch to {solutionName} nav for easier access to analytics and management.',
57100
values: { solutionName: SOLUTION_VIEW_CONFIG[currentSolution].name },
58101
})}
59102
</EuiText>

x-pack/platform/plugins/shared/spaces/public/solution_view_switch/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface SolutionViewSwitchCalloutInternalProps {
1111
manageSpacesUrl: string;
1212
updateSpace: (solution: SupportedSolutionView) => Promise<void>;
1313
showError: (error: unknown) => void;
14+
onDismiss: () => void;
1415
}
1516

1617
export interface SolutionViewSwitchCalloutProps {

x-pack/solutions/security/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_security_solution_navigation.test.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,15 +113,13 @@ describe('Security Solution Navigation', () => {
113113
it('includes footer with SolutionViewSwitchCallout when all conditions are met', () => {
114114
const { result } = renderHook(useSecuritySolutionNavigation);
115115
expect(result.current?.footer).toBeDefined();
116-
expect(result.current?.hasPinnedBottomNavItems).toBe(true);
117116
});
118117

119118
it('does not include footer when announcements are disabled', () => {
120119
mockNotifications.tours.isEnabled.mockReturnValue(false);
121120

122121
const { result } = renderHook(useSecuritySolutionNavigation);
123122
expect(result.current?.footer).toBeUndefined();
124-
expect(result.current?.hasPinnedBottomNavItems).toBeUndefined();
125123
});
126124

127125
it('does not include footer when canManageSpaces is false', () => {
@@ -132,7 +130,6 @@ describe('Security Solution Navigation', () => {
132130

133131
const { result } = renderHook(useSecuritySolutionNavigation);
134132
expect(result.current?.footer).toBeUndefined();
135-
expect(result.current?.hasPinnedBottomNavItems).toBeUndefined();
136133
});
137134
});
138135
});

x-pack/solutions/security/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_security_solution_navigation.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,5 @@ export const useSecuritySolutionNavigation = (): KibanaPageTemplateProps['soluti
5555
children: <SecuritySideNav />,
5656
closeFlyoutButtonPosition: 'inside',
5757
footer: solutionNavFooter,
58-
...(solutionNavFooter ? { hasPinnedBottomNavItems: true } : {}),
5958
};
6059
};

0 commit comments

Comments
 (0)