mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
[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:
parent
ee247e33b4
commit
c7af53b79f
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -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
|
||||
|
@ -216,18 +216,11 @@ export function FileDropzone({ options, children, readAs, onLoad, fileListRender
|
||||
{children ?? <FileDropzoneDefaultChildren primaryText={getPrimaryText(files, options)} />}
|
||||
</div>
|
||||
{fileErrors.length > 0 && renderErrorMessages(fileErrors)}
|
||||
<div className={styles.acceptContainer}>
|
||||
{options?.accept && (
|
||||
<small className={cx(styles.small, styles.acceptMargin, styles.acceptedFiles)}>
|
||||
{getAcceptedFileTypeText(options.accept)}
|
||||
</small>
|
||||
)}
|
||||
{options?.maxSize && (
|
||||
<small className={cx(styles.small, styles.acceptMargin)}>{`Max file size: ${formattedValueToString(
|
||||
formattedSize
|
||||
)}`}</small>
|
||||
)}
|
||||
</div>
|
||||
<small className={cx(styles.small, styles.acceptContainer)}>
|
||||
{options?.maxSize && `Max file size: ${formattedValueToString(formattedSize)}`}
|
||||
{options?.maxSize && options?.accept && <span className={styles.acceptSeparator}>|</span>}
|
||||
{options?.accept && getAcceptedFileTypeText(options.accept)}
|
||||
</small>
|
||||
{fileList}
|
||||
</div>
|
||||
);
|
||||
@ -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 (
|
||||
<div className={styles.iconWrapper}>
|
||||
<Icon name="upload" size="xxl" />
|
||||
<h3>{primaryText}</h3>
|
||||
<div className={cx(styles.defaultDropZone)}>
|
||||
<Icon className={cx(styles.icon)} name="upload" size="xl" />
|
||||
<h6 className={cx(styles.primaryText)}>{primaryText}</h6>
|
||||
<small className={styles.small}>{secondaryText}</small>
|
||||
</div>
|
||||
);
|
||||
@ -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};
|
||||
|
@ -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<DataSourceJsonData>;
|
||||
|
||||
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<DataSourceJsonData>)
|
||||
).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<DataSourceJsonData>)
|
||||
).toBeFalsy();
|
||||
});
|
||||
it('doesnt match a datasource with a datasource ref with a different uid', () => {
|
||||
expect(isDataSourceMatch(dataSourceInstanceSettings, { uid: 'b' } as DataSourceRef)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
@ -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 (
|
||||
<ModalsController>
|
||||
{({ showModal, hideModal }) => (
|
||||
<Button
|
||||
className={styles.picker}
|
||||
onClick={() => {
|
||||
showModal(PickerContent, {
|
||||
...props,
|
||||
onDismiss: hideModal,
|
||||
onChange: (ds) => {
|
||||
onChange(ds);
|
||||
hideModal();
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<DataSourceDisplay dataSource={current}></DataSourceDisplay>
|
||||
</Button>
|
||||
)}
|
||||
</ModalsController>
|
||||
);
|
||||
}
|
||||
|
||||
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<string>('');
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const filteredDataSources = datasources.filter((ds) => {
|
||||
return ds?.name.toLocaleLowerCase().indexOf(filterTerm.toLocaleLowerCase()) !== -1;
|
||||
});
|
||||
|
||||
return (
|
||||
<Drawer closeOnMaskClick={true} onClose={onDismiss}>
|
||||
<div className={styles.drawerContent}>
|
||||
<div className={styles.filterContainer}>
|
||||
<Input
|
||||
onChange={(e) => {
|
||||
onFilterChange(e.currentTarget.value);
|
||||
}}
|
||||
value={filterTerm}
|
||||
></Input>
|
||||
</div>
|
||||
<div className={styles.dataSourceList}>
|
||||
<CustomScrollbar>
|
||||
{recentlyUsed
|
||||
.map((uid) => filteredDataSources.find((ds) => ds.uid === uid))
|
||||
.map((ds) => {
|
||||
if (!ds) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<DataSourceCard
|
||||
selected={isDataSourceMatch(ds, current)}
|
||||
key={ds.uid}
|
||||
ds={ds}
|
||||
onChange={changeCallback}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{recentlyUsed && recentlyUsed.length > 0 && <hr />}
|
||||
{filteredDataSources.map((ds) => (
|
||||
<DataSourceCard
|
||||
selected={isDataSourceMatch(ds, current)}
|
||||
key={ds.uid}
|
||||
ds={ds}
|
||||
onChange={changeCallback}
|
||||
/>
|
||||
))}
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
{enableFileUpload && (
|
||||
<div className={styles.additionalContent}>
|
||||
<FileDropzone
|
||||
readAs="readAsArrayBuffer"
|
||||
fileListRenderer={() => undefined}
|
||||
options={{
|
||||
...fileUploadOptions,
|
||||
onDrop: (...args) => {
|
||||
onDismiss();
|
||||
fileUploadOptions?.onDrop?.(...args);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<FileDropzoneDefaultChildren primaryText={'Upload file'} />
|
||||
</FileDropzone>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
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<DataSourceJsonData> | undefined,
|
||||
current: string | DataSourceInstanceSettings<DataSourceJsonData> | 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;
|
||||
}
|
@ -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<DataSourceJsonData>;
|
||||
}
|
||||
|
||||
export function DataSourceCard(props: DataSourceCardProps) {
|
||||
const { selected, ds, onChange } = props;
|
||||
const styles = useStyles2(getStyles);
|
||||
return (
|
||||
<Card className={selected ? styles.selectedDataSource : undefined} key={ds.uid} onClick={() => onChange(ds.uid)}>
|
||||
<Card.Figure>
|
||||
<img alt={`${ds.meta.name} logo`} src={ds.meta.info.logos.large}></img>
|
||||
</Card.Figure>
|
||||
<Card.Meta>
|
||||
{[ds.meta.name, ds.url, ds.isDefault && <Tag key="default-tag" name={'default'} colorIndex={1} />]}
|
||||
</Card.Meta>
|
||||
<Card.Tags>
|
||||
<PluginSignatureBadge status={ds.meta.signature} />
|
||||
</Card.Tags>
|
||||
<Card.Heading>{ds.name}</Card.Heading>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
selectedDataSource: css`
|
||||
background-color: ${theme.colors.emphasize(theme.colors.background.secondary, 0.1)};
|
||||
`,
|
||||
};
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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<DataSourceJsonData> | string | DataSourceRef | null | undefined;
|
||||
}
|
||||
|
||||
export function DataSourceDisplay(props: DataSourceDisplayProps) {
|
||||
export function DataSourceLogo(props: DataSourceLogoProps) {
|
||||
const { dataSource } = props;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
if (!dataSource) {
|
||||
return <span>Unknown</span>;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof dataSource === 'string') {
|
||||
return <span>${dataSource} - not found</span>;
|
||||
return null;
|
||||
}
|
||||
|
||||
if ('name' in dataSource) {
|
||||
return (
|
||||
<>
|
||||
<img
|
||||
className={styles.pickerDSLogo}
|
||||
alt={`${dataSource.meta.name} logo`}
|
||||
src={dataSource.meta.info.logos.small}
|
||||
></img>
|
||||
<span>{dataSource.name}</span>
|
||||
</>
|
||||
<img
|
||||
className={styles.pickerDSLogo}
|
||||
alt={`${dataSource.meta.name} logo`}
|
||||
src={dataSource.meta.info.logos.small}
|
||||
></img>
|
||||
);
|
||||
}
|
||||
|
||||
return <span>{dataSource.uid} - not found</span>;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function DataSourceLogoPlaceHolder() {
|
||||
const styles = useStyles2(getStyles);
|
||||
return <div className={styles.pickerDSLogo}></div>;
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
@ -42,7 +44,6 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
pickerDSLogo: css`
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin-right: ${theme.spacing(1)};
|
||||
`,
|
||||
};
|
||||
}
|
@ -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} />
|
||||
);
|
||||
}
|
@ -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<DataSourcePickerProps, DataS
|
||||
}
|
||||
}
|
||||
|
||||
onChange = (ds?: string) => {
|
||||
const dsSettings = this.dataSourceSrv.getInstanceSettings(ds);
|
||||
|
||||
if (dsSettings) {
|
||||
this.props.onChange(dsSettings);
|
||||
this.setState({ error: undefined });
|
||||
}
|
||||
onChange = (ds: DataSourceInstanceSettings<DataSourceJsonData>) => {
|
||||
this.props.onChange(ds);
|
||||
this.setState({ error: undefined });
|
||||
};
|
||||
|
||||
private getCurrentDs(): DataSourceInstanceSettings<DataSourceJsonData> | string | DataSourceRef | null | undefined {
|
||||
@ -80,17 +76,18 @@ export class DataSourcePicker extends PureComponent<DataSourcePickerProps, DataS
|
||||
}
|
||||
|
||||
render() {
|
||||
const { recentlyUsed, fileUploadOptions, enableFileUpload } = this.props;
|
||||
const { recentlyUsed, fileUploadOptions, enableFileUpload, onClickAddCSV } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DataSourceDrawer
|
||||
<DataSourceDropdown
|
||||
datasources={this.getDatasources()}
|
||||
onChange={this.onChange}
|
||||
recentlyUsed={recentlyUsed}
|
||||
current={this.getCurrentDs()}
|
||||
fileUploadOptions={fileUploadOptions}
|
||||
enableFileUpload={enableFileUpload}
|
||||
onClickAddCSV={onClickAddCSV}
|
||||
/>
|
||||
</div>
|
||||
);
|
@ -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';
|
@ -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<DataSourceInstanceSettings<DataSourceJsonData>>;
|
||||
onFileDrop?: () => void;
|
||||
onChange: (ds: string) => void;
|
||||
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;
|
||||
}
|
||||
|
||||
@ -40,6 +44,7 @@ export interface DataSourcePickerProps {
|
||||
disabled?: boolean;
|
||||
enableFileUpload?: boolean;
|
||||
fileUploadOptions?: DropzoneOptions;
|
||||
onClickAddCSV?: () => void;
|
||||
}
|
||||
|
||||
export interface DataSourcePickerWithHistoryProps extends Omit<DataSourcePickerProps, 'recentlyUsed'> {
|
@ -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();
|
||||
});
|
||||
});
|
39
public/app/features/datasources/components/picker/utils.ts
Normal file
39
public/app/features/datasources/components/picker/utils.ts
Normal file
@ -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';
|
||||
}
|
@ -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<Props, State> {
|
||||
Data source
|
||||
</InlineFormLabel>
|
||||
<div className={styles.dataSourceRowItem}>
|
||||
{config.featureToggles.advancedDataSourcePicker ? (
|
||||
<DataSourcePickerWithHistory
|
||||
onChange={this.onChangeDataSource}
|
||||
current={options.dataSource}
|
||||
metrics={true}
|
||||
mixed={true}
|
||||
dashboard={true}
|
||||
variables={true}
|
||||
enableFileUpload={config.featureToggles.editPanelCSVDragAndDrop}
|
||||
fileUploadOptions={{
|
||||
onDrop: this.onFileDrop,
|
||||
maxSize: DFImport.maxFileSize,
|
||||
multiple: false,
|
||||
accept: DFImport.acceptedFiles,
|
||||
}}
|
||||
></DataSourcePickerWithHistory>
|
||||
) : (
|
||||
<DataSourcePicker
|
||||
onChange={this.onChangeDataSource}
|
||||
current={options.dataSource}
|
||||
metrics={true}
|
||||
mixed={true}
|
||||
dashboard={true}
|
||||
variables={true}
|
||||
></DataSourcePicker>
|
||||
)}
|
||||
<DataSourcePicker
|
||||
onChange={this.onChangeDataSource}
|
||||
current={options.dataSource}
|
||||
metrics={true}
|
||||
mixed={true}
|
||||
dashboard={true}
|
||||
variables={true}
|
||||
enableFileUpload={config.featureToggles.editPanelCSVDragAndDrop}
|
||||
fileUploadOptions={{
|
||||
onDrop: this.onFileDrop,
|
||||
maxSize: DFImport.maxFileSize,
|
||||
multiple: false,
|
||||
accept: DFImport.acceptedFiles,
|
||||
}}
|
||||
onClickAddCSV={this.onClickAddCSV}
|
||||
/>
|
||||
</div>
|
||||
{dataSource && (
|
||||
<>
|
||||
@ -315,6 +305,24 @@ export class QueryGroup extends PureComponent<Props, State> {
|
||||
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[] = [];
|
||||
|
@ -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"
|
||||
|
@ -419,7 +419,9 @@ export class UnthemedQueryEditor extends PureComponent<Props, State> {
|
||||
accept: DFImport.acceptedFiles,
|
||||
}}
|
||||
>
|
||||
<FileDropzoneDefaultChildren primaryText={this.props?.query?.file ? 'Replace file' : 'Upload file'} />
|
||||
<FileDropzoneDefaultChildren
|
||||
primaryText={this.props?.query?.file ? 'Replace file' : 'Drop file here or click to upload'}
|
||||
/>
|
||||
</FileDropzone>
|
||||
{file && (
|
||||
<div className={styles.file}>
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user