diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 2d551c7a4df..bc933311d41 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -353,7 +353,6 @@ lerna.json @grafana/frontend-ops
/public/app/features/datasources/ @grafana/plugins-platform-frontend
/public/app/features/dimensions/ @grafana/dataviz-squad
/public/app/features/dataframe-import/ @grafana/grafana-bi-squad
-/public/app/features/datasource-drawer/ @grafana/grafana-bi-squad
/public/app/features/explore/ @grafana/explore-squad
/public/app/features/expressions/ @grafana/observability-metrics
/public/app/features/folders/ @grafana/grafana-frontend-platform
diff --git a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx
index 228c013282f..cd2ee88422d 100644
--- a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx
+++ b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx
@@ -216,18 +216,11 @@ export function FileDropzone({ options, children, readAs, onLoad, fileListRender
{children ?? }
{fileErrors.length > 0 && renderErrorMessages(fileErrors)}
-
- {options?.accept && (
-
- {getAcceptedFileTypeText(options.accept)}
-
- )}
- {options?.maxSize && (
- {`Max file size: ${formattedValueToString(
- formattedSize
- )}`}
- )}
-
+
+ {options?.maxSize && `Max file size: ${formattedValueToString(formattedSize)}`}
+ {options?.maxSize && options?.accept && |}
+ {options?.accept && getAcceptedFileTypeText(options.accept)}
+
{fileList}
);
@@ -261,17 +254,14 @@ export function transformAcceptToNewFormat(accept?: string | string[] | Accept):
return accept;
}
-export function FileDropzoneDefaultChildren({
- primaryText = 'Upload file',
- secondaryText = 'Drag and drop here or browse',
-}) {
+export function FileDropzoneDefaultChildren({ primaryText = 'Drop file here or click to upload', secondaryText = '' }) {
const theme = useTheme2();
const styles = getStyles(theme);
return (
-
-
-
{primaryText}
+
+
+
{primaryText}
{secondaryText}
);
@@ -312,31 +302,33 @@ function getStyles(theme: GrafanaTheme2, isDragActive?: boolean) {
display: flex;
flex-direction: column;
width: 100%;
+ padding: ${theme.spacing(2)};
+ border-radius: 2px;
+ border: 1px dashed ${theme.colors.border.strong};
+ background-color: ${isDragActive ? theme.colors.background.secondary : theme.colors.background.primary};
+ cursor: pointer;
+ align-items: center;
+ justify-content: center;
`,
dropzone: css`
display: flex;
- flex: 1;
flex-direction: column;
- align-items: center;
- padding: ${theme.spacing(6)};
- border-radius: 2px;
- border: 2px dashed ${theme.colors.border.medium};
- background-color: ${isDragActive ? theme.colors.background.secondary : theme.colors.background.primary};
- cursor: pointer;
`,
- iconWrapper: css`
- display: flex;
- flex-direction: column;
- align-items: center;
+ defaultDropZone: css`
+ text-align: center;
+ `,
+ icon: css`
+ margin-bottom: ${theme.spacing(1)};
+ `,
+ primaryText: css`
+ margin-bottom: ${theme.spacing(1)};
`,
acceptContainer: css`
- display: flex;
+ text-align: center;
+ margin: 0;
`,
- acceptedFiles: css`
- flex-grow: 1;
- `,
- acceptMargin: css`
- margin: ${theme.spacing(2, 0, 1)};
+ acceptSeparator: css`
+ margin: 0 ${theme.spacing(1)};
`,
small: css`
color: ${theme.colors.text.secondary};
diff --git a/public/app/features/datasource-drawer/DataSourceDrawer.test.ts b/public/app/features/datasource-drawer/DataSourceDrawer.test.ts
deleted file mode 100644
index 918ea38ac8e..00000000000
--- a/public/app/features/datasource-drawer/DataSourceDrawer.test.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { DataSourceInstanceSettings } from '@grafana/data';
-import { DataSourceJsonData, DataSourceRef } from '@grafana/schema';
-
-import { isDataSourceMatch } from './DataSourceDrawer';
-
-describe('DataSourceDrawer', () => {
- 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();
- });
- });
-});
diff --git a/public/app/features/datasource-drawer/DataSourceDrawer.tsx b/public/app/features/datasource-drawer/DataSourceDrawer.tsx
deleted file mode 100644
index 4e7936bce86..00000000000
--- a/public/app/features/datasource-drawer/DataSourceDrawer.tsx
+++ /dev/null
@@ -1,161 +0,0 @@
-import { css } from '@emotion/css';
-import React, { useCallback, useState } from 'react';
-
-import { DataSourceInstanceSettings, DataSourceJsonData, DataSourceRef, GrafanaTheme2 } from '@grafana/data';
-import {
- Button,
- CustomScrollbar,
- Drawer,
- FileDropzone,
- FileDropzoneDefaultChildren,
- Input,
- ModalsController,
- useStyles2,
-} from '@grafana/ui';
-
-import { DataSourceCard } from './components/DataSourceCard';
-import { DataSourceDisplay } from './components/DataSourceDisplay';
-import { PickerContentProps, DataSourceDrawerProps } from './types';
-
-export function DataSourceDrawer(props: DataSourceDrawerProps) {
- const { current, onChange } = props;
- const styles = useStyles2(getStyles);
-
- return (
-
- {({ showModal, hideModal }) => (
-
- )}
-
- );
-}
-
-function PickerContent(props: PickerContentProps) {
- const { datasources, enableFileUpload, recentlyUsed = [], onChange, fileUploadOptions, onDismiss, current } = props;
- const changeCallback = useCallback(
- (ds: string) => {
- onChange(ds);
- },
- [onChange]
- );
-
- const [filterTerm, onFilterChange] = useState('');
- const styles = useStyles2(getStyles);
-
- const filteredDataSources = datasources.filter((ds) => {
- return ds?.name.toLocaleLowerCase().indexOf(filterTerm.toLocaleLowerCase()) !== -1;
- });
-
- return (
-
-
-
- {
- onFilterChange(e.currentTarget.value);
- }}
- value={filterTerm}
- >
-
-
-
- {recentlyUsed
- .map((uid) => filteredDataSources.find((ds) => ds.uid === uid))
- .map((ds) => {
- if (!ds) {
- return null;
- }
- return (
-
- );
- })}
- {recentlyUsed && recentlyUsed.length > 0 &&
}
- {filteredDataSources.map((ds) => (
-
- ))}
-
-
- {enableFileUpload && (
-
- undefined}
- options={{
- ...fileUploadOptions,
- onDrop: (...args) => {
- onDismiss();
- fileUploadOptions?.onDrop?.(...args);
- },
- }}
- >
-
-
-
- )}
-
-
- );
-}
-
-function getStyles(theme: GrafanaTheme2) {
- return {
- drawerContent: css`
- display: flex;
- flex-direction: column;
- height: 100%;
- `,
- picker: css`
- background: ${theme.colors.background.secondary};
- `,
- filterContainer: css`
- padding-bottom: ${theme.spacing(1)};
- `,
- dataSourceList: css`
- height: 50px;
- flex-grow: 1;
- `,
- additionalContent: css`
- padding-top: ${theme.spacing(1)};
- `,
- };
-}
-
-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;
-}
diff --git a/public/app/features/datasource-drawer/components/DataSourceCard.tsx b/public/app/features/datasource-drawer/components/DataSourceCard.tsx
deleted file mode 100644
index 7856f570cb2..00000000000
--- a/public/app/features/datasource-drawer/components/DataSourceCard.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { css } from '@emotion/css';
-import React from 'react';
-
-import { DataSourceInstanceSettings, DataSourceJsonData, GrafanaTheme2 } from '@grafana/data';
-import { Card, PluginSignatureBadge, Tag, useStyles2 } from '@grafana/ui';
-
-export interface DataSourceCardProps {
- onChange: (uid: string) => void;
- selected?: boolean;
- ds: DataSourceInstanceSettings;
-}
-
-export function DataSourceCard(props: DataSourceCardProps) {
- const { selected, ds, onChange } = props;
- const styles = useStyles2(getStyles);
- return (
- onChange(ds.uid)}>
-
-
-
-
- {[ds.meta.name, ds.url, ds.isDefault && ]}
-
-
-
-
- {ds.name}
-
- );
-}
-
-function getStyles(theme: GrafanaTheme2) {
- return {
- selectedDataSource: css`
- background-color: ${theme.colors.emphasize(theme.colors.background.secondary, 0.1)};
- `,
- };
-}
diff --git a/public/app/features/datasources/components/picker/DataSourceCard.tsx b/public/app/features/datasources/components/picker/DataSourceCard.tsx
new file mode 100644
index 00000000000..fd9c65234c2
--- /dev/null
+++ b/public/app/features/datasources/components/picker/DataSourceCard.tsx
@@ -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 (
+
+ {ds.name}
+
+ {ds.meta.name}
+ {ds.meta.info.description}
+
+
+
+
+ {ds.isDefault ? : null}
+
+ );
+}
+
+// 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;
+ `,
+ };
+}
diff --git a/public/app/features/datasources/components/picker/DataSourceDropdown.tsx b/public/app/features/datasources/components/picker/DataSourceDropdown.tsx
new file mode 100644
index 00000000000..8c563e11e00
--- /dev/null
+++ b/public/app/features/datasources/components/picker/DataSourceDropdown.tsx
@@ -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();
+ const [selectorElement, setSelectorElement] = useState();
+ const [filterTerm, setFilterTerm] = useState();
+
+ const popper = usePopper(markerElement, selectorElement, {
+ placement: 'bottom-start',
+ });
+
+ const ref = useRef(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 (
+
+ {isOpen ? (
+
+ : }
+ suffix={}
+ placeholder={dataSourceLabel(current)}
+ className={styles.input}
+ onChange={(e) => {
+ setFilterTerm(e.currentTarget.value);
+ }}
+ ref={setMarkerElement}
+ >
+
+
+
+
) => {
+ setFilterTerm(undefined);
+ setOpen(false);
+ onChange(ds);
+ }}
+ onClose={() => {
+ setOpen(false);
+ }}
+ current={current}
+ style={popper.styles.popper}
+ ref={setSelectorElement}
+ {...restProps}
+ onDismiss={() => {}}
+ >
+
+
+
+ ) : (
+
{
+ setOpen(true);
+ }}
+ >
+ }
+ suffix={}
+ value={dataSourceLabel(current)}
+ onFocus={() => {
+ setOpen(true);
+ }}
+ />
+
+ )}
+
+ );
+}
+
+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((props, ref) => {
+ const { filterTerm, onChange, onClose, onClickAddCSV, current } = props;
+ const changeCallback = useCallback(
+ (ds: DataSourceInstanceSettings) => {
+ onChange(ds);
+ },
+ [onChange]
+ );
+
+ const clickAddCSVCallback = useCallback(() => {
+ onClickAddCSV?.();
+ onClose();
+ }, [onClickAddCSV, onClose]);
+
+ const styles = useStyles2(getStylesPickerContent);
+
+ return (
+
+
+
+ !ds.meta.builtIn && ds.name.includes(filterTerm ?? '')}
+ >
+
+
+
+
+ {onClickAddCSV && (
+
+ )}
+
+ {({ showModal, hideModal }) => (
+
+ )}
+
+
+
+ );
+});
+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;
+ `,
+ };
+}
diff --git a/public/app/features/datasources/components/picker/DataSourceList.tsx b/public/app/features/datasources/components/picker/DataSourceList.tsx
new file mode 100644
index 00000000000..408f8aa4c9c
--- /dev/null
+++ b/public/app/features/datasources/components/picker/DataSourceList.tsx
@@ -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 {
+ 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) => (
+
+ ))}
+
+ );
+ }
+}
diff --git a/public/app/features/datasource-drawer/components/DataSourceDisplay.tsx b/public/app/features/datasources/components/picker/DataSourceLogo.tsx
similarity index 57%
rename from public/app/features/datasource-drawer/components/DataSourceDisplay.tsx
rename to public/app/features/datasources/components/picker/DataSourceLogo.tsx
index 45e46c6414a..cb05adb3c37 100644
--- a/public/app/features/datasource-drawer/components/DataSourceDisplay.tsx
+++ b/public/app/features/datasources/components/picker/DataSourceLogo.tsx
@@ -5,36 +5,38 @@ import { DataSourceInstanceSettings, DataSourceJsonData, GrafanaTheme2 } from '@
import { DataSourceRef } from '@grafana/schema';
import { useStyles2 } from '@grafana/ui';
-export interface DataSourceDisplayProps {
+export interface DataSourceLogoProps {
dataSource: DataSourceInstanceSettings | string | DataSourceRef | null | undefined;
}
-export function DataSourceDisplay(props: DataSourceDisplayProps) {
+export function DataSourceLogo(props: DataSourceLogoProps) {
const { dataSource } = props;
const styles = useStyles2(getStyles);
if (!dataSource) {
- return Unknown;
+ return null;
}
if (typeof dataSource === 'string') {
- return ${dataSource} - not found;
+ return null;
}
if ('name' in dataSource) {
return (
- <>
-
- {dataSource.name}
- >
+
);
}
- return {dataSource.uid} - not found;
+ return null;
+}
+
+export function DataSourceLogoPlaceHolder() {
+ const styles = useStyles2(getStyles);
+ return ;
}
function getStyles(theme: GrafanaTheme2) {
@@ -42,7 +44,6 @@ function getStyles(theme: GrafanaTheme2) {
pickerDSLogo: css`
height: 20px;
width: 20px;
- margin-right: ${theme.spacing(1)};
`,
};
}
diff --git a/public/app/features/datasources/components/picker/DataSourceModal.tsx b/public/app/features/datasources/components/picker/DataSourceModal.tsx
new file mode 100644
index 00000000000..99f86a0e7b4
--- /dev/null
+++ b/public/app/features/datasources/components/picker/DataSourceModal.tsx
@@ -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 (
+
+
+
+ }
+ placeholder="Search data source"
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+ ds.name.includes(search) && ds.name !== '-- Grafana --'}
+ onChange={onChange}
+ current={current}
+ />
+
+
+
+
+ !!ds.meta.builtIn}
+ dashboard
+ mixed
+ onChange={onChange}
+ current={current}
+ />
+ {enableFileUpload && (
+ undefined}
+ options={{
+ maxSize: DFImport.maxFileSize,
+ multiple: false,
+ accept: DFImport.acceptedFiles,
+ ...fileUploadOptions,
+ onDrop: (...args) => {
+ fileUploadOptions?.onDrop?.(...args);
+ onDismiss();
+ },
+ }}
+ >
+
+
+ )}
+
+
+
+ Configure a new data source
+
+
+
+
+
+ );
+}
+
+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)};
+ `,
+ };
+}
diff --git a/public/app/features/datasources/components/picker/DataSourcePicker.tsx b/public/app/features/datasources/components/picker/DataSourcePicker.tsx
new file mode 100644
index 00000000000..83677a1fd1e
--- /dev/null
+++ b/public/app/features/datasources/components/picker/DataSourcePicker.tsx
@@ -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 ? (
+
+ ) : (
+
+ );
+}
diff --git a/public/app/features/datasource-drawer/DataSourcePicker.tsx b/public/app/features/datasources/components/picker/DataSourcePickerNG.tsx
similarity index 85%
rename from public/app/features/datasource-drawer/DataSourcePicker.tsx
rename to public/app/features/datasources/components/picker/DataSourcePickerNG.tsx
index c9b84753a22..9c71bc118d1 100644
--- a/public/app/features/datasource-drawer/DataSourcePicker.tsx
+++ b/public/app/features/datasources/components/picker/DataSourcePickerNG.tsx
@@ -6,7 +6,7 @@ import { DataSourceInstanceSettings, DataSourceRef, getDataSourceUID } from '@gr
import { getDataSourceSrv } from '@grafana/runtime';
import { DataSourceJsonData } from '@grafana/schema';
-import { DataSourceDrawer } from './DataSourceDrawer';
+import { DataSourceDropdown } from './DataSourceDropdown';
import { DataSourcePickerProps } from './types';
/**
@@ -37,13 +37,9 @@ export class DataSourcePicker extends PureComponent {
- const dsSettings = this.dataSourceSrv.getInstanceSettings(ds);
-
- if (dsSettings) {
- this.props.onChange(dsSettings);
- this.setState({ error: undefined });
- }
+ onChange = (ds: DataSourceInstanceSettings) => {
+ this.props.onChange(ds);
+ this.setState({ error: undefined });
};
private getCurrentDs(): DataSourceInstanceSettings | string | DataSourceRef | null | undefined {
@@ -80,17 +76,18 @@ export class DataSourcePicker extends PureComponent
-
);
diff --git a/public/app/features/datasource-drawer/DataSourcePickerWithHistory.test.ts b/public/app/features/datasources/components/picker/DataSourcePickerWithHistory.test.ts
similarity index 100%
rename from public/app/features/datasource-drawer/DataSourcePickerWithHistory.test.ts
rename to public/app/features/datasources/components/picker/DataSourcePickerWithHistory.test.ts
diff --git a/public/app/features/datasource-drawer/DataSourcePickerWithHistory.tsx b/public/app/features/datasources/components/picker/DataSourcePickerWithHistory.tsx
similarity index 96%
rename from public/app/features/datasource-drawer/DataSourcePickerWithHistory.tsx
rename to public/app/features/datasources/components/picker/DataSourcePickerWithHistory.tsx
index 1f0f8272413..b9ea0335415 100644
--- a/public/app/features/datasource-drawer/DataSourcePickerWithHistory.tsx
+++ b/public/app/features/datasources/components/picker/DataSourcePickerWithHistory.tsx
@@ -3,7 +3,7 @@ import React from 'react';
import { dateTime } from '@grafana/data';
import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider';
-import { DataSourcePicker } from './DataSourcePicker';
+import { DataSourcePicker } from './DataSourcePickerNG';
import { DataSourcePickerHistoryItem, DataSourcePickerWithHistoryProps } from './types';
const DS_PICKER_STORAGE_KEY = 'DATASOURCE_PICKER';
diff --git a/public/app/features/datasource-drawer/types.ts b/public/app/features/datasources/components/picker/types.ts
similarity index 86%
rename from public/app/features/datasource-drawer/types.ts
rename to public/app/features/datasources/components/picker/types.ts
index c7589ab7110..b6e3101c261 100644
--- a/public/app/features/datasource-drawer/types.ts
+++ b/public/app/features/datasources/components/picker/types.ts
@@ -1,3 +1,4 @@
+import React from 'react';
import { DropzoneOptions } from 'react-dropzone';
import { DataSourceInstanceSettings } from '@grafana/data';
@@ -5,15 +6,18 @@ import { DataSourceJsonData, DataSourceRef } from '@grafana/schema';
export interface DataSourceDrawerProps {
datasources: Array>;
- onFileDrop?: () => void;
- onChange: (ds: string) => void;
+ onChange: (ds: DataSourceInstanceSettings) => void;
current: DataSourceInstanceSettings | 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;
}
@@ -40,6 +44,7 @@ export interface DataSourcePickerProps {
disabled?: boolean;
enableFileUpload?: boolean;
fileUploadOptions?: DropzoneOptions;
+ onClickAddCSV?: () => void;
}
export interface DataSourcePickerWithHistoryProps extends Omit {
diff --git a/public/app/features/datasources/components/picker/utils.test.ts b/public/app/features/datasources/components/picker/utils.test.ts
new file mode 100644
index 00000000000..8462309c539
--- /dev/null
+++ b/public/app/features/datasources/components/picker/utils.test.ts
@@ -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();
+ });
+});
diff --git a/public/app/features/datasources/components/picker/utils.ts b/public/app/features/datasources/components/picker/utils.ts
new file mode 100644
index 00000000000..257dca80a84
--- /dev/null
+++ b/public/app/features/datasources/components/picker/utils.ts
@@ -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 | 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';
+}
diff --git a/public/app/features/query/components/QueryGroup.tsx b/public/app/features/query/components/QueryGroup.tsx
index 0d97f58750b..eefdb6404a0 100644
--- a/public/app/features/query/components/QueryGroup.tsx
+++ b/public/app/features/query/components/QueryGroup.tsx
@@ -15,14 +15,14 @@ import {
PanelData,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
-import { DataSourcePicker, getDataSourceSrv } from '@grafana/runtime';
+import { getDataSourceSrv } from '@grafana/runtime';
import { Button, CustomScrollbar, HorizontalGroup, InlineFormLabel, Modal, stylesFactory } from '@grafana/ui';
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
import config from 'app/core/config';
import { backendSrv } from 'app/core/services/backend_srv';
import { addQuery, queryIsEmpty } from 'app/core/utils/query';
import * as DFImport from 'app/features/dataframe-import';
-import { DataSourcePickerWithHistory } from 'app/features/datasource-drawer/DataSourcePickerWithHistory';
+import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
import { dataSource as expressionDatasource } from 'app/features/expressions/ExpressionDatasource';
import { DashboardQueryEditor, isSharedDashboardQuery } from 'app/plugins/datasource/dashboard';
import { GrafanaQuery, GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
@@ -214,32 +214,22 @@ export class QueryGroup extends PureComponent {
Data source
- {config.featureToggles.advancedDataSourcePicker ? (
-
- ) : (
-
- )}
+
{dataSource && (
<>
@@ -315,6 +305,24 @@ export class QueryGroup extends PureComponent {
this.onScrollBottom();
};
+ onClickAddCSV = async () => {
+ const ds = getDataSourceSrv().getInstanceSettings('-- Grafana --');
+ await this.onChangeDataSource(ds!);
+
+ this.onQueriesChange([
+ {
+ refId: 'A',
+ datasource: {
+ type: 'grafana',
+ uid: 'grafana',
+ },
+ queryType: GrafanaQueryType.Snapshot,
+ snapshot: [],
+ },
+ ]);
+ this.props.onRunQueries();
+ };
+
onFileDrop = (acceptedFiles: File[], fileRejections: FileRejection[], event: DropEvent) => {
DFImport.filesToDataframes(acceptedFiles).subscribe(async (next) => {
const snapshot: DataFrameJSON[] = [];
diff --git a/public/app/plugins/datasource/dashboard/plugin.json b/public/app/plugins/datasource/dashboard/plugin.json
index 9b7ab421e8c..149811cbc44 100644
--- a/public/app/plugins/datasource/dashboard/plugin.json
+++ b/public/app/plugins/datasource/dashboard/plugin.json
@@ -5,7 +5,7 @@
"builtIn": true,
"info": {
- "description": "TODO",
+ "description": "Uses the result set from another panel in the same dashboard",
"author": {
"name": "Grafana Labs",
"url": "https://grafana.com"
diff --git a/public/app/plugins/datasource/grafana/components/QueryEditor.tsx b/public/app/plugins/datasource/grafana/components/QueryEditor.tsx
index 8fc7d07ec7a..2578bba448b 100644
--- a/public/app/plugins/datasource/grafana/components/QueryEditor.tsx
+++ b/public/app/plugins/datasource/grafana/components/QueryEditor.tsx
@@ -419,7 +419,9 @@ export class UnthemedQueryEditor extends PureComponent {
accept: DFImport.acceptedFiles,
}}
>
-
+
{file && (
diff --git a/public/app/plugins/datasource/grafana/plugin.json b/public/app/plugins/datasource/grafana/plugin.json
index 73a96ba2151..df8e1ec3c60 100644
--- a/public/app/plugins/datasource/grafana/plugin.json
+++ b/public/app/plugins/datasource/grafana/plugin.json
@@ -5,7 +5,7 @@
"builtIn": true,
"info": {
- "description": "TODO",
+ "description": "A built-in data source that generates random walk data and can poll the Testdata data source. This helps you test visualizations and run experiments.",
"author": {
"name": "Grafana Labs",
"url": "https://grafana.com"
diff --git a/public/app/plugins/datasource/mixed/plugin.json b/public/app/plugins/datasource/mixed/plugin.json
index b28c58710f0..cc4cd84eaea 100644
--- a/public/app/plugins/datasource/mixed/plugin.json
+++ b/public/app/plugins/datasource/mixed/plugin.json
@@ -4,6 +4,19 @@
"id": "mixed",
"builtIn": true,
+
+ "info": {
+ "description": "Lets you query multiple data sources in the same panel.",
+ "author": {
+ "name": "Grafana Labs",
+ "url": "https://grafana.com"
+ },
+ "logos": {
+ "small": "public/img/icn-datasource.svg",
+ "large": "public/img/icn-datasource.svg"
+ }
+ },
+
"mixed": true,
"metrics": true,