From a7e74f6d6dede432d947eb044d4e11c13560f707 Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Thu, 20 Apr 2023 12:11:13 +0200 Subject: [PATCH] DataSourcePicker: Refactor and collapse the DataSourceDropdown components (#66820) * clean up the components and convert to functional components * Create hooks for getting DS * remove focus style override from input --------- Co-authored-by: Ivan Ortega --- .../components/picker/DataSourceDropdown.tsx | 36 +++--- .../components/picker/DataSourceList.tsx | 108 +++++------------- .../components/picker/DataSourceLogo.tsx | 27 ++--- .../components/picker/DataSourceModal.tsx | 3 +- .../components/picker/DataSourcePicker.tsx | 8 +- .../components/picker/DataSourcePickerNG.tsx | 96 ---------------- .../DataSourcePickerWithHistory.test.ts | 27 ----- .../picker/DataSourcePickerWithHistory.tsx | 55 --------- .../datasources/components/picker/types.ts | 40 +------ .../datasources/components/picker/utils.ts | 23 +++- 10 files changed, 83 insertions(+), 340 deletions(-) delete mode 100644 public/app/features/datasources/components/picker/DataSourcePickerNG.tsx delete mode 100644 public/app/features/datasources/components/picker/DataSourcePickerWithHistory.test.ts delete mode 100644 public/app/features/datasources/components/picker/DataSourcePickerWithHistory.tsx diff --git a/public/app/features/datasources/components/picker/DataSourceDropdown.tsx b/public/app/features/datasources/components/picker/DataSourceDropdown.tsx index a74e0d80111..f2f1f91e45d 100644 --- a/public/app/features/datasources/components/picker/DataSourceDropdown.tsx +++ b/public/app/features/datasources/components/picker/DataSourceDropdown.tsx @@ -12,10 +12,10 @@ import { Button, CustomScrollbar, Icon, Input, ModalsController, Portal, useStyl import { DataSourceList } from './DataSourceList'; import { DataSourceLogo, DataSourceLogoPlaceHolder } from './DataSourceLogo'; import { DataSourceModal } from './DataSourceModal'; -import { PickerContentProps, DataSourceDrawerProps } from './types'; -import { dataSourceName as dataSourceLabel } from './utils'; +import { PickerContentProps, DataSourceDropdownProps } from './types'; +import { dataSourceLabel, useGetDatasource } from './utils'; -export function DataSourceDropdown(props: DataSourceDrawerProps) { +export function DataSourceDropdown(props: DataSourceDropdownProps) { const { current, onChange, ...restProps } = props; const [isOpen, setOpen] = useState(false); @@ -23,6 +23,8 @@ export function DataSourceDropdown(props: DataSourceDrawerProps) { const [selectorElement, setSelectorElement] = useState(); const [filterTerm, setFilterTerm] = useState(); + const currentDataSourceInstanceSettings = useGetDatasource(current); + const popper = usePopper(markerElement, selectorElement, { placement: 'bottom-start', }); @@ -51,10 +53,15 @@ export function DataSourceDropdown(props: DataSourceDrawerProps) { {isOpen ? ( : } + prefix={ + filterTerm ? ( + + ) : ( + + ) + } suffix={} - placeholder={dataSourceLabel(current)} - className={styles.input} + placeholder={dataSourceLabel(currentDataSourceInstanceSettings)} onChange={(e) => { setFilterTerm(e.currentTarget.value); }} @@ -73,7 +80,7 @@ export function DataSourceDropdown(props: DataSourceDrawerProps) { onClose={() => { setOpen(false); }} - current={current} + current={currentDataSourceInstanceSettings} style={popper.styles.popper} ref={setSelectorElement} {...restProps} @@ -90,10 +97,10 @@ export function DataSourceDropdown(props: DataSourceDrawerProps) { }} > } + className={styles.input} + prefix={} suffix={} - value={dataSourceLabel(current)} + value={dataSourceLabel(currentDataSourceInstanceSettings)} onFocus={() => { setOpen(true); }} @@ -113,11 +120,6 @@ function getStylesDropdown(theme: GrafanaTheme2) { cursor: pointer; `, input: css` - input:focus { - box-shadow: none; - } - `, - markerInput: css` input { cursor: pointer; } @@ -149,7 +151,7 @@ const PickerContent = React.forwardRef((prop {...props} current={current} onChange={changeCallback} - filter={(ds) => ds.name.includes(filterTerm ?? '')} + filter={(ds) => ds.name.toLowerCase().includes(filterTerm?.toLowerCase() ?? '')} > @@ -169,8 +171,6 @@ const PickerContent = React.forwardRef((prop onClick={() => { onClose(); showModal(DataSourceModal, { - datasources: props.datasources, - recentlyUsed: props.recentlyUsed, enableFileUpload: props.enableFileUpload, fileUploadOptions: props.fileUploadOptions, current, diff --git a/public/app/features/datasources/components/picker/DataSourceList.tsx b/public/app/features/datasources/components/picker/DataSourceList.tsx index 408f8aa4c9c..a199a793c7b 100644 --- a/public/app/features/datasources/components/picker/DataSourceList.tsx +++ b/public/app/features/datasources/components/picker/DataSourceList.tsx @@ -1,10 +1,9 @@ -import React, { PureComponent } from 'react'; +import React from 'react'; import { DataSourceInstanceSettings, DataSourceRef } from '@grafana/data'; -import { getDataSourceSrv } from '@grafana/runtime'; import { DataSourceCard } from './DataSourceCard'; -import { isDataSourceMatch } from './utils'; +import { isDataSourceMatch, useGetDatasources } from './utils'; /** * Component props description for the {@link DataSourceList} @@ -14,7 +13,8 @@ import { isDataSourceMatch } from './utils'; export interface DataSourceListProps { className?: string; onChange: (ds: DataSourceInstanceSettings) => void; - current: DataSourceRef | string | null; // uid + current: DataSourceRef | DataSourceInstanceSettings | string | null | undefined; + /** Would be nicer if these parameters were part of a filtering object */ tracing?: boolean; mixed?: boolean; dashboard?: boolean; @@ -32,88 +32,34 @@ export interface DataSourceListProps { onClear?: () => void; } -/** - * Component state description for the {@link DataSourceList} - * - * @internal - */ -export interface DataSourceListState { - error?: string; -} +export function DataSourceList(props: DataSourceListProps) { + const { className, current, onChange } = props; + // QUESTION: Should we use data from the Redux store as admin DS view does? + const dataSources = useGetDatasources({ + alerting: props.alerting, + annotations: props.annotations, + dashboard: props.dashboard, + logs: props.logs, + metrics: props.metrics, + mixed: props.mixed, + pluginId: props.pluginId, + tracing: props.tracing, + type: props.type, + variables: props.variables, + }); -/** - * Component to be able to select a datasource from the list of installed and enabled - * datasources in the current Grafana instance. - * - * @internal - */ -export class DataSourceList extends PureComponent { - dataSourceSrv = getDataSourceSrv(); - - static defaultProps: Partial = { - filter: () => true, - }; - - state: DataSourceListState = {}; - - constructor(props: DataSourceListProps) { - super(props); - } - - componentDidMount() { - const { current } = this.props; - const dsSettings = this.dataSourceSrv.getInstanceSettings(current); - if (!dsSettings) { - this.setState({ error: 'Could not find data source ' + current }); - } - } - - onChange = (item: DataSourceInstanceSettings) => { - const dsSettings = this.dataSourceSrv.getInstanceSettings(item); - - if (dsSettings) { - this.props.onChange(dsSettings); - this.setState({ error: undefined }); - } - }; - - getDataSourceOptions() { - const { alerting, tracing, metrics, mixed, dashboard, variables, annotations, pluginId, type, filter, logs } = - this.props; - - const options = this.dataSourceSrv.getList({ - alerting, - tracing, - metrics, - logs, - dashboard, - mixed, - variables, - annotations, - pluginId, - filter, - type, - }); - - return options; - } - - render() { - const { className, current } = this.props; - // QUESTION: Should we use data from the Redux store as admin DS view does? - const options = this.getDataSourceOptions(); - - return ( -
- {options.map((ds) => ( + return ( +
+ {dataSources + .filter((ds) => (props.filter ? props.filter(ds) : true)) + .map((ds) => ( onChange(ds)} selected={!!isDataSourceMatch(ds, current)} /> ))} -
- ); - } +
+ ); } diff --git a/public/app/features/datasources/components/picker/DataSourceLogo.tsx b/public/app/features/datasources/components/picker/DataSourceLogo.tsx index cb05adb3c37..55b3857892c 100644 --- a/public/app/features/datasources/components/picker/DataSourceLogo.tsx +++ b/public/app/features/datasources/components/picker/DataSourceLogo.tsx @@ -2,11 +2,10 @@ import { css } from '@emotion/css'; import React from 'react'; import { DataSourceInstanceSettings, DataSourceJsonData, GrafanaTheme2 } from '@grafana/data'; -import { DataSourceRef } from '@grafana/schema'; import { useStyles2 } from '@grafana/ui'; export interface DataSourceLogoProps { - dataSource: DataSourceInstanceSettings | string | DataSourceRef | null | undefined; + dataSource: DataSourceInstanceSettings | undefined; } export function DataSourceLogo(props: DataSourceLogoProps) { @@ -14,24 +13,16 @@ export function DataSourceLogo(props: DataSourceLogoProps) { const styles = useStyles2(getStyles); if (!dataSource) { - return null; + return DataSourceLogoPlaceHolder(); } - if (typeof dataSource === 'string') { - return null; - } - - if ('name' in dataSource) { - return ( - {`${dataSource.meta.name} - ); - } - - return null; + return ( + {`${dataSource.meta.name} + ); } export function DataSourceLogoPlaceHolder() { diff --git a/public/app/features/datasources/components/picker/DataSourceModal.tsx b/public/app/features/datasources/components/picker/DataSourceModal.tsx index 9b8997a82a2..6c0a191923f 100644 --- a/public/app/features/datasources/components/picker/DataSourceModal.tsx +++ b/public/app/features/datasources/components/picker/DataSourceModal.tsx @@ -21,7 +21,6 @@ interface DataSourceModalProps { onChange: (ds: DataSourceInstanceSettings) => void; current: DataSourceRef | string | null | undefined; onDismiss: () => void; - datasources: DataSourceInstanceSettings[]; recentlyUsed?: string[]; enableFileUpload?: boolean; fileUploadOptions?: DropzoneOptions; @@ -62,7 +61,7 @@ export function DataSourceModal({ mixed={false} variables // FIXME: Filter out the grafana data source in a hacky way - filter={(ds) => ds.name.includes(search) && ds.name !== '-- Grafana --'} + filter={(ds) => ds.name.toLowerCase().includes(search.toLowerCase()) && ds.name !== '-- Grafana --'} onChange={onChange} current={current} /> diff --git a/public/app/features/datasources/components/picker/DataSourcePicker.tsx b/public/app/features/datasources/components/picker/DataSourcePicker.tsx index 83677a1fd1e..b67a348a9a9 100644 --- a/public/app/features/datasources/components/picker/DataSourcePicker.tsx +++ b/public/app/features/datasources/components/picker/DataSourcePicker.tsx @@ -6,10 +6,10 @@ import { } from '@grafana/runtime'; import { config } from 'app/core/config'; -import { DataSourcePickerWithHistory } from './DataSourcePickerWithHistory'; -import { DataSourcePickerWithHistoryProps } from './types'; +import { DataSourceDropdown } from './DataSourceDropdown'; +import { DataSourceDropdownProps } from './types'; -type DataSourcePickerProps = DeprecatedDataSourcePickerProps | DataSourcePickerWithHistoryProps; +type DataSourcePickerProps = DeprecatedDataSourcePickerProps | DataSourceDropdownProps; /** * DataSourcePicker is a wrapper around the old DataSourcePicker and the new one. @@ -20,6 +20,6 @@ export function DataSourcePicker(props: DataSourcePickerProps) { return !config.featureToggles.advancedDataSourcePicker ? ( ) : ( - + ); } diff --git a/public/app/features/datasources/components/picker/DataSourcePickerNG.tsx b/public/app/features/datasources/components/picker/DataSourcePickerNG.tsx deleted file mode 100644 index 99aa7d3879c..00000000000 --- a/public/app/features/datasources/components/picker/DataSourcePickerNG.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import React, { PureComponent } from 'react'; - -// Components - -import { DataSourceInstanceSettings, DataSourceRef, getDataSourceUID } from '@grafana/data'; -import { getDataSourceSrv } from '@grafana/runtime'; -import { DataSourceJsonData } from '@grafana/schema'; - -import { DataSourceDropdown } from './DataSourceDropdown'; -import { DataSourcePickerProps } from './types'; - -/** - * Component state description for the {@link DataSourcePicker} - * - * @internal - */ -export interface DataSourcePickerState { - error?: string; -} - -/** - * Component to be able to select a datasource from the list of installed and enabled - * datasources in the current Grafana instance. - * - * @internal - */ -export class DataSourcePicker extends PureComponent { - dataSourceSrv = getDataSourceSrv(); - - state: DataSourcePickerState = {}; - - componentDidMount() { - const { current } = this.props; - const dsSettings = this.dataSourceSrv.getInstanceSettings(current); - if (!dsSettings) { - this.setState({ error: 'Could not find data source ' + current }); - } - } - - onChange = (ds: DataSourceInstanceSettings) => { - this.props.onChange(ds); - this.setState({ error: undefined }); - }; - - private getCurrentDs(): DataSourceInstanceSettings | string | DataSourceRef | null | undefined { - const { current, noDefault } = this.props; - if (!current && noDefault) { - return; - } - - const ds = this.dataSourceSrv.getInstanceSettings(current); - if (ds) { - return ds; - } - - return getDataSourceUID(current); - } - - getDatasources() { - const { alerting, tracing, metrics, mixed, dashboard, variables, annotations, pluginId, type, filter, logs } = - this.props; - - return this.dataSourceSrv.getList({ - alerting, - tracing, - metrics, - logs, - dashboard, - mixed, - variables, - annotations, - pluginId, - filter, - type, - }); - } - - render() { - const { recentlyUsed, fileUploadOptions, enableFileUpload, onClickAddCSV } = this.props; - - return ( -
- -
- ); - } -} diff --git a/public/app/features/datasources/components/picker/DataSourcePickerWithHistory.test.ts b/public/app/features/datasources/components/picker/DataSourcePickerWithHistory.test.ts deleted file mode 100644 index 752deb96927..00000000000 --- a/public/app/features/datasources/components/picker/DataSourcePickerWithHistory.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { updateHistory } from './DataSourcePickerWithHistory'; - -describe('DataSourcePickerWithHistory', () => { - describe('updateHistory', () => { - const early = { uid: 'b', lastUse: '2023-02-27T13:39:08.318Z' }; - const later = { uid: 'a', lastUse: '2023-02-28T13:39:08.318Z' }; - - it('should add an item to the history', () => { - expect(updateHistory([], early)).toEqual([early]); - }); - - it('should sort later entries first', () => { - expect(updateHistory([early], later)).toEqual([later, early]); - }); - - it('should update an already existing history item with the new lastUsed date', () => { - const laterB = { uid: early.uid, lastUse: later.lastUse }; - expect(updateHistory([early], laterB)).toEqual([laterB]); - }); - - it('should keep the three latest items in history', () => { - const evenLater = { uid: 'c', lastUse: '2023-03-01T13:39:08.318Z' }; - const latest = { uid: 'd', lastUse: '2023-03-02T13:39:08.318Z' }; - expect(updateHistory([early, later, evenLater], latest)).toEqual([latest, evenLater, later]); - }); - }); -}); diff --git a/public/app/features/datasources/components/picker/DataSourcePickerWithHistory.tsx b/public/app/features/datasources/components/picker/DataSourcePickerWithHistory.tsx deleted file mode 100644 index b9ea0335415..00000000000 --- a/public/app/features/datasources/components/picker/DataSourcePickerWithHistory.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; - -import { dateTime } from '@grafana/data'; -import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider'; - -import { DataSourcePicker } from './DataSourcePickerNG'; -import { DataSourcePickerHistoryItem, DataSourcePickerWithHistoryProps } from './types'; - -const DS_PICKER_STORAGE_KEY = 'DATASOURCE_PICKER'; - -export const DataSourcePickerWithHistory = (props: DataSourcePickerWithHistoryProps) => { - return ( - - defaultValue={[]} - storageKey={props.localStorageKey ?? DS_PICKER_STORAGE_KEY} - > - {(rawValues, onSaveToStore) => { - return ( - dsi.uid)} //Filter recently to have a time cutoff - onChange={(ds) => { - onSaveToStore(updateHistory(rawValues, { uid: ds.uid, lastUse: dateTime(new Date()).toISOString() })); - props.onChange(ds); - }} - > - ); - }} - - ); -}; - -export function updateHistory(values: DataSourcePickerHistoryItem[], newValue: DataSourcePickerHistoryItem) { - const newHistory = values; - const existingIndex = newHistory.findIndex((dpi) => dpi.uid === newValue.uid); - if (existingIndex !== -1) { - newHistory[existingIndex] = newValue; - } else { - newHistory.push(newValue); - } - - newHistory.sort((a, b) => { - const al = dateTime(a.lastUse); - const bl = dateTime(b.lastUse); - if (al.isBefore(bl)) { - return 1; - } else if (bl.isBefore(al)) { - return -1; - } else { - return 0; - } - }); - - return newHistory.slice(0, 3); -} diff --git a/public/app/features/datasources/components/picker/types.ts b/public/app/features/datasources/components/picker/types.ts index b6e3101c261..4dc7dd36a4e 100644 --- a/public/app/features/datasources/components/picker/types.ts +++ b/public/app/features/datasources/components/picker/types.ts @@ -4,8 +4,7 @@ import { DropzoneOptions } from 'react-dropzone'; import { DataSourceInstanceSettings } from '@grafana/data'; import { DataSourceJsonData, DataSourceRef } from '@grafana/schema'; -export interface DataSourceDrawerProps { - datasources: Array>; +export interface DataSourceDropdownProps { onChange: (ds: DataSourceInstanceSettings) => void; current: DataSourceInstanceSettings | string | DataSourceRef | null | undefined; enableFileUpload?: boolean; @@ -14,44 +13,9 @@ export interface DataSourceDrawerProps { recentlyUsed?: string[]; } -export interface PickerContentProps extends DataSourceDrawerProps { +export interface PickerContentProps extends DataSourceDropdownProps { style: React.CSSProperties; filterTerm?: string; onClose: () => void; onDismiss: () => void; } - -export interface DataSourcePickerProps { - onChange: (ds: DataSourceInstanceSettings) => void; - current: DataSourceRef | string | null; // uid - tracing?: boolean; - recentlyUsed?: string[]; - mixed?: boolean; - dashboard?: boolean; - metrics?: boolean; - type?: string | string[]; - annotations?: boolean; - variables?: boolean; - alerting?: boolean; - pluginId?: string; - /** If true,we show only DSs with logs; and if true, pluginId shouldnt be passed in */ - logs?: boolean; - // Does not set the default data source if there is no value. - noDefault?: boolean; - inputId?: string; - filter?: (dataSource: DataSourceInstanceSettings) => boolean; - onClear?: () => void; - disabled?: boolean; - enableFileUpload?: boolean; - fileUploadOptions?: DropzoneOptions; - onClickAddCSV?: () => void; -} - -export interface DataSourcePickerWithHistoryProps extends Omit { - localStorageKey?: string; -} - -export interface DataSourcePickerHistoryItem { - lastUse: string; - uid: string; -} diff --git a/public/app/features/datasources/components/picker/utils.ts b/public/app/features/datasources/components/picker/utils.ts index 257dca80a84..61b7abeb5c7 100644 --- a/public/app/features/datasources/components/picker/utils.ts +++ b/public/app/features/datasources/components/picker/utils.ts @@ -1,4 +1,5 @@ import { DataSourceInstanceSettings, DataSourceJsonData, DataSourceRef } from '@grafana/data'; +import { GetDataSourceListFilters, getDataSourceSrv } from '@grafana/runtime'; export function isDataSourceMatch( ds: DataSourceInstanceSettings | undefined, @@ -16,7 +17,7 @@ export function isDataSourceMatch( return ds.uid === current.uid; } -export function dataSourceName( +export function dataSourceLabel( dataSource: DataSourceInstanceSettings | string | DataSourceRef | null | undefined ) { if (!dataSource) { @@ -37,3 +38,23 @@ export function dataSourceName( return 'Unknown'; } + +export function useGetDatasources(filters: GetDataSourceListFilters) { + const dataSourceSrv = getDataSourceSrv(); + + return dataSourceSrv.getList(filters); +} + +export function useGetDatasource(dataSource: string | DataSourceRef | DataSourceInstanceSettings | null | undefined) { + const dataSourceSrv = getDataSourceSrv(); + + if (!dataSource) { + return undefined; + } + + if (typeof dataSource === 'string') { + return dataSourceSrv.getInstanceSettings(dataSource); + } + + return dataSourceSrv.getInstanceSettings(dataSource); +}