Skip to content

Commit a6a668b

Browse files
authored
[9.2] [SharedUX] Add kbn-tour-queue package to avoid tour overlapping (elastic#242640) (elastic#243754)
# Backport This will backport the following commits from `main` to `9.2`: - [[SharedUX] Add kbn-tour-queue package to avoid tour overlapping (elastic#242640)](elastic#242640) <!--- Backport version: 10.2.0 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Ángeles Martínez Barrio","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-11-20T16:30:43Z","message":"[SharedUX] Add kbn-tour-queue package to avoid tour overlapping (elastic#242640)\n\nCloses https://github.com/elastic/kibana-team/issues/2146\n\n## Summary\n\n- This PR introduces a new package to orchestrate tours to avoid\noverlapping. Currently, Side Nav Tour and Security tour are overlapping\nin this way:\n\n<img width=\"675\" height=\"391\" alt=\"Screenshot 2025-11-12 at 10 32 34\"\nsrc=\"https://github.com/user-attachments/assets/27a3ba52-2c69-4fe0-bf62-18d649354de5\"\n/>\n\n- Package `kbn-tour-queue` uses `globalThis` to share state across\nplugins and to ensure only one single instance of the state manager is\nused. This approach is based on @Dosant `kbn-developer-toolbar` [state\nimplementation](https://github.com/elastic/kibana/blob/7518eebd9acaf352f02241901cbeb7129e0a1f32/src/platform/packages/shared/kbn-developer-toolbar/src/hooks/use_toolbar_state.tsx#L14-L40).\n- If any of the tours in the queue are skipped, the remaining tours are\nalso skipped (currently only Side Nav tour handles skip behaviour).\nSkipped status is not persisted for now, it is only honored until page\nreload.\n- Side Nav and Security tours were updated to use the new tour queue\nsystem.\n\n### Testing\n\nTour completion (Spaces + SideNav + Security):\n\n\nhttps://github.com/user-attachments/assets/139847ac-48ea-41e6-8eba-cf5c4981ed96\n\nTour skipping (Spaces + SideNav):\n\n\nhttps://github.com/user-attachments/assets/e75f7aa0-6cb4-4c43-9117-172f88f1fe8b","sha":"60952a0bbdeb09c931eaf4469c01f21931836651","branchLabelMapping":{"^v9.3.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:SharedUX","backport:version","v9.3.0","v9.2.2"],"title":"[SharedUX] Add kbn-tour-queue package to avoid tour overlapping","number":242640,"url":"https://github.com/elastic/kibana/pull/242640","mergeCommit":{"message":"[SharedUX] Add kbn-tour-queue package to avoid tour overlapping (elastic#242640)\n\nCloses https://github.com/elastic/kibana-team/issues/2146\n\n## Summary\n\n- This PR introduces a new package to orchestrate tours to avoid\noverlapping. Currently, Side Nav Tour and Security tour are overlapping\nin this way:\n\n<img width=\"675\" height=\"391\" alt=\"Screenshot 2025-11-12 at 10 32 34\"\nsrc=\"https://github.com/user-attachments/assets/27a3ba52-2c69-4fe0-bf62-18d649354de5\"\n/>\n\n- Package `kbn-tour-queue` uses `globalThis` to share state across\nplugins and to ensure only one single instance of the state manager is\nused. This approach is based on @Dosant `kbn-developer-toolbar` [state\nimplementation](https://github.com/elastic/kibana/blob/7518eebd9acaf352f02241901cbeb7129e0a1f32/src/platform/packages/shared/kbn-developer-toolbar/src/hooks/use_toolbar_state.tsx#L14-L40).\n- If any of the tours in the queue are skipped, the remaining tours are\nalso skipped (currently only Side Nav tour handles skip behaviour).\nSkipped status is not persisted for now, it is only honored until page\nreload.\n- Side Nav and Security tours were updated to use the new tour queue\nsystem.\n\n### Testing\n\nTour completion (Spaces + SideNav + Security):\n\n\nhttps://github.com/user-attachments/assets/139847ac-48ea-41e6-8eba-cf5c4981ed96\n\nTour skipping (Spaces + SideNav):\n\n\nhttps://github.com/user-attachments/assets/e75f7aa0-6cb4-4c43-9117-172f88f1fe8b","sha":"60952a0bbdeb09c931eaf4469c01f21931836651"}},"sourceBranch":"main","suggestedTargetBranches":["9.2"],"targetPullRequestStates":[{"branch":"main","label":"v9.3.0","branchLabelMappingKey":"^v9.3.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/242640","number":242640,"mergeCommit":{"message":"[SharedUX] Add kbn-tour-queue package to avoid tour overlapping (elastic#242640)\n\nCloses https://github.com/elastic/kibana-team/issues/2146\n\n## Summary\n\n- This PR introduces a new package to orchestrate tours to avoid\noverlapping. Currently, Side Nav Tour and Security tour are overlapping\nin this way:\n\n<img width=\"675\" height=\"391\" alt=\"Screenshot 2025-11-12 at 10 32 34\"\nsrc=\"https://github.com/user-attachments/assets/27a3ba52-2c69-4fe0-bf62-18d649354de5\"\n/>\n\n- Package `kbn-tour-queue` uses `globalThis` to share state across\nplugins and to ensure only one single instance of the state manager is\nused. This approach is based on @Dosant `kbn-developer-toolbar` [state\nimplementation](https://github.com/elastic/kibana/blob/7518eebd9acaf352f02241901cbeb7129e0a1f32/src/platform/packages/shared/kbn-developer-toolbar/src/hooks/use_toolbar_state.tsx#L14-L40).\n- If any of the tours in the queue are skipped, the remaining tours are\nalso skipped (currently only Side Nav tour handles skip behaviour).\nSkipped status is not persisted for now, it is only honored until page\nreload.\n- Side Nav and Security tours were updated to use the new tour queue\nsystem.\n\n### Testing\n\nTour completion (Spaces + SideNav + Security):\n\n\nhttps://github.com/user-attachments/assets/139847ac-48ea-41e6-8eba-cf5c4981ed96\n\nTour skipping (Spaces + SideNav):\n\n\nhttps://github.com/user-attachments/assets/e75f7aa0-6cb4-4c43-9117-172f88f1fe8b","sha":"60952a0bbdeb09c931eaf4469c01f21931836651"}},{"branch":"9.2","label":"v9.2.2","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT-->
1 parent 7a98037 commit a6a668b

File tree

21 files changed

+747
-15
lines changed

21 files changed

+747
-15
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,7 @@ src/platform/packages/shared/kbn-test-jest-helpers @elastic/kibana-operations @e
595595
src/platform/packages/shared/kbn-test-subj-selector @elastic/kibana-operations @elastic/appex-qa
596596
src/platform/packages/shared/kbn-timerange @elastic/obs-onboarding-team
597597
src/platform/packages/shared/kbn-tooling-log @elastic/kibana-operations
598+
src/platform/packages/shared/kbn-tour-queue @elastic/appex-sharedux
598599
src/platform/packages/shared/kbn-traced-es-client @elastic/observability-ui
599600
src/platform/packages/shared/kbn-tracing @elastic/kibana-core @elastic/obs-ai-assistant
600601
src/platform/packages/shared/kbn-tracing-config @elastic/kibana-core

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,6 +1058,7 @@
10581058
"@kbn/timelion-grammar": "link:src/platform/packages/private/kbn-timelion-grammar",
10591059
"@kbn/timerange": "link:src/platform/packages/shared/kbn-timerange",
10601060
"@kbn/tinymath": "link:src/platform/packages/private/kbn-tinymath",
1061+
"@kbn/tour-queue": "link:src/platform/packages/shared/kbn-tour-queue",
10611062
"@kbn/traced-es-client": "link:src/platform/packages/shared/kbn-traced-es-client",
10621063
"@kbn/tracing": "link:src/platform/packages/shared/kbn-tracing",
10631064
"@kbn/tracing-config": "link:src/platform/packages/shared/kbn-tracing-config",

packages/kbn-optimizer/limits.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ pageLoadAssetSize:
106106
ml: 89000
107107
mockIdpPlugin: 7544
108108
monitoring: 28983
109-
navigation: 13598
109+
navigation: 14742
110110
newsfeed: 12371
111111
noDataPage: 1749
112112
observability: 176467
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# @kbn/tour-queue
2+
3+
# Tour Queue
4+
5+
A global queue mechanism for managing sequential tour display across Kibana plugins.
6+
7+
## API
8+
9+
### useTourQueue(tourId: TourId)
10+
11+
Hook that manages tour registration and state.
12+
13+
**Returns:**
14+
- `isActive: boolean` - Whether this tour should be shown
15+
- `onComplete: () => void` - Callback to mark tour as completed
16+
17+
### Tour Object Methods
18+
19+
When you register a tour, you get a tour object with these methods:
20+
21+
- `isActive()` - Check if this tour is currently active
22+
- `complete()` - Mark tour as completed
23+
- `skip()` - Skip all remaining tours for current page load
24+
- `unregister()` - Remove tour from queue (cleanup)
25+
26+
### Tour Order
27+
28+
| Order | Tour ID | Description |
29+
|----------|---------|-------------|
30+
| 1 | `solutionNavigationTour` | Solution navigation tour (Navigation plugin) |
31+
| 2 | `siemMigrationSetupTour` | Security SIEM migration setup (Security plugin) |
32+
33+
**Note:** Lower order = shown first. If a tour is skipped, all remaining tours are skipped for the current page load only. Currently, only the navigation tour implements skip functionality.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { renderHook, act } from '@testing-library/react';
11+
import { useTourQueue } from './use_tour_queue';
12+
import { getTourQueue } from '../state/registry';
13+
import type { TourId } from '..';
14+
15+
describe('useTourQueue', () => {
16+
const REGISTRY_KEY = '__KIBANA_TOUR_QUEUE_CTX__';
17+
18+
const TOUR_1 = 'tour1' as TourId;
19+
const TOUR_2 = 'tour2' as TourId;
20+
21+
beforeEach(() => {
22+
// Clean up the global registry before each test
23+
if (typeof globalThis !== 'undefined') {
24+
delete (globalThis as any)[REGISTRY_KEY];
25+
}
26+
});
27+
28+
it('should return isActive as true for the active tour', () => {
29+
const { result } = renderHook(() => useTourQueue(TOUR_1));
30+
31+
expect(result.current.isActive).toBe(true);
32+
});
33+
34+
it('should return isActive as false for a waiting tour', () => {
35+
renderHook(() => useTourQueue(TOUR_1));
36+
const tour2Hook = renderHook(() => useTourQueue(TOUR_2));
37+
38+
expect(tour2Hook.result.current.isActive).toBe(false);
39+
});
40+
41+
it('should update isActive when the tour becomes active', () => {
42+
const tour1Hook = renderHook(() => useTourQueue(TOUR_1));
43+
const tour2Hook = renderHook(() => useTourQueue(TOUR_2));
44+
45+
expect(tour2Hook.result.current.isActive).toBe(false);
46+
47+
// Complete the first tour
48+
act(() => {
49+
tour1Hook.result.current.onComplete();
50+
});
51+
52+
// Second tour should now be active
53+
expect(tour2Hook.result.current.isActive).toBe(true);
54+
});
55+
56+
it('should update isActive to false when tour is completed', () => {
57+
const { result } = renderHook(() => useTourQueue(TOUR_1));
58+
59+
expect(result.current.isActive).toBe(true);
60+
61+
act(() => {
62+
result.current.onComplete();
63+
});
64+
65+
expect(result.current.isActive).toBe(false);
66+
});
67+
68+
it('should update isActive to false when queue is skipped', () => {
69+
const tour1Hook = renderHook(() => useTourQueue(TOUR_1));
70+
const tour2Hook = renderHook(() => useTourQueue(TOUR_2));
71+
const tourQueue = getTourQueue();
72+
73+
// Initial state
74+
expect(tour1Hook.result.current.isActive).toBe(true);
75+
expect(tour2Hook.result.current.isActive).toBe(false);
76+
77+
// Skip all tours
78+
act(() => {
79+
tourQueue.skipAll();
80+
});
81+
82+
// All tours should be false
83+
expect(tour1Hook.result.current.isActive).toBe(false);
84+
expect(tour2Hook.result.current.isActive).toBe(false);
85+
});
86+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { useCallback, useEffect, useRef, useState } from 'react';
11+
import { getTourQueue } from '../state/registry';
12+
import type { TourId } from '..';
13+
import type { Tour } from '../state/tour_queue_state';
14+
15+
/**
16+
* Result object returned by useTourQueue
17+
* @public
18+
*/
19+
export interface TourQueueResult {
20+
/** Whether this tour is currently active and should be shown */
21+
isActive: boolean;
22+
/** Callback to mark this tour as completed */
23+
onComplete: () => void;
24+
}
25+
26+
/**
27+
* Hook to manage tour queue state for a specific tour.
28+
* Automatically registers the tour, subscribes to queue changes,
29+
* and handles cleanup on unmount.
30+
*
31+
* @param tourId - The ID of the tour. Must be a valid ID from {@link TOURS}
32+
* @returns Object containing isActive state and onComplete callback
33+
* @public
34+
*/
35+
export const useTourQueue = (tourId: TourId): TourQueueResult => {
36+
const tourQueue = getTourQueue();
37+
const [isActive, setIsActive] = useState(false);
38+
const tourRef = useRef<Tour | null>(null);
39+
40+
useEffect(() => {
41+
// Register and get tour object
42+
const tour = tourQueue.register(tourId);
43+
tourRef.current = tour;
44+
// Set initial isActive state
45+
setIsActive(tour.isActive());
46+
// Subscribe to state changes and get cleanup function
47+
const stopListening = tourQueue.subscribe(() => {
48+
setIsActive(tour.isActive());
49+
});
50+
return () => {
51+
tourRef.current?.unregister();
52+
stopListening();
53+
};
54+
}, [tourId, tourQueue]);
55+
56+
const onComplete = useCallback(() => {
57+
tourRef.current?.complete();
58+
}, []);
59+
60+
return {
61+
isActive,
62+
onComplete,
63+
};
64+
};
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
export { useTourQueue } from './hooks/use_tour_queue';
11+
export { getTourQueue } from './state/registry';
12+
export type { TourQueueResult } from './hooks/use_tour_queue';
13+
export type { Tour } from './state/tour_queue_state';
14+
15+
const TOUR_REGISTRY = {
16+
solutionNavigationTour: 1,
17+
siemMigrationSetupTour: 2,
18+
} as const;
19+
20+
/**
21+
* Valid tour IDs for registering tours in the queue.
22+
* Tours are shown in order based on their registry value.
23+
* @public
24+
*/
25+
export const TOURS = {
26+
NAVIGATION: 'solutionNavigationTour',
27+
SECURITY_SIEM_MIGRATION: 'siemMigrationSetupTour',
28+
} as const;
29+
30+
/**
31+
* Union type of all available tour IDs
32+
* @public
33+
*/
34+
export type TourId = (typeof TOURS)[keyof typeof TOURS];
35+
36+
/**
37+
* Get the display order for a tour. Lower numbers are shown first.
38+
* @param tourId - The tour ID
39+
* @returns The numeric order value for the tour
40+
* @internal
41+
*/
42+
export const getOrder = (tourId: TourId): number => {
43+
return TOUR_REGISTRY[tourId];
44+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
module.exports = {
11+
preset: '@kbn/test',
12+
rootDir: '../../../../..',
13+
roots: ['<rootDir>/src/platform/packages/shared/kbn-tour-queue'],
14+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "shared-browser",
3+
"id": "@kbn/tour-queue",
4+
"owner": "@elastic/appex-sharedux",
5+
"group": "platform",
6+
"visibility": "shared"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "@kbn/tour-queue",
3+
"private": true,
4+
"version": "1.0.0",
5+
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0",
6+
"sideEffects": false
7+
}

0 commit comments

Comments
 (0)