[Under FF] New DS Picker with advance mode (#66566)

* Add a wrapper to switch between the previous and new DS picker depending on the feature toggle advancedDataSourcePicker.

* Add a new component to represent the modal DS picker, which we will refer as advanced DS picker
Integrate this into the Edit panel, for now, until we're ready to replace everywhere the grafana-runtime DS picker by the wrapper.

* Replace Drawer component with the dropdown

* Adjust the first version of the styles to fit into this Figma design

* Adjust the design of the FileDropzoneDefaultChildren to match with the new DS modal but everywhere else is used nowadays.

---------

Co-authored-by: Oscar Kilhed <oscar.kilhed@grafana.com>
This commit is contained in:
Ivan Ortega Alba
2023-04-19 16:08:09 +03:00
committed by GitHub
co-authored by Oscar Kilhed
parent ee247e33b4
commit c7af53b79f
22 changed files with 762 additions and 331 deletions
@@ -0,0 +1,54 @@
import { css, cx } from '@emotion/css';
import React from 'react';
import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data';
import { Card, TagList, useStyles2 } from '@grafana/ui';
interface DataSourceCardProps {
ds: DataSourceInstanceSettings;
onClick: () => void;
selected: boolean;
}
export function DataSourceCard({ ds, onClick, selected }: DataSourceCardProps) {
const styles = useStyles2(getStyles);
return (
<Card key={ds.uid} onClick={onClick} className={cx(styles.card, selected ? styles.selected : undefined)}>
<Card.Heading>{ds.name}</Card.Heading>
<Card.Meta className={styles.meta}>
{ds.meta.name}
{ds.meta.info.description}
</Card.Meta>
<Card.Figure>
<img src={ds.meta.info.logos.small} alt={`${ds.meta.name} Logo`} height="40" width="40" />
</Card.Figure>
<Card.Tags>{ds.isDefault ? <TagList tags={['default']} /> : null}</Card.Tags>
</Card>
);
}
// Get styles for the component
function getStyles(theme: GrafanaTheme2) {
return {
card: css`
cursor: pointer;
background-color: ${theme.colors.background.primary};
border-bottom: 1px solid ${theme.colors.border.weak};
// Move to list component
margin-bottom: 0;
border-radius: 0;
`,
selected: css`
background-color: ${theme.colors.background.secondary};
`,
meta: css`
display: block;
overflow-wrap: unset;
white-space: nowrap;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
`,
};
}
@@ -0,0 +1,223 @@
import { css } from '@emotion/css';
import { useDialog } from '@react-aria/dialog';
import { FocusScope } from '@react-aria/focus';
import { useOverlay } from '@react-aria/overlays';
import React, { useCallback, useRef, useState } from 'react';
import { usePopper } from 'react-popper';
import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data';
import { DataSourceJsonData } from '@grafana/schema';
import { Button, CustomScrollbar, Icon, Input, ModalsController, Portal, useStyles2 } from '@grafana/ui';
import { DataSourceList } from './DataSourceList';
import { DataSourceLogo, DataSourceLogoPlaceHolder } from './DataSourceLogo';
import { DataSourceModal } from './DataSourceModal';
import { PickerContentProps, DataSourceDrawerProps } from './types';
import { dataSourceName as dataSourceLabel } from './utils';
export function DataSourceDropdown(props: DataSourceDrawerProps) {
const { current, onChange, ...restProps } = props;
const [isOpen, setOpen] = useState(false);
const [markerElement, setMarkerElement] = useState<HTMLInputElement | null>();
const [selectorElement, setSelectorElement] = useState<HTMLDivElement | null>();
const [filterTerm, setFilterTerm] = useState<string>();
const popper = usePopper(markerElement, selectorElement, {
placement: 'bottom-start',
});
const ref = useRef<HTMLDivElement>(null);
const { overlayProps, underlayProps } = useOverlay(
{
onClose: () => {
setFilterTerm(undefined);
setOpen(false);
},
isDismissable: true,
isOpen,
shouldCloseOnInteractOutside: (element) => {
return markerElement ? !markerElement.isSameNode(element) : false;
},
},
ref
);
const { dialogProps } = useDialog({}, ref);
const styles = useStyles2(getStylesDropdown);
return (
<div className={styles.container}>
{isOpen ? (
<FocusScope contain autoFocus restoreFocus>
<Input
prefix={filterTerm ? <DataSourceLogoPlaceHolder /> : <DataSourceLogo dataSource={current} />}
suffix={<Icon name={filterTerm ? 'search' : 'angle-down'} />}
placeholder={dataSourceLabel(current)}
className={styles.input}
onChange={(e) => {
setFilterTerm(e.currentTarget.value);
}}
ref={setMarkerElement}
></Input>
<Portal>
<div {...underlayProps} />
<div ref={ref} {...overlayProps} {...dialogProps}>
<PickerContent
filterTerm={filterTerm}
onChange={(ds: DataSourceInstanceSettings<DataSourceJsonData>) => {
setFilterTerm(undefined);
setOpen(false);
onChange(ds);
}}
onClose={() => {
setOpen(false);
}}
current={current}
style={popper.styles.popper}
ref={setSelectorElement}
{...restProps}
onDismiss={() => {}}
></PickerContent>
</div>
</Portal>
</FocusScope>
) : (
<div
className={styles.trigger}
onClick={() => {
setOpen(true);
}}
>
<Input
className={styles.markerInput}
prefix={<DataSourceLogo dataSource={current} />}
suffix={<Icon name="angle-down" />}
value={dataSourceLabel(current)}
onFocus={() => {
setOpen(true);
}}
/>
</div>
)}
</div>
);
}
function getStylesDropdown(theme: GrafanaTheme2) {
return {
container: css`
position: relative;
`,
trigger: css`
cursor: pointer;
`,
input: css`
input:focus {
box-shadow: none;
}
`,
markerInput: css`
input {
cursor: pointer;
}
`,
};
}
const PickerContent = React.forwardRef<HTMLDivElement, PickerContentProps>((props, ref) => {
const { filterTerm, onChange, onClose, onClickAddCSV, current } = props;
const changeCallback = useCallback(
(ds: DataSourceInstanceSettings<DataSourceJsonData>) => {
onChange(ds);
},
[onChange]
);
const clickAddCSVCallback = useCallback(() => {
onClickAddCSV?.();
onClose();
}, [onClickAddCSV, onClose]);
const styles = useStyles2(getStylesPickerContent);
return (
<div style={props.style} ref={ref} className={styles.container}>
<div className={styles.dataSourceList}>
<CustomScrollbar>
<DataSourceList
mixed
dashboard
current={current}
onChange={changeCallback}
filter={(ds) => !ds.meta.builtIn && ds.name.includes(filterTerm ?? '')}
></DataSourceList>
</CustomScrollbar>
</div>
<div className={styles.footer}>
{onClickAddCSV && (
<Button variant="secondary" size="sm" onClick={clickAddCSVCallback}>
Add csv or spreadsheet
</Button>
)}
<ModalsController>
{({ showModal, hideModal }) => (
<Button
size="sm"
variant="secondary"
fill="text"
onClick={() => {
onClose();
showModal(DataSourceModal, {
datasources: props.datasources,
recentlyUsed: props.recentlyUsed,
enableFileUpload: props.enableFileUpload,
fileUploadOptions: props.fileUploadOptions,
current,
onDismiss: hideModal,
onChange: (ds) => {
onChange(ds);
hideModal();
},
});
}}
>
Open advanced data source picker
<Icon name="arrow-right" />
</Button>
)}
</ModalsController>
</div>
</div>
);
});
PickerContent.displayName = 'PickerContent';
function getStylesPickerContent(theme: GrafanaTheme2) {
return {
container: css`
display: flex;
flex-direction: column;
height: 480px;
box-shadow: ${theme.shadows.z3};
width: 480px;
background: ${theme.colors.background.primary};
box-shadow: ${theme.shadows.z3};
`,
picker: css`
background: ${theme.colors.background.secondary};
`,
dataSourceList: css`
height: 423px;
padding: 0 ${theme.spacing(2)};
`,
footer: css`
display: flex;
justify-content: space-between;
padding: ${theme.spacing(2)};
border-top: 1px solid ${theme.colors.border.weak};
height: 57px;
`,
};
}
@@ -0,0 +1,119 @@
import React, { PureComponent } from 'react';
import { DataSourceInstanceSettings, DataSourceRef } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import { DataSourceCard } from './DataSourceCard';
import { isDataSourceMatch } from './utils';
/**
* Component props description for the {@link DataSourceList}
*
* @internal
*/
export interface DataSourceListProps {
className?: string;
onChange: (ds: DataSourceInstanceSettings) => void;
current: DataSourceRef | string | null; // uid
tracing?: boolean;
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;
width?: number;
inputId?: string;
filter?: (dataSource: DataSourceInstanceSettings) => boolean;
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;
// QUESTION: Should we use data from the Redux store as admin DS view does?
const options = this.getDataSourceOptions();
return (
<div className={className}>
{options.map((ds) => (
<DataSourceCard
key={ds.uid}
ds={ds}
onClick={this.onChange.bind(this, ds)}
selected={!!isDataSourceMatch(ds, current)}
/>
))}
</div>
);
}
}
@@ -0,0 +1,49 @@
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;
}
export function DataSourceLogo(props: DataSourceLogoProps) {
const { dataSource } = props;
const styles = useStyles2(getStyles);
if (!dataSource) {
return null;
}
if (typeof dataSource === 'string') {
return null;
}
if ('name' in dataSource) {
return (
<img
className={styles.pickerDSLogo}
alt={`${dataSource.meta.name} logo`}
src={dataSource.meta.info.logos.small}
></img>
);
}
return null;
}
export function DataSourceLogoPlaceHolder() {
const styles = useStyles2(getStyles);
return <div className={styles.pickerDSLogo}></div>;
}
function getStyles(theme: GrafanaTheme2) {
return {
pickerDSLogo: css`
height: 20px;
width: 20px;
`,
};
}
@@ -0,0 +1,160 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import { DropzoneOptions } from 'react-dropzone';
import { DataSourceInstanceSettings, DataSourceRef, GrafanaTheme2 } from '@grafana/data';
import {
Modal,
FileDropzone,
FileDropzoneDefaultChildren,
CustomScrollbar,
LinkButton,
useStyles2,
Input,
Icon,
} from '@grafana/ui';
import * as DFImport from 'app/features/dataframe-import';
import { DataSourceList } from './DataSourceList';
interface DataSourceModalProps {
onChange: (ds: DataSourceInstanceSettings) => void;
current: DataSourceRef | string | null | undefined;
onDismiss: () => void;
datasources: DataSourceInstanceSettings[];
recentlyUsed?: string[];
enableFileUpload?: boolean;
fileUploadOptions?: DropzoneOptions;
}
export function DataSourceModal({
enableFileUpload,
fileUploadOptions,
onChange,
current,
onDismiss,
}: DataSourceModalProps) {
const styles = useStyles2(getDataSourceModalStyles);
const [search, setSearch] = useState('');
return (
<Modal
title="Select data source"
closeOnEscape={true}
closeOnBackdropClick={true}
isOpen={true}
className={styles.modal}
onClickBackdrop={onDismiss}
onDismiss={onDismiss}
>
<div className={styles.modalContent}>
<div className={styles.leftColumn}>
<Input
className={styles.searchInput}
value={search}
prefix={<Icon name="search" />}
placeholder="Search data source"
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<CustomScrollbar>
<DataSourceList
dashboard={false}
mixed={false}
// FIXME: Filter out the grafana data source in a hacky way
filter={(ds) => ds.name.includes(search) && ds.name !== '-- Grafana --'}
onChange={onChange}
current={current}
/>
</CustomScrollbar>
</div>
<div className={styles.rightColumn}>
<div className={styles.builtInDataSources}>
<DataSourceList
className={styles.builtInDataSourceList}
filter={(ds) => !!ds.meta.builtIn}
dashboard
mixed
onChange={onChange}
current={current}
/>
{enableFileUpload && (
<FileDropzone
readAs="readAsArrayBuffer"
fileListRenderer={() => undefined}
options={{
maxSize: DFImport.maxFileSize,
multiple: false,
accept: DFImport.acceptedFiles,
...fileUploadOptions,
onDrop: (...args) => {
fileUploadOptions?.onDrop?.(...args);
onDismiss();
},
}}
>
<FileDropzoneDefaultChildren />
</FileDropzone>
)}
</div>
<div className={styles.dsCTAs}>
<LinkButton variant="secondary" href={`datasources/new`}>
Configure a new data source
</LinkButton>
</div>
</div>
</div>
</Modal>
);
}
function getDataSourceModalStyles(theme: GrafanaTheme2) {
return {
modal: css`
width: 80%;
height: 80%;
`,
modalContent: css`
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: stretch;
height: 100%;
`,
leftColumn: css`
display: flex;
flex-direction: column;
width: 50%;
height: 100%;
padding-right: ${theme.spacing(1)};
border-right: 1px solid ${theme.colors.border.weak};
`,
rightColumn: css`
display: flex;
flex-direction: column;
width: 50%;
height: 100%;
padding: ${theme.spacing(1)};
justify-items: space-evenly;
align-items: stretch;
padding-left: ${theme.spacing(1)};
`,
builtInDataSources: css`
flex: 1;
margin-bottom: ${theme.spacing(4)};
`,
builtInDataSourceList: css`
margin-bottom: ${theme.spacing(4)};
`,
dsCTAs: css`
display: flex;
flex-direction: row;
width: 100%;
justify-content: flex-end;
`,
searchInput: css`
width: 100%;
min-height: 32px;
margin-bottom: ${theme.spacing(1)};
`,
};
}
@@ -0,0 +1,25 @@
import React from 'react';
import {
DataSourcePicker as DeprecatedDataSourcePicker,
DataSourcePickerProps as DeprecatedDataSourcePickerProps,
} from '@grafana/runtime';
import { config } from 'app/core/config';
import { DataSourcePickerWithHistory } from './DataSourcePickerWithHistory';
import { DataSourcePickerWithHistoryProps } from './types';
type DataSourcePickerProps = DeprecatedDataSourcePickerProps | DataSourcePickerWithHistoryProps;
/**
* DataSourcePicker is a wrapper around the old DataSourcePicker and the new one.
* Depending on the feature toggle, it will render the old or the new one.
* Feature toggle: advancedDataSourcePicker
*/
export function DataSourcePicker(props: DataSourcePickerProps) {
return !config.featureToggles.advancedDataSourcePicker ? (
<DeprecatedDataSourcePicker {...props} />
) : (
<DataSourcePickerWithHistory {...props} />
);
}
@@ -0,0 +1,95 @@
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
datasources={this.getDatasources()}
onChange={this.onChange}
recentlyUsed={recentlyUsed}
current={this.getCurrentDs()}
fileUploadOptions={fileUploadOptions}
enableFileUpload={enableFileUpload}
onClickAddCSV={onClickAddCSV}
/>
</div>
);
}
}
@@ -0,0 +1,27 @@
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]);
});
});
});
@@ -0,0 +1,55 @@
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);
}
@@ -0,0 +1,57 @@
import React from 'react';
import { DropzoneOptions } from 'react-dropzone';
import { DataSourceInstanceSettings } from '@grafana/data';
import { DataSourceJsonData, DataSourceRef } from '@grafana/schema';
export interface DataSourceDrawerProps {
datasources: Array<DataSourceInstanceSettings<DataSourceJsonData>>;
onChange: (ds: DataSourceInstanceSettings<DataSourceJsonData>) => void;
current: DataSourceInstanceSettings<DataSourceJsonData> | string | DataSourceRef | null | undefined;
enableFileUpload?: boolean;
fileUploadOptions?: DropzoneOptions;
onClickAddCSV?: () => void;
recentlyUsed?: string[];
}
export interface PickerContentProps extends DataSourceDrawerProps {
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;
}
@@ -0,0 +1,30 @@
import { DataSourceInstanceSettings, DataSourceRef } from '@grafana/data';
import { isDataSourceMatch } from './utils';
describe('isDataSourceMatch', () => {
const dataSourceInstanceSettings = { uid: 'a' } as DataSourceInstanceSettings;
it('matches a string with the uid', () => {
expect(isDataSourceMatch(dataSourceInstanceSettings, 'a')).toBeTruthy();
});
it('matches a datasource with a datasource by the uid', () => {
expect(isDataSourceMatch(dataSourceInstanceSettings, { uid: 'a' } as DataSourceInstanceSettings)).toBeTruthy();
});
it('matches a datasource ref with a datasource by the uid', () => {
expect(isDataSourceMatch(dataSourceInstanceSettings, { uid: 'a' } as DataSourceRef)).toBeTruthy();
});
it('doesnt match with null', () => {
expect(isDataSourceMatch(dataSourceInstanceSettings, null)).toBeFalsy();
});
it('doesnt match a datasource to a non matching string', () => {
expect(isDataSourceMatch(dataSourceInstanceSettings, 'b')).toBeFalsy();
});
it('doesnt match a datasource with a different datasource uid', () => {
expect(isDataSourceMatch(dataSourceInstanceSettings, { uid: 'b' } as DataSourceInstanceSettings)).toBeFalsy();
});
it('doesnt match a datasource with a datasource ref with a different uid', () => {
expect(isDataSourceMatch(dataSourceInstanceSettings, { uid: 'b' } as DataSourceRef)).toBeFalsy();
});
});
@@ -0,0 +1,39 @@
import { DataSourceInstanceSettings, DataSourceJsonData, DataSourceRef } from '@grafana/data';
export function isDataSourceMatch(
ds: DataSourceInstanceSettings | undefined,
current: string | DataSourceInstanceSettings | DataSourceRef | null | undefined
): boolean | undefined {
if (!ds) {
return false;
}
if (!current) {
return false;
}
if (typeof current === 'string') {
return ds.uid === current;
}
return ds.uid === current.uid;
}
export function dataSourceName(
dataSource: DataSourceInstanceSettings<DataSourceJsonData> | string | DataSourceRef | null | undefined
) {
if (!dataSource) {
return 'Unknown';
}
if (typeof dataSource === 'string') {
return `${dataSource} - not found`;
}
if ('name' in dataSource) {
return dataSource.name;
}
if (dataSource.uid) {
return `${dataSource.uid} - not found`;
}
return 'Unknown';
}