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.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.meta.name} + + {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.meta.name} - {dataSource.name} - + {`${dataSource.meta.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,