Skip to content

Commit

Permalink
Adds the data source selector and a useIndexPatterns hook (#1763)
Browse files Browse the repository at this point in the history
Signed-off-by: Brooke Green <cptn@amazon.com>
  • Loading branch information
CPTNB committed Jun 23, 2022
1 parent 3a01279 commit 98e160e
Show file tree
Hide file tree
Showing 12 changed files with 387 additions and 46 deletions.
4 changes: 3 additions & 1 deletion src/plugins/wizard/public/application/_variables.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@import '@elastic/eui/src/global_styling/variables/header';
@import '@elastic/eui/src/global_styling/variables/form';

$osdHeaderOffset: $euiHeaderHeightCompensation * 2;
$osdHeaderOffset: $euiHeaderHeightCompensation * 2;
$wizSideNavWidth: 470px;
2 changes: 1 addition & 1 deletion src/plugins/wizard/public/application/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
padding: 0;
display: grid;
grid-template-rows: min-content 1fr;
grid-template-columns: 470px 1fr;
grid-template-columns: $wizSideNavWidth 1fr;
grid-template-areas:
"topNav topNav"
"sideNav workspace"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { i18n } from '@osd/i18n';
import { EuiIcon } from '@elastic/eui';
import { SearchableDropdown, SearchableDropdownOption } from './searchable_dropdown';
import { useIndexPatterns } from '../utils/use';
import indexPatternSvg from '../../assets/index_pattern.svg';
import { useTypedDispatch } from '../utils/state_management';
import { setIndexPattern } from '../utils/state_management/visualization_slice';
import { IndexPattern } from '../../../../data/public';

function indexPatternEquality(A?: SearchableDropdownOption, B?: SearchableDropdownOption): boolean {
return !A || !B ? false : A.id === B.id;
}

function toSearchableDropdownOption(indexPattern: IndexPattern): SearchableDropdownOption {
return {
id: indexPattern.id || '',
label: indexPattern.title,
searchableLabel: indexPattern.title,
prepend: <EuiIcon type={indexPatternSvg} />,
};
}

export const DataSourceSelect = () => {
const { indexPatterns, loading, error, selected } = useIndexPatterns();
const dispatch = useTypedDispatch();

return (
<SearchableDropdown
selected={selected !== undefined ? toSearchableDropdownOption(selected) : undefined}
onChange={(option) => {
const foundOption = indexPatterns.filter((s) => s.id === option.id)[0];
if (foundOption !== undefined && typeof foundOption.id === 'string') {
dispatch(setIndexPattern(foundOption.id));
}
}}
prepend={i18n.translate('wizard.nav.dataSource.selector.title', {
defaultMessage: 'Data Source',
})}
error={error}
loading={loading}
options={indexPatterns.map(toSearchableDropdownOption)}
equality={indexPatternEquality}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@import "../variables";

.searchableDropdown {
overflow: "hidden";
}

.searchableDropdown .euiPopover,
.searchableDropdown .euiPopover__anchor {
width: 100%;
}

.searchableDropdown--fixedWidthChild {
width: calc(#{$wizSideNavWidth} - #{$euiSizeXL} * 2) ;
}

.searchableDropdown--topDisplay {
padding-right: $euiSizeL;
}


.searchableDropdown--selectableWrapper .euiSelectableList {
// When clicking on the selectable content it will "highlight" itself with a box shadow
// This turns that off
box-shadow: none !important;
margin: ($euiFormControlPadding * -1) - 4;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useState, useEffect } from 'react';
import {
EuiLoadingSpinner,
EuiFormControlLayout,
EuiPopoverTitle,
EuiButtonEmpty,
EuiPopover,
EuiSelectable,
EuiTextColor,
} from '@elastic/eui';
import './searchable_dropdown.scss';

export interface SearchableDropdownOption {
id: string;
label: string;
searchableLabel: string;
prepend: any;
}

interface SearchableDropdownProps {
selected?: SearchableDropdownOption;
onChange: (selection) => void;
options: SearchableDropdownOption[];
loading: boolean;
error?: Error;
prepend: string;
// not just the first time!
onOpen?: () => void;
equality: (A, B) => boolean;
}

type DisplayError = any;

function displayError(error: DisplayError) {
return typeof error === 'object' ? error.toString() : <>{error}</>;
}

export const SearchableDropdown = ({
onChange,
equality,
selected,
options,
error,
loading,
prepend,
onOpen,
}: SearchableDropdownProps) => {
const [localOptions, setLocalOptions] = useState<any[] | undefined>(undefined);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const onButtonClick = () => {
if (!isPopoverOpen && typeof onOpen === 'function') {
onOpen();
}
setIsPopoverOpen(!isPopoverOpen);
};
const closePopover = () => setIsPopoverOpen(false);

function selectNewOption(newOptions) {
// alright, the EUI Selectable is pretty ratchet
// this is as smarmy as it is because it needs to be

// first go through and count all the "checked" options
const selectedCount = newOptions.filter((o) => o.checked === 'on').length;

// if the count is 0, the user just "unchecked" our selection and we can just do nothing
if (selectedCount === 0) {
setIsPopoverOpen(false);
return;
}

// then, if there's more than two selections, the Selectable left the previous selection as "checked"
// so we need to go and "uncheck" it
for (let i = 0; i < newOptions.length; i++) {
if (equality(newOptions[i], selected) && selectedCount > 1) {
delete newOptions[i].checked;
}
}

// finally, we can pick the checked option as the actual selection
const newSelection = newOptions.filter((o) => o.checked === 'on')[0];

setLocalOptions(newOptions);
setIsPopoverOpen(false);
onChange(newSelection);
}

useEffect(() => {
setLocalOptions(
options.map((o) => ({
...o,
checked: equality(o, selected) ? 'on' : undefined,
}))
);
}, [selected, options, equality]);

const listDisplay = (list, search) =>
loading ? (
<div style={{ textAlign: 'center' }}>
<EuiLoadingSpinner />
</div>
) : error !== undefined ? (
displayError(error)
) : (
<>
<EuiPopoverTitle paddingSize="s" className="wizPopoverTitle">
{search}
</EuiPopoverTitle>
{list}
</>
);

const selectable = (
<div className="searchableDropdown--selectableWrapper">
<EuiSelectable
aria-label="Selectable options"
searchable
options={localOptions}
onChange={selectNewOption}
listProps={{
showIcons: false,
}}
>
{listDisplay}
</EuiSelectable>
</div>
);

const selectedText =
selected === undefined ? (
<EuiTextColor color="subdued">{loading ? 'Loading' : 'Select an option'}</EuiTextColor>
) : (
<>
{selected.prepend} {selected.label}
</>
);

const selectedView = (
<EuiButtonEmpty
color="text"
style={{ textAlign: 'left' }}
className="searchableDropdown--topDisplay"
onClick={onButtonClick}
>
{selectedText}
</EuiButtonEmpty>
);

const formControl = <EuiFormControlLayout
title={selected === undefined ? "Select an option" : selected.label}
isLoading={loading}
fullWidth={true}
style={{ cursor: 'pointer' }}
prepend={prepend}
icon={{ type: 'arrowDown', side: 'right' }}
readOnly={true}
>{selectedView}</EuiFormControlLayout>

return (
<div className="searchableDropdown">
<EuiPopover button={formControl} isOpen={isPopoverOpen} closePopover={closePopover}>
<div className="searchableDropdown--fixedWidthChild">{selectable}</div>
</EuiPopover>
</div>
);
};
10 changes: 6 additions & 4 deletions src/plugins/wizard/public/application/components/side_nav.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@import "../util";
@import "../variables";

.wizSidenav {
@include scrollNavParent(auto 1fr);
Expand All @@ -7,10 +8,6 @@
border-right: $euiBorderThin;
}

.wizDatasourceSelector {
padding: $euiSize $euiSize 0 $euiSize;
}

.wizSidenavTabs {
.euiTab__content {
text-transform: capitalize;
Expand All @@ -22,3 +19,8 @@
@include scrollNavParent;
}
}

.wizDatasourceSelect {
max-width: $wizSideNavWidth;
padding: $euiSize $euiSize 0 $euiSize;
}
44 changes: 6 additions & 38 deletions src/plugins/wizard/public/application/components/side_nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,22 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { i18n } from '@osd/i18n';
import { EuiFormLabel, EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui';
import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public';
import { WizardServices } from '../../types';
import React, { ReactElement } from 'react';
import { EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui';
import './side_nav.scss';
import { useTypedDispatch, useTypedSelector } from '../utils/state_management';
import { setIndexPattern } from '../utils/state_management/visualization_slice';
import { useVisualizationType } from '../utils/use';
import { DataSourceSelect } from './data_source_select';
import { DataTab } from '../contributions';
import { StyleTabConfig } from '../../services/type_service';

export const SideNav = () => {
const {
services: {
data,
savedObjects: { client: savedObjectsClient },
},
} = useOpenSearchDashboards<WizardServices>();
const { IndexPatternSelect } = data.ui;
const { indexPattern: indexPatternId } = useTypedSelector((state) => state.visualization);
const dispatch = useTypedDispatch();
const {
ui: { containerConfig },
} = useVisualizationType();

const tabs: EuiTabbedContentTab[] = Object.entries(containerConfig).map(
([containerName, config]) => {
let content = null;
let content: null | ReactElement = null;
switch (containerName) {
case 'data':
content = <DataTab key="containerName" />;
Expand All @@ -52,27 +39,8 @@ export const SideNav = () => {

return (
<section className="wizSidenav">
<div className="wizDatasourceSelector">
<EuiFormLabel>
{i18n.translate('wizard.nav.dataSource.selector.title', {
defaultMessage: 'Index Pattern',
})}
</EuiFormLabel>
<IndexPatternSelect
savedObjectsClient={savedObjectsClient}
placeholder={i18n.translate('wizard.nav.dataSource.selector.placeholder', {
defaultMessage: 'Select index pattern',
})}
indexPatternId={indexPatternId || ''}
onChange={async (newIndexPatternId: any) => {
const newIndexPattern = await data.indexPatterns.get(newIndexPatternId);

if (newIndexPattern) {
dispatch(setIndexPattern(newIndexPatternId));
}
}}
isClearable={false}
/>
<div className="wizDatasourceSelect">
<DataSourceSelect />
</div>
<EuiTabbedContent tabs={tabs} className="wizSidenavTabs" />
</section>
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/wizard/public/application/utils/use/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
*/

export { useVisualizationType } from './use_visualization_type';
export { useIndexPattern } from './use_index_pattern';
export { useIndexPattern, useIndexPatterns } from './use_index_pattern';
Loading

0 comments on commit 98e160e

Please sign in to comment.