-
Notifications
You must be signed in to change notification settings - Fork 894
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
[Discover Next] Dataset Dropdown #7289
Changes from all commits
c67590f
d743e35
82ec7f3
5c78cb7
fa0e33f
7fd59e8
41f0378
d3702fa
939a9cb
0ae2098
4250843
bb956fb
f424f44
5533086
dd27499
a8ffff7
4c2de55
c7a5fab
c4fc2cf
4059d74
b02977a
7d2a15a
a004be6
1562935
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,2 @@ | ||
feat: | ||
- Add DataSet dropdown with index patterns and indices ([#7289](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7289)) |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,253 @@ | ||||||
/* | ||||||
* Copyright OpenSearch Contributors | ||||||
* SPDX-License-Identifier: Apache-2.0 | ||||||
*/ | ||||||
|
||||||
import React, { useEffect, useState } from 'react'; | ||||||
|
||||||
import { EuiButtonEmpty, EuiContextMenu, EuiPopover } from '@elastic/eui'; | ||||||
import { SavedObjectsClientContract, SimpleSavedObject } from 'opensearch-dashboards/public'; | ||||||
import { map, scan } from 'rxjs/operators'; | ||||||
import { ISearchStart } from '../../search/types'; | ||||||
import { IIndexPattern } from '../..'; | ||||||
import { getUiService, getIndexPatterns, getSearchService } from '../../services'; | ||||||
|
||||||
const getClusters = async (savedObjectsClient: SavedObjectsClientContract) => { | ||||||
return await savedObjectsClient.find({ | ||||||
type: 'data-source', | ||||||
perPage: 10000, | ||||||
}); | ||||||
}; | ||||||
|
||||||
export const searchResponseToArray = (showAllIndices: boolean) => (response) => { | ||||||
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. nit. "all" indices might be ambiguous, would it be better with and there's the
Suggested change
|
||||||
const { rawResponse } = response; | ||||||
if (!rawResponse.aggregations) { | ||||||
return []; | ||||||
} else { | ||||||
return rawResponse.aggregations.indices.buckets | ||||||
.map((bucket: { key: string }) => { | ||||||
return bucket.key; | ||||||
}) | ||||||
.filter((indexName: string) => { | ||||||
if (showAllIndices) { | ||||||
return true; | ||||||
} else { | ||||||
return !indexName.startsWith('.'); | ||||||
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. probably doesn't matter but can this filtering be done in the request rather than post processing? |
||||||
} | ||||||
}) | ||||||
.map((indexName: string) => { | ||||||
return { | ||||||
name: indexName, | ||||||
// item: {}, | ||||||
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. is this comment useful? |
||||||
}; | ||||||
}); | ||||||
} | ||||||
}; | ||||||
|
||||||
const buildSearchRequest = (showAllIndices: boolean, pattern: string, dataSourceId?: string) => { | ||||||
const request = { | ||||||
params: { | ||||||
ignoreUnavailable: true, | ||||||
expand_wildcards: showAllIndices ? 'all' : 'open', | ||||||
index: pattern, | ||||||
body: { | ||||||
size: 0, // no hits | ||||||
aggs: { | ||||||
indices: { | ||||||
terms: { | ||||||
field: '_index', | ||||||
size: 100, | ||||||
}, | ||||||
}, | ||||||
}, | ||||||
Comment on lines
+55
to
+62
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. why not just cat indices? |
||||||
}, | ||||||
}, | ||||||
dataSourceId, | ||||||
}; | ||||||
|
||||||
return request; | ||||||
}; | ||||||
|
||||||
const getIndices = async (search: ISearchStart, dataSourceId: string) => { | ||||||
const request = buildSearchRequest(true, '*', dataSourceId); | ||||||
return search | ||||||
.getDefaultSearchInterceptor() | ||||||
.search(request) | ||||||
.pipe(map(searchResponseToArray(true))) | ||||||
.pipe(scan((accumulator = [], value) => accumulator.join(value))) | ||||||
.toPromise() | ||||||
.catch(() => []); | ||||||
}; | ||||||
|
||||||
interface DataSetOption { | ||||||
id: string; | ||||||
name: string; | ||||||
dataSourceRef?: string; | ||||||
} | ||||||
|
||||||
interface DataSetNavigatorProps { | ||||||
savedObjectsClient: SavedObjectsClientContract; | ||||||
indexPatterns: Array<IIndexPattern | string>; | ||||||
} | ||||||
|
||||||
export const DataSetNavigator = ({ savedObjectsClient, indexPatterns }: DataSetNavigatorProps) => { | ||||||
const [isDataSetNavigatorOpen, setIsDataSetNavigatorOpen] = useState(false); | ||||||
const [clusterList, setClusterList] = useState<SimpleSavedObject[]>([]); | ||||||
const [indexList, setIndexList] = useState<DataSetOption[]>([]); | ||||||
const [selectedCluster, setSelectedCluster] = useState<any>(); | ||||||
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. what exactly is a |
||||||
const [selectedDataSet, setSelectedDataSet] = useState<DataSetOption>({ | ||||||
id: indexPatterns[0]?.id, | ||||||
name: indexPatterns[0]?.title, | ||||||
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.
|
||||||
}); | ||||||
const [indexPatternList, setIndexPatternList] = useState<DataSetOption[]>([]); | ||||||
const search = getSearchService(); | ||||||
const uiService = getUiService(); | ||||||
const indexPatternsService = getIndexPatterns(); | ||||||
|
||||||
const onButtonClick = () => setIsDataSetNavigatorOpen((isOpen) => !isOpen); | ||||||
const closePopover = () => setIsDataSetNavigatorOpen(false); | ||||||
|
||||||
const onDataSetClick = async (ds: DataSetOption) => { | ||||||
const existingIndexPattern = indexPatternsService.getByTitle(ds.id, true); | ||||||
const dataSet = await indexPatternsService.create( | ||||||
{ id: ds.id, title: ds.name }, | ||||||
!existingIndexPattern?.id | ||||||
); | ||||||
// save to cache by title because the id is not unique for temporary index pattern created | ||||||
indexPatternsService.saveToCache(dataSet.title, dataSet); | ||||||
uiService.Settings.setSelectedDataSet({ | ||||||
id: dataSet.id, | ||||||
name: dataSet.title, | ||||||
dataSourceRef: selectedCluster?.id ?? undefined, | ||||||
}); | ||||||
setSelectedDataSet(ds); | ||||||
closePopover(); | ||||||
}; | ||||||
|
||||||
useEffect(() => { | ||||||
const subscription = uiService.Settings.getSelectedDataSet$().subscribe((dataSet) => { | ||||||
if (dataSet) { | ||||||
setSelectedDataSet(dataSet); | ||||||
} | ||||||
}); | ||||||
return () => subscription.unsubscribe(); | ||||||
}, [uiService]); | ||||||
|
||||||
// get all index patterns | ||||||
useEffect(() => { | ||||||
indexPatternsService.getIdsWithTitle().then((res) => | ||||||
setIndexPatternList( | ||||||
res.map((indexPattern: { id: string; title: string }) => ({ | ||||||
id: indexPattern.id, | ||||||
name: indexPattern.title, | ||||||
})) | ||||||
) | ||||||
); | ||||||
}, [indexPatternsService]); | ||||||
|
||||||
useEffect(() => { | ||||||
Promise.all([getClusters(savedObjectsClient)]).then((res) => { | ||||||
setClusterList(res.length > 0 ? res?.[0].savedObjects : []); | ||||||
}); | ||||||
}, [savedObjectsClient]); | ||||||
|
||||||
useEffect(() => { | ||||||
if (selectedCluster) { | ||||||
// Get all indexes | ||||||
getIndices(search, selectedCluster.id).then((res) => { | ||||||
setIndexList( | ||||||
res.map((index: { name: string }) => ({ | ||||||
name: index.name, | ||||||
id: index.name, | ||||||
dataSourceRef: selectedCluster.id, | ||||||
})) | ||||||
); | ||||||
}); | ||||||
} | ||||||
}, [search, selectedCluster, setIndexList]); | ||||||
|
||||||
const dataSetButton = ( | ||||||
<EuiButtonEmpty | ||||||
className="dataExplorerDSSelect" | ||||||
color="text" | ||||||
iconType="arrowDown" | ||||||
iconSide="right" | ||||||
onClick={onButtonClick} | ||||||
> | ||||||
{selectedDataSet ? selectedDataSet.name : 'Datasets'} | ||||||
</EuiButtonEmpty> | ||||||
); | ||||||
|
||||||
return ( | ||||||
<EuiPopover | ||||||
button={dataSetButton} | ||||||
isOpen={isDataSetNavigatorOpen} | ||||||
closePopover={closePopover} | ||||||
anchorPosition="downLeft" | ||||||
> | ||||||
<EuiContextMenu | ||||||
initialPanelId={0} | ||||||
className="datasetNavigator" | ||||||
panels={[ | ||||||
{ | ||||||
id: 0, | ||||||
title: 'DATA', | ||||||
items: [ | ||||||
{ | ||||||
name: 'Index Patterns', | ||||||
panel: 1, | ||||||
}, | ||||||
...(clusterList | ||||||
? clusterList.map((cluster) => ({ | ||||||
name: cluster.attributes.title, | ||||||
panel: 2, | ||||||
onClick: () => { | ||||||
setSelectedCluster(cluster); | ||||||
}, | ||||||
})) | ||||||
: []), | ||||||
], | ||||||
}, | ||||||
{ | ||||||
id: 1, | ||||||
title: 'Index Patterns', | ||||||
items: [ | ||||||
...(indexPatternList | ||||||
? indexPatternList.map((indexPattern) => ({ | ||||||
name: indexPattern.name, | ||||||
onClick: () => onDataSetClick(indexPattern), | ||||||
})) | ||||||
: []), | ||||||
], | ||||||
}, | ||||||
{ | ||||||
id: 2, | ||||||
title: selectedCluster ? selectedCluster.attributes.title : 'Cluster', | ||||||
items: [ | ||||||
{ | ||||||
name: 'Indexes', | ||||||
panel: 3, | ||||||
}, | ||||||
], | ||||||
}, | ||||||
{ | ||||||
id: 3, | ||||||
title: selectedCluster ? selectedCluster.attributes.title : 'Cluster', | ||||||
items: [ | ||||||
...(indexList | ||||||
? indexList.map((index) => ({ | ||||||
name: index.name, | ||||||
onClick: () => onDataSetClick(index), | ||||||
})) | ||||||
: []), | ||||||
], | ||||||
}, | ||||||
{ | ||||||
id: 4, | ||||||
title: 'clicked', | ||||||
}, | ||||||
]} | ||||||
/> | ||||||
</EuiPopover> | ||||||
); | ||||||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
.datasetNavigator { | ||
min-width: 350px; | ||
border-bottom: $euiBorderThin !important; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
@import "./language_selector"; | ||
@import "./dataset_navigator"; | ||
@import "./query_editor"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,7 @@ | |
private isEnabled = false; | ||
private enabledQueryEnhancementsUpdated$ = new BehaviorSubject<boolean>(this.isEnabled); | ||
private enhancedAppNames: string[] = []; | ||
private selectedDataSet$ = new BehaviorSubject<any>(null); | ||
|
||
constructor( | ||
private readonly config: ConfigSchema['enhancements'], | ||
|
@@ -38,8 +39,28 @@ | |
this.isEnabled = true; | ||
this.setUserQueryEnhancementsEnabled(this.isEnabled); | ||
this.enhancedAppNames = this.isEnabled ? this.config.supportedAppNames : []; | ||
this.setSelectedDataSet(this.getSelectedDataSet()); | ||
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. what does this do? |
||
} | ||
|
||
/** | ||
* @experimental - Sets the dataset BehaviorSubject | ||
*/ | ||
setSelectedDataSet = (dataSet: any) => { | ||
this.storage.set('opensearchDashboards.userQueryDataSet', dataSet); | ||
this.selectedDataSet$.next(dataSet); | ||
}; | ||
|
||
/** | ||
* @experimental - Gets the dataset Observable | ||
*/ | ||
getSelectedDataSet$ = () => { | ||
return this.selectedDataSet$.asObservable(); | ||
}; | ||
|
||
getSelectedDataSet = () => { | ||
return this.storage.get('opensearchDashboards.userQueryDataSet'); | ||
}; | ||
|
||
supportsEnhancementsEnabled(appName: string) { | ||
return this.enhancedAppNames.includes(appName); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this be
getDataSources
?