Skip to content

Commit 42ae376

Browse files
[explore]Add resultsActionBar slot to explore plugin (#10842)
* Add resultsActionBar slot to explore plugin Signed-off-by: Lin Wang <[email protected]> * Changeset file for PR #10842 created/updated --------- Signed-off-by: Lin Wang <[email protected]> Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
1 parent e22b1c3 commit 42ae376

File tree

10 files changed

+327
-5
lines changed

10 files changed

+327
-5
lines changed

changelogs/fragments/10842.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
feat:
2+
- Add resultsActionBar slot to explore plugin ([#10842](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10842))

src/plugins/explore/public/build_services.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { TabRegistryService } from './services/tab_registry/tab_registry_service
1616
import { VisualizationRegistryService } from './services/visualization_registry_service';
1717
import { AppStore } from './application/utils/state_management/store';
1818
import { QueryPanelActionsRegistryService } from './services/query_panel_actions_registry';
19+
import { SlotRegistryService } from './services/slot_registry';
1920

2021
export function buildServices(
2122
core: CoreStart,
@@ -24,7 +25,8 @@ export function buildServices(
2425
tabRegistry: TabRegistryService,
2526
visualizationRegistry: VisualizationRegistryService,
2627
queryPanelActionsRegistry: QueryPanelActionsRegistryService,
27-
isDatasetManagementEnabled: boolean
28+
isDatasetManagementEnabled: boolean,
29+
slotRegistry: SlotRegistryService
2830
): ExploreServices {
2931
const config = context.config.get<ConfigSchema>();
3032
const supportedTypes = config.supportedTypes;
@@ -91,6 +93,7 @@ export function buildServices(
9193
tabRegistry,
9294
visualizationRegistry,
9395
queryPanelActionsRegistry,
96+
slotRegistry,
9497
expressions: plugins.expressions,
9598

9699
dashboard: plugins.dashboard,

src/plugins/explore/public/components/tabs/action_bar/action_bar.test.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ jest.mock('../../../../../opensearch_dashboards_react/public', () => ({
4545
},
4646
},
4747
},
48+
slotRegistry: {
49+
getSortedItems$: () => ({
50+
subscribe: jest.fn(() => ({ unsubscribe: jest.fn() })),
51+
}),
52+
},
4853
},
4954
}),
5055
withOpenSearchDashboards: jest.fn((component: any) => component),

src/plugins/explore/public/components/tabs/action_bar/action_bar.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
* Copyright OpenSearch Contributors
33
* SPDX-License-Identifier: Apache-2.0
44
*/
5-
import React, { memo } from 'react';
5+
import React, { memo, useMemo } from 'react';
6+
import { useObservable } from 'react-use';
67
import { DiscoverResultsActionBar } from './results_action_bar/results_action_bar';
78
import { ExploreServices } from '../../../types';
89
import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public';
@@ -22,9 +23,14 @@ const ActionBarComponent = () => {
2223
const { dataset } = useDatasetContext();
2324
const { results } = useTabResults();
2425
const { results: histogramResults } = useHistogramResults();
25-
const { core, inspector, inspectorAdapters } = services;
26+
const { core, inspector, inspectorAdapters, slotRegistry } = services;
2627
const savedSearch = useSelector(selectSavedSearch);
2728

29+
const sortedSlotItems$ = useMemo(() => {
30+
return slotRegistry.getSortedItems$('resultsActionBar');
31+
}, [slotRegistry]);
32+
const slotItems = useObservable(sortedSlotItems$, []);
33+
2834
const openInspector = () => {
2935
if (inspector) {
3036
inspector.open(inspectorAdapters, {
@@ -50,6 +56,7 @@ const ActionBarComponent = () => {
5056
elapsedMs={elapsedMs}
5157
dataset={dataset}
5258
inspectionHanlder={openInspector}
59+
extraActions={slotItems}
5360
/>
5461
);
5562
};

src/plugins/explore/public/components/tabs/action_bar/results_action_bar/results_action_bar.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { SaveAndAddButtonWithModal } from '../../../visualizations/add_to_dashbo
1818
import { selectActiveTabId } from '../../../../application/utils/state_management/selectors';
1919
import { PatternsSettingsPopoverButton } from '../patterns_settings/patterns_settings_popover_button';
2020
import { getVisualizationBuilder } from '../../../visualizations/visualization_builder';
21+
import { SlotItemsForType } from '../../../../services/slot_registry';
2122

2223
export interface DiscoverResultsActionBarProps {
2324
hits?: number;
@@ -27,6 +28,7 @@ export interface DiscoverResultsActionBarProps {
2728
elapsedMs?: number;
2829
dataset?: Dataset;
2930
inspectionHanlder?: () => void;
31+
extraActions?: Array<SlotItemsForType<'resultsActionBar'>>;
3032
}
3133

3234
export const DiscoverResultsActionBar = ({
@@ -37,6 +39,7 @@ export const DiscoverResultsActionBar = ({
3739
elapsedMs,
3840
dataset,
3941
inspectionHanlder,
42+
extraActions,
4043
}: DiscoverResultsActionBarProps) => {
4144
const currentTab = useSelector(selectActiveTabId);
4245
const shouldShowAddToDashboardButton = currentTab !== 'explore_patterns_tab';
@@ -127,6 +130,11 @@ export const DiscoverResultsActionBar = ({
127130
<SaveAndAddButtonWithModal dataset={dataset} />
128131
</EuiFlexItem>
129132
)}
133+
{extraActions?.map((item) => (
134+
<EuiFlexItem grow={false} key={item.id}>
135+
{item.render()}
136+
</EuiFlexItem>
137+
))}
130138
</EuiFlexGroup>
131139
) : null}
132140
</EuiFlexItem>

src/plugins/explore/public/plugin.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import { createAbortDataQueryAction } from './application/utils/state_management
7070
import { ABORT_DATA_QUERY_TRIGGER } from '../../ui_actions/public';
7171
import { abortAllActiveQueries } from './application/utils/state_management/actions/query_actions';
7272
import { setServices } from './services/services';
73+
import { SlotRegistryService } from './services/slot_registry';
7374

7475
// Log Actions
7576
import { logActionRegistry } from './services/log_action_registry';
@@ -103,6 +104,7 @@ export class ExplorePlugin
103104
private tabRegistry: TabRegistryService = new TabRegistryService();
104105
private visualizationRegistryService = new VisualizationRegistryService();
105106
private queryPanelActionsRegistryService = new QueryPanelActionsRegistryService();
107+
private slotRegistryService = new SlotRegistryService();
106108

107109
constructor(private readonly initializerContext: PluginInitializerContext) {
108110
this.config = initializerContext.config.get<ConfigSchema>();
@@ -315,7 +317,8 @@ export class ExplorePlugin
315317
this.tabRegistry,
316318
this.visualizationRegistryService,
317319
this.queryPanelActionsRegistryService,
318-
this.isDatasetManagementEnabled
320+
this.isDatasetManagementEnabled,
321+
this.slotRegistryService
319322
);
320323

321324
// Add osdUrlStateStorage to services (like VisBuilder and DataExplorer)
@@ -481,7 +484,8 @@ export class ExplorePlugin
481484
this.tabRegistry,
482485
this.visualizationRegistryService,
483486
this.queryPanelActionsRegistryService,
484-
this.isDatasetManagementEnabled
487+
this.isDatasetManagementEnabled,
488+
this.slotRegistryService
485489
);
486490
setLegacyServices({
487491
...services,
@@ -514,6 +518,7 @@ export class ExplorePlugin
514518
savedSearchLoader: savedExploreLoader, // For backward compatibility
515519
savedExploreLoader,
516520
visualizationRegistry: this.visualizationRegistryService.start(),
521+
slotRegistry: this.slotRegistryService.start(),
517522
};
518523
}
519524

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
export {
7+
SlotRegistryService,
8+
SlotRegistryServiceStart,
9+
SlotItemConfig,
10+
SlotTypeDefinitions,
11+
SlotItemsForType,
12+
} from './slot_registry_service';
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { SlotRegistryService, SlotItemConfig } from './slot_registry_service';
7+
import { take } from 'rxjs/operators';
8+
9+
describe('SlotRegistryService', () => {
10+
let service: SlotRegistryService;
11+
12+
beforeEach(() => {
13+
service = new SlotRegistryService();
14+
});
15+
16+
it('registers and retrieves slot items', (done) => {
17+
const start = service.start();
18+
const slotItems: Array<SlotItemConfig<'resultsActionBar'>> = [
19+
{
20+
id: 'test-action-bar-1',
21+
order: 1,
22+
slotType: 'resultsActionBar',
23+
render: () => null as any,
24+
},
25+
{
26+
id: 'test-action-bar-2',
27+
order: 2,
28+
slotType: 'resultsActionBar',
29+
render: () => null as any,
30+
},
31+
];
32+
33+
start.register(slotItems);
34+
35+
service
36+
.getAllItems$()
37+
.pipe(take(1))
38+
.subscribe((items) => {
39+
expect(items).toHaveLength(2);
40+
expect(items[0]).toEqual(slotItems[0]);
41+
expect(items[1]).toEqual(slotItems[1]);
42+
done();
43+
});
44+
});
45+
46+
it('throws error when registering duplicate slot item id', () => {
47+
const start = service.start();
48+
const slotItem: SlotItemConfig<'resultsActionBar'> = {
49+
id: 'duplicate-id',
50+
order: 1,
51+
slotType: 'resultsActionBar',
52+
render: () => null as any,
53+
};
54+
55+
start.register(slotItem);
56+
57+
expect(() => {
58+
start.register(slotItem);
59+
}).toThrow('Slot item with id "duplicate-id" is already registered');
60+
});
61+
62+
it('filters and sorts items by slot type', (done) => {
63+
const start = service.start();
64+
const actionBarItem: SlotItemConfig<'resultsActionBar'> = {
65+
id: 'test-action-bar',
66+
order: 20,
67+
slotType: 'resultsActionBar',
68+
render: () => null as any,
69+
};
70+
const anotherActionBarItem: SlotItemConfig<'resultsActionBar'> = {
71+
id: 'another-action-bar',
72+
order: 10,
73+
slotType: 'resultsActionBar',
74+
render: () => null as any,
75+
};
76+
77+
start.register([actionBarItem, anotherActionBarItem]);
78+
79+
service
80+
.getSortedItems$('resultsActionBar')
81+
.pipe(take(1))
82+
.subscribe((items) => {
83+
expect(items).toHaveLength(2);
84+
expect(items[0].id).toBe('another-action-bar');
85+
expect(items[1].id).toBe('test-action-bar');
86+
done();
87+
});
88+
});
89+
90+
it('handles negative order values correctly', (done) => {
91+
const start = service.start();
92+
const items: Array<SlotItemConfig<'resultsActionBar'>> = [
93+
{
94+
id: 'item-1',
95+
order: 10,
96+
slotType: 'resultsActionBar',
97+
render: () => null as any,
98+
},
99+
{
100+
id: 'item-2',
101+
order: -5,
102+
slotType: 'resultsActionBar',
103+
render: () => null as any,
104+
},
105+
{
106+
id: 'item-3',
107+
order: 0,
108+
slotType: 'resultsActionBar',
109+
render: () => null as any,
110+
},
111+
];
112+
113+
start.register(items);
114+
115+
service
116+
.getSortedItems$('resultsActionBar')
117+
.pipe(take(1))
118+
.subscribe((sortedItems) => {
119+
expect(sortedItems).toHaveLength(3);
120+
expect(sortedItems[0].id).toBe('item-2');
121+
expect(sortedItems[1].id).toBe('item-3');
122+
expect(sortedItems[2].id).toBe('item-1');
123+
done();
124+
});
125+
});
126+
127+
it('emits updates when new items are registered', (done) => {
128+
const start = service.start();
129+
const emissions: Array<Array<SlotItemConfig<'resultsActionBar'>>> = [];
130+
131+
const subscription = service.getSortedItems$('resultsActionBar').subscribe((items) => {
132+
emissions.push(items);
133+
134+
if (emissions.length === 3) {
135+
expect(emissions[0]).toHaveLength(0);
136+
expect(emissions[1]).toHaveLength(1);
137+
expect(emissions[2]).toHaveLength(2);
138+
subscription.unsubscribe();
139+
done();
140+
}
141+
});
142+
143+
setTimeout(() => {
144+
start.register({
145+
id: 'item-1',
146+
order: 1,
147+
slotType: 'resultsActionBar',
148+
render: () => null as any,
149+
});
150+
}, 10);
151+
152+
setTimeout(() => {
153+
start.register({
154+
id: 'item-2',
155+
order: 2,
156+
slotType: 'resultsActionBar',
157+
render: () => null as any,
158+
});
159+
}, 20);
160+
});
161+
});

0 commit comments

Comments
 (0)