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 <ivanortegaalba@gmail.com>
pull/66977/head
Oscar Kilhed 2 years ago committed by GitHub
parent 9ff221098d
commit a7e74f6d6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 36
      public/app/features/datasources/components/picker/DataSourceDropdown.tsx
  2. 98
      public/app/features/datasources/components/picker/DataSourceList.tsx
  3. 13
      public/app/features/datasources/components/picker/DataSourceLogo.tsx
  4. 3
      public/app/features/datasources/components/picker/DataSourceModal.tsx
  5. 8
      public/app/features/datasources/components/picker/DataSourcePicker.tsx
  6. 96
      public/app/features/datasources/components/picker/DataSourcePickerNG.tsx
  7. 27
      public/app/features/datasources/components/picker/DataSourcePickerWithHistory.test.ts
  8. 55
      public/app/features/datasources/components/picker/DataSourcePickerWithHistory.tsx
  9. 40
      public/app/features/datasources/components/picker/types.ts
  10. 23
      public/app/features/datasources/components/picker/utils.ts

@ -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<HTMLDivElement | null>();
const [filterTerm, setFilterTerm] = useState<string>();
const currentDataSourceInstanceSettings = useGetDatasource(current);
const popper = usePopper(markerElement, selectorElement, {
placement: 'bottom-start',
});
@ -51,10 +53,15 @@ export function DataSourceDropdown(props: DataSourceDrawerProps) {
{isOpen ? (
<FocusScope contain autoFocus restoreFocus>
<Input
prefix={filterTerm ? <DataSourceLogoPlaceHolder /> : <DataSourceLogo dataSource={current} />}
prefix={
filterTerm ? (
<DataSourceLogoPlaceHolder />
) : (
<DataSourceLogo dataSource={currentDataSourceInstanceSettings} />
)
}
suffix={<Icon name={filterTerm ? 'search' : 'angle-down'} />}
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) {
}}
>
<Input
className={styles.markerInput}
prefix={<DataSourceLogo dataSource={current} />}
className={styles.input}
prefix={<DataSourceLogo dataSource={currentDataSourceInstanceSettings} />}
suffix={<Icon name="angle-down" />}
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<HTMLDivElement, PickerContentProps>((prop
{...props}
current={current}
onChange={changeCallback}
filter={(ds) => ds.name.includes(filterTerm ?? '')}
filter={(ds) => ds.name.toLowerCase().includes(filterTerm?.toLowerCase() ?? '')}
></DataSourceList>
</CustomScrollbar>
</div>
@ -169,8 +171,6 @@ const PickerContent = React.forwardRef<HTMLDivElement, PickerContentProps>((prop
onClick={() => {
onClose();
showModal(DataSourceModal, {
datasources: props.datasources,
recentlyUsed: props.recentlyUsed,
enableFileUpload: props.enableFileUpload,
fileUploadOptions: props.fileUploadOptions,
current,

@ -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;
}
/**
* 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<DataSourceListProps, DataSourceListState> {
dataSourceSrv = getDataSourceSrv();
static defaultProps: Partial<DataSourceListProps> = {
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;
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 options = this.getDataSourceOptions();
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,
});
return (
<div className={className}>
{options.map((ds) => (
{dataSources
.filter((ds) => (props.filter ? props.filter(ds) : true))
.map((ds) => (
<DataSourceCard
key={ds.uid}
ds={ds}
onClick={this.onChange.bind(this, ds)}
onClick={() => onChange(ds)}
selected={!!isDataSourceMatch(ds, current)}
/>
))}
</div>
);
}
}

@ -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<DataSourceJsonData> | string | DataSourceRef | null | undefined;
dataSource: DataSourceInstanceSettings<DataSourceJsonData> | undefined;
}
export function DataSourceLogo(props: DataSourceLogoProps) {
@ -14,14 +13,9 @@ 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 (
<img
className={styles.pickerDSLogo}
@ -31,9 +25,6 @@ export function DataSourceLogo(props: DataSourceLogoProps) {
);
}
return null;
}
export function DataSourceLogoPlaceHolder() {
const styles = useStyles2(getStyles);
return <div className={styles.pickerDSLogo}></div>;

@ -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}
/>

@ -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 ? (
<DeprecatedDataSourcePicker {...props} />
) : (
<DataSourcePickerWithHistory {...props} />
<DataSourceDropdown {...props} />
);
}

@ -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<DataSourcePickerProps, DataSourcePickerState> {
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<DataSourceJsonData>) => {
this.props.onChange(ds);
this.setState({ error: undefined });
};
private getCurrentDs(): DataSourceInstanceSettings<DataSourceJsonData> | 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 (
<div>
<DataSourceDropdown
{...this.props}
datasources={this.getDatasources()}
onChange={this.onChange}
recentlyUsed={recentlyUsed}
current={this.getCurrentDs()}
fileUploadOptions={fileUploadOptions}
enableFileUpload={enableFileUpload}
onClickAddCSV={onClickAddCSV}
/>
</div>
);
}
}

@ -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]);
});
});
});

@ -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 (
<LocalStorageValueProvider<DataSourcePickerHistoryItem[]>
defaultValue={[]}
storageKey={props.localStorageKey ?? DS_PICKER_STORAGE_KEY}
>
{(rawValues, onSaveToStore) => {
return (
<DataSourcePicker
{...props}
recentlyUsed={rawValues.map((dsi) => 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);
}}
></DataSourcePicker>
);
}}
</LocalStorageValueProvider>
);
};
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);
}

@ -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<DataSourceInstanceSettings<DataSourceJsonData>>;
export interface DataSourceDropdownProps {
onChange: (ds: DataSourceInstanceSettings<DataSourceJsonData>) => void;
current: DataSourceInstanceSettings<DataSourceJsonData> | 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<DataSourcePickerProps, 'recentlyUsed'> {
localStorageKey?: string;
}
export interface DataSourcePickerHistoryItem {
lastUse: string;
uid: string;
}

@ -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<DataSourceJsonData> | 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);
}

Loading…
Cancel
Save