mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Improve empty state when no ds picker were found (#67422)
This commit is contained in:
parent
e03a8b6826
commit
1afaf4d73e
@ -1,8 +1,6 @@
|
|||||||
{
|
{
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"useWorkspaces": true,
|
"useWorkspaces": true,
|
||||||
"packages": [
|
"packages": ["packages/*"],
|
||||||
"packages/*"
|
|
||||||
],
|
|
||||||
"version": "10.1.0-pre"
|
"version": "10.1.0-pre"
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { LinkButton, ButtonVariant } from '@grafana/ui';
|
||||||
|
import { config } from 'app/core/config';
|
||||||
|
import { contextSrv } from 'app/core/core';
|
||||||
|
import { ROUTES as CONNECTIONS_ROUTES } from 'app/features/connections/constants';
|
||||||
|
import { DATASOURCES_ROUTES } from 'app/features/datasources/constants';
|
||||||
|
import { AccessControlAction } from 'app/types';
|
||||||
|
|
||||||
|
interface AddNewDataSourceButtonProps {
|
||||||
|
onClick?: () => void;
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AddNewDataSourceButton({ variant, onClick }: AddNewDataSourceButtonProps) {
|
||||||
|
const hasCreateRights = contextSrv.hasPermission(AccessControlAction.DataSourcesCreate);
|
||||||
|
const newDataSourceURL = config.featureToggles.dataConnectionsConsole
|
||||||
|
? CONNECTIONS_ROUTES.DataSourcesNew
|
||||||
|
: DATASOURCES_ROUTES.New;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LinkButton
|
||||||
|
variant={variant || 'primary'}
|
||||||
|
href={newDataSourceURL}
|
||||||
|
disabled={!hasCreateRights}
|
||||||
|
tooltip={!hasCreateRights ? 'You do not have permission to configure new data sources' : undefined}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
Configure a new data source
|
||||||
|
</LinkButton>
|
||||||
|
);
|
||||||
|
}
|
@ -25,6 +25,7 @@ const INTERACTION_ITEM = {
|
|||||||
SELECT_DS: 'select_ds',
|
SELECT_DS: 'select_ds',
|
||||||
ADD_FILE: 'add_file',
|
ADD_FILE: 'add_file',
|
||||||
OPEN_ADVANCED_DS_PICKER: 'open_advanced_ds_picker',
|
OPEN_ADVANCED_DS_PICKER: 'open_advanced_ds_picker',
|
||||||
|
CONFIG_NEW_DS_EMPTY_STATE: 'config_new_ds_empty_state',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function DataSourceDropdown(props: DataSourceDropdownProps) {
|
export function DataSourceDropdown(props: DataSourceDropdownProps) {
|
||||||
@ -205,16 +206,19 @@ const PickerContent = React.forwardRef<HTMLDivElement, PickerContentProps>((prop
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={props.style} ref={ref} className={styles.container}>
|
<div style={props.style} ref={ref} className={styles.container}>
|
||||||
<div className={styles.dataSourceList}>
|
<DataSourceList
|
||||||
<DataSourceList
|
{...props}
|
||||||
{...props}
|
enableKeyboardNavigation
|
||||||
enableKeyboardNavigation
|
className={styles.dataSourceList}
|
||||||
current={current}
|
current={current}
|
||||||
onChange={changeCallback}
|
onChange={changeCallback}
|
||||||
filter={(ds) => matchDataSourceWithSearch(ds, filterTerm)}
|
filter={(ds) => matchDataSourceWithSearch(ds, filterTerm)}
|
||||||
></DataSourceList>
|
onClickEmptyStateCTA={() =>
|
||||||
</div>
|
reportInteraction(INTERACTION_EVENT_NAME, {
|
||||||
|
item: INTERACTION_ITEM.CONFIG_NEW_DS_EMPTY_STATE,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
></DataSourceList>
|
||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
{onClickAddCSV && config.featureToggles.editPanelCSVDragAndDrop && (
|
{onClickAddCSV && config.featureToggles.editPanelCSVDragAndDrop && (
|
||||||
<Button variant="secondary" size="sm" onClick={clickAddCSVCallback}>
|
<Button variant="secondary" size="sm" onClick={clickAddCSVCallback}>
|
||||||
|
@ -4,10 +4,11 @@ import { Observable } from 'rxjs';
|
|||||||
|
|
||||||
import { DataSourceInstanceSettings, DataSourceRef, GrafanaTheme2 } from '@grafana/data';
|
import { DataSourceInstanceSettings, DataSourceRef, GrafanaTheme2 } from '@grafana/data';
|
||||||
import { getTemplateSrv } from '@grafana/runtime';
|
import { getTemplateSrv } from '@grafana/runtime';
|
||||||
import { useTheme2 } from '@grafana/ui';
|
import { useStyles2, useTheme2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { useDatasources, useKeyboardNavigatableList, useRecentlyUsedDataSources } from '../../hooks';
|
import { useDatasources, useKeyboardNavigatableList, useRecentlyUsedDataSources } from '../../hooks';
|
||||||
|
|
||||||
|
import { AddNewDataSourceButton } from './AddNewDataSourceButton';
|
||||||
import { DataSourceCard } from './DataSourceCard';
|
import { DataSourceCard } from './DataSourceCard';
|
||||||
import { getDataSourceCompareFn, isDataSourceMatch } from './utils';
|
import { getDataSourceCompareFn, isDataSourceMatch } from './utils';
|
||||||
|
|
||||||
@ -37,6 +38,7 @@ export interface DataSourceListProps {
|
|||||||
inputId?: string;
|
inputId?: string;
|
||||||
filter?: (dataSource: DataSourceInstanceSettings) => boolean;
|
filter?: (dataSource: DataSourceInstanceSettings) => boolean;
|
||||||
onClear?: () => void;
|
onClear?: () => void;
|
||||||
|
onClickEmptyStateCTA?: () => void;
|
||||||
enableKeyboardNavigation?: boolean;
|
enableKeyboardNavigation?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,7 +53,7 @@ export function DataSourceList(props: DataSourceListProps) {
|
|||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = getStyles(theme, selectedItemCssSelector);
|
const styles = getStyles(theme, selectedItemCssSelector);
|
||||||
|
|
||||||
const { className, current, onChange, enableKeyboardNavigation } = props;
|
const { className, current, onChange, enableKeyboardNavigation, onClickEmptyStateCTA } = props;
|
||||||
// QUESTION: Should we use data from the Redux store as admin DS view does?
|
// QUESTION: Should we use data from the Redux store as admin DS view does?
|
||||||
const dataSources = useDatasources({
|
const dataSources = useDatasources({
|
||||||
alerting: props.alerting,
|
alerting: props.alerting,
|
||||||
@ -67,11 +69,14 @@ export function DataSourceList(props: DataSourceListProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [recentlyUsedDataSources, pushRecentlyUsedDataSource] = useRecentlyUsedDataSources();
|
const [recentlyUsedDataSources, pushRecentlyUsedDataSource] = useRecentlyUsedDataSources();
|
||||||
|
const filteredDataSources = props.filter ? dataSources.filter(props.filter) : dataSources;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className={cx(className, styles.container)}>
|
<div ref={containerRef} className={cx(className, styles.container)}>
|
||||||
{dataSources
|
{filteredDataSources.length === 0 && (
|
||||||
.filter((ds) => (props.filter ? props.filter(ds) : true))
|
<EmptyState className={styles.emptyState} onClickCTA={onClickEmptyStateCTA} />
|
||||||
|
)}
|
||||||
|
{filteredDataSources
|
||||||
.sort(getDataSourceCompareFn(current, recentlyUsedDataSources, getDataSourceVariableIDs()))
|
.sort(getDataSourceCompareFn(current, recentlyUsedDataSources, getDataSourceVariableIDs()))
|
||||||
.map((ds) => (
|
.map((ds) => (
|
||||||
<DataSourceCard
|
<DataSourceCard
|
||||||
@ -89,6 +94,30 @@ export function DataSourceList(props: DataSourceListProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function EmptyState({ className, onClickCTA }: { className?: string; onClickCTA?: () => void }) {
|
||||||
|
const styles = useStyles2(getEmptyStateStyles);
|
||||||
|
return (
|
||||||
|
<div className={cx(className, styles.container)}>
|
||||||
|
<p className={styles.message}>No data sources found</p>
|
||||||
|
<AddNewDataSourceButton onClick={onClickCTA} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEmptyStateStyles(theme: GrafanaTheme2) {
|
||||||
|
return {
|
||||||
|
container: css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
`,
|
||||||
|
message: css`
|
||||||
|
margin-bottom: ${theme.spacing(3)};
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function getDataSourceVariableIDs() {
|
function getDataSourceVariableIDs() {
|
||||||
const templateSrv = getTemplateSrv();
|
const templateSrv = getTemplateSrv();
|
||||||
/** Unforunately there is no easy way to identify data sources that are variables. The uid of the data source will be the name of the variable in a templating syntax $([name]) **/
|
/** Unforunately there is no easy way to identify data sources that are variables. The uid of the data source will be the name of the variable in a templating syntax $([name]) **/
|
||||||
@ -101,9 +130,15 @@ function getDataSourceVariableIDs() {
|
|||||||
function getStyles(theme: GrafanaTheme2, selectedItemCssSelector: string) {
|
function getStyles(theme: GrafanaTheme2, selectedItemCssSelector: string) {
|
||||||
return {
|
return {
|
||||||
container: css`
|
container: css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
${selectedItemCssSelector} {
|
${selectedItemCssSelector} {
|
||||||
background-color: ${theme.colors.background.secondary};
|
background-color: ${theme.colors.background.secondary};
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
emptyState: css`
|
||||||
|
height: 100%;
|
||||||
|
flex: 1;
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -10,18 +10,13 @@ import {
|
|||||||
FileDropzone,
|
FileDropzone,
|
||||||
FileDropzoneDefaultChildren,
|
FileDropzoneDefaultChildren,
|
||||||
CustomScrollbar,
|
CustomScrollbar,
|
||||||
LinkButton,
|
|
||||||
useStyles2,
|
useStyles2,
|
||||||
Input,
|
Input,
|
||||||
Icon,
|
Icon,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import { config } from 'app/core/config';
|
|
||||||
import { contextSrv } from 'app/core/core';
|
|
||||||
import { ROUTES as CONNECTIONS_ROUTES } from 'app/features/connections/constants';
|
|
||||||
import * as DFImport from 'app/features/dataframe-import';
|
import * as DFImport from 'app/features/dataframe-import';
|
||||||
import { DATASOURCES_ROUTES } from 'app/features/datasources/constants';
|
|
||||||
import { AccessControlAction } from 'app/types';
|
|
||||||
|
|
||||||
|
import { AddNewDataSourceButton } from './AddNewDataSourceButton';
|
||||||
import { DataSourceList } from './DataSourceList';
|
import { DataSourceList } from './DataSourceList';
|
||||||
import { matchDataSourceWithSearch } from './utils';
|
import { matchDataSourceWithSearch } from './utils';
|
||||||
|
|
||||||
@ -30,6 +25,7 @@ const INTERACTION_ITEM = {
|
|||||||
SELECT_DS: 'select_ds',
|
SELECT_DS: 'select_ds',
|
||||||
UPLOAD_FILE: 'upload_file',
|
UPLOAD_FILE: 'upload_file',
|
||||||
CONFIG_NEW_DS: 'config_new_ds',
|
CONFIG_NEW_DS: 'config_new_ds',
|
||||||
|
CONFIG_NEW_DS_EMPTY_STATE: 'config_new_ds_empty_state',
|
||||||
SEARCH: 'search',
|
SEARCH: 'search',
|
||||||
DISMISS: 'dismiss',
|
DISMISS: 'dismiss',
|
||||||
};
|
};
|
||||||
@ -54,11 +50,7 @@ export function DataSourceModal({
|
|||||||
}: DataSourceModalProps) {
|
}: DataSourceModalProps) {
|
||||||
const styles = useStyles2(getDataSourceModalStyles);
|
const styles = useStyles2(getDataSourceModalStyles);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const hasCreateRights = contextSrv.hasPermission(AccessControlAction.DataSourcesCreate);
|
|
||||||
const analyticsInteractionSrc = reportedInteractionFrom || 'modal';
|
const analyticsInteractionSrc = reportedInteractionFrom || 'modal';
|
||||||
const newDataSourceURL = config.featureToggles.dataConnectionsConsole
|
|
||||||
? CONNECTIONS_ROUTES.DataSourcesNew
|
|
||||||
: DATASOURCES_ROUTES.New;
|
|
||||||
|
|
||||||
const onDismissModal = () => {
|
const onDismissModal = () => {
|
||||||
onDismiss();
|
onDismiss();
|
||||||
@ -106,12 +98,19 @@ export function DataSourceModal({
|
|||||||
/>
|
/>
|
||||||
<CustomScrollbar>
|
<CustomScrollbar>
|
||||||
<DataSourceList
|
<DataSourceList
|
||||||
|
className={styles.dataSourceList}
|
||||||
dashboard={false}
|
dashboard={false}
|
||||||
mixed={false}
|
mixed={false}
|
||||||
variables
|
variables
|
||||||
filter={(ds) => matchDataSourceWithSearch(ds, search) && !ds.meta.builtIn}
|
filter={(ds) => matchDataSourceWithSearch(ds, search) && !ds.meta.builtIn}
|
||||||
onChange={onChangeDataSource}
|
onChange={onChangeDataSource}
|
||||||
current={current}
|
current={current}
|
||||||
|
onClickEmptyStateCTA={() =>
|
||||||
|
reportInteraction(INTERACTION_EVENT_NAME, {
|
||||||
|
item: INTERACTION_ITEM.CONFIG_NEW_DS_EMPTY_STATE,
|
||||||
|
src: analyticsInteractionSrc,
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</CustomScrollbar>
|
</CustomScrollbar>
|
||||||
</div>
|
</div>
|
||||||
@ -149,20 +148,15 @@ export function DataSourceModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.dsCTAs}>
|
<div className={styles.dsCTAs}>
|
||||||
<LinkButton
|
<AddNewDataSourceButton
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
href={newDataSourceURL}
|
|
||||||
disabled={!hasCreateRights}
|
|
||||||
tooltip={!hasCreateRights ? 'You do not have permission to configure new data sources' : undefined}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
reportInteraction(INTERACTION_EVENT_NAME, {
|
reportInteraction(INTERACTION_EVENT_NAME, {
|
||||||
item: INTERACTION_ITEM.CONFIG_NEW_DS,
|
item: INTERACTION_ITEM.CONFIG_NEW_DS,
|
||||||
src: analyticsInteractionSrc,
|
src: analyticsInteractionSrc,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
Configure a new data source
|
|
||||||
</LinkButton>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
@ -203,6 +197,9 @@ function getDataSourceModalStyles(theme: GrafanaTheme2) {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
margin-bottom: ${theme.spacing(4)};
|
margin-bottom: ${theme.spacing(4)};
|
||||||
`,
|
`,
|
||||||
|
dataSourceList: css`
|
||||||
|
height: 100%;
|
||||||
|
`,
|
||||||
builtInDataSourceList: css`
|
builtInDataSourceList: css`
|
||||||
margin-bottom: ${theme.spacing(4)};
|
margin-bottom: ${theme.spacing(4)};
|
||||||
`,
|
`,
|
||||||
|
Loading…
Reference in New Issue
Block a user