Files
Ashley Harrison 192423956b GrafanaUI: Mark ScrollContainer as stable and deprecate CustomScrollbar (#96331)
* deprecate CustomScrollbar, promote ScrollContainer

* add documentation start

* use the properly exported ScrollContainer

* Update packages/grafana-ui/src/components/ScrollContainer/ScrollContainer.mdx

Co-authored-by: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com>

---------

Co-authored-by: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com>
2024-11-14 12:06:21 +00:00

332 lines
9.1 KiB
TypeScript

import { css } from '@emotion/css';
import { once } from 'lodash';
import { useMemo, useState } from 'react';
import { DataSourceInstanceSettings, DataSourceRef, GrafanaTheme2 } from '@grafana/data';
import { config, reportInteraction } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import {
Modal,
FileDropzone,
FileDropzoneDefaultChildren,
useStyles2,
Input,
Icon,
ScrollContainer,
} from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import * as DFImport from 'app/features/dataframe-import';
import { GrafanaQuery } from 'app/plugins/datasource/grafana/types';
import { getFileDropToQueryHandler } from 'app/plugins/datasource/grafana/utils';
import { useDatasource } from '../../hooks';
import { AddNewDataSourceButton } from './AddNewDataSourceButton';
import { BuiltInDataSourceList } from './BuiltInDataSourceList';
import { DataSourceList } from './DataSourceList';
import { matchDataSourceWithSearch } from './utils';
const INTERACTION_EVENT_NAME = 'dashboards_dspickermodal_clicked';
const INTERACTION_ITEM = {
SELECT_DS: 'select_ds',
UPLOAD_FILE: 'upload_file',
CONFIG_NEW_DS: 'config_new_ds',
CONFIG_NEW_DS_EMPTY_STATE: 'config_new_ds_empty_state',
SEARCH: 'search',
DISMISS: 'dismiss',
};
export interface DataSourceModalProps {
onChange: (ds: DataSourceInstanceSettings, defaultQueries?: DataQuery[] | GrafanaQuery[]) => void;
current: DataSourceRef | string | null | undefined;
onDismiss: () => void;
recentlyUsed?: string[];
reportedInteractionFrom?: string;
// DS filters
filter?: (ds: DataSourceInstanceSettings) => boolean;
tracing?: boolean;
mixed?: boolean;
dashboard?: boolean;
metrics?: boolean;
type?: string | string[];
annotations?: boolean;
variables?: boolean;
alerting?: boolean;
pluginId?: string;
logs?: boolean;
uploadFile?: boolean;
}
export function DataSourceModal({
tracing,
dashboard,
mixed,
metrics,
type,
annotations,
variables,
alerting,
pluginId,
logs,
uploadFile,
filter,
onChange,
current,
onDismiss,
reportedInteractionFrom,
}: DataSourceModalProps) {
const styles = useStyles2(getDataSourceModalStyles);
const [search, setSearch] = useState('');
const analyticsInteractionSrc = reportedInteractionFrom || 'modal';
const onDismissModal = () => {
onDismiss();
reportInteraction(INTERACTION_EVENT_NAME, { item: INTERACTION_ITEM.DISMISS, src: analyticsInteractionSrc });
};
const onChangeDataSource = (ds: DataSourceInstanceSettings) => {
onChange(ds);
reportInteraction(INTERACTION_EVENT_NAME, {
item: INTERACTION_ITEM.SELECT_DS,
ds_type: ds.type,
src: analyticsInteractionSrc,
});
};
// Memoizing to keep once() cached so it avoids reporting multiple times
const reportSearchUsageOnce = useMemo(
() =>
once(() => {
reportInteraction(INTERACTION_EVENT_NAME, { item: 'search', src: analyticsInteractionSrc });
}),
[analyticsInteractionSrc]
);
const grafanaDS = useDatasource('-- Grafana --');
const onFileDrop = getFileDropToQueryHandler((query, fileRejections) => {
if (!grafanaDS) {
return;
}
onChange(grafanaDS, [query]);
reportInteraction(INTERACTION_EVENT_NAME, {
item: INTERACTION_ITEM.UPLOAD_FILE,
src: analyticsInteractionSrc,
});
if (fileRejections.length < 1) {
onDismiss();
}
});
// Built-in data sources used twice because of mobile layout adjustments
// In movile the list is appended to the bottom of the DS list
const BuiltInList = ({ className }: { className?: string }) => {
return (
<BuiltInDataSourceList
className={className}
onChange={onChangeDataSource}
current={current}
filter={filter}
variables={variables}
tracing={tracing}
metrics={metrics}
type={type}
annotations={annotations}
alerting={alerting}
pluginId={pluginId}
logs={logs}
dashboard={dashboard}
mixed={mixed}
/>
);
};
return (
<Modal
title={t('data-source-picker.modal.title', 'Select data source')}
closeOnEscape={true}
closeOnBackdropClick={true}
isOpen={true}
className={styles.modal}
contentClassName={styles.modalContent}
onClickBackdrop={onDismissModal}
onDismiss={onDismissModal}
>
<div className={styles.leftColumn}>
<Input
type="search"
autoFocus
className={styles.searchInput}
value={search}
prefix={<Icon name="search" />}
placeholder={t('data-source-picker.modal.input-placeholder', 'Select data source')}
onChange={(e) => {
setSearch(e.currentTarget.value);
reportSearchUsageOnce();
}}
/>
<ScrollContainer>
<DataSourceList
onChange={onChangeDataSource}
current={current}
onClickEmptyStateCTA={() =>
reportInteraction(INTERACTION_EVENT_NAME, {
item: INTERACTION_ITEM.CONFIG_NEW_DS_EMPTY_STATE,
src: analyticsInteractionSrc,
})
}
filter={(ds) => (filter ? filter?.(ds) : true) && matchDataSourceWithSearch(ds, search) && !ds.meta.builtIn}
variables={variables}
tracing={tracing}
metrics={metrics}
type={type}
annotations={annotations}
alerting={alerting}
pluginId={pluginId}
logs={logs}
dashboard={dashboard}
mixed={mixed}
/>
<BuiltInList className={styles.appendBuiltInDataSourcesList} />
</ScrollContainer>
</div>
<div className={styles.rightColumn}>
<div className={styles.builtInDataSources}>
<div className={styles.builtInDataSourcesList}>
<ScrollContainer>
<BuiltInList />
</ScrollContainer>
</div>
{uploadFile && config.featureToggles.editPanelCSVDragAndDrop && (
<FileDropzone
readAs="readAsArrayBuffer"
fileListRenderer={() => undefined}
options={{
maxSize: DFImport.maxFileSize,
multiple: false,
accept: DFImport.acceptedFiles,
onDrop: onFileDrop,
}}
>
<FileDropzoneDefaultChildren />
</FileDropzone>
)}
</div>
<div className={styles.newDSSection}>
<span className={styles.newDSDescription}>
<Trans i18nKey="data-source-picker.modal.configure-new-data-source">
Open a new tab and configure a data source
</Trans>
</span>
<AddNewDataSourceButton
variant="secondary"
onClick={() => {
reportInteraction(INTERACTION_EVENT_NAME, {
item: INTERACTION_ITEM.CONFIG_NEW_DS,
src: analyticsInteractionSrc,
});
onDismiss();
}}
/>
</div>
</div>
</Modal>
);
}
function getDataSourceModalStyles(theme: GrafanaTheme2) {
return {
modal: css({
width: '80%',
height: '80%',
maxWidth: '1200px',
maxHeight: '900px',
[theme.breakpoints.down('md')]: {
width: '100%',
},
}),
modalContent: css({
display: 'flex',
flexDirection: 'row',
height: '100%',
[theme.breakpoints.down('md')]: {
flexDirection: 'column',
},
}),
leftColumn: css({
display: 'flex',
flexDirection: 'column',
width: '50%',
height: '100%',
paddingRight: theme.spacing(4),
borderRight: `1px solid ${theme.colors.border.weak}`,
[theme.breakpoints.down('md')]: {
width: '100%',
borderRight: 0,
paddingRight: 0,
flex: 1,
overflowY: 'auto',
},
}),
rightColumn: css({
display: 'flex',
flexDirection: 'column',
width: '50%',
height: '100%',
justifyItems: 'space-evenly',
alignItems: 'stretch',
paddingLeft: theme.spacing(4),
[theme.breakpoints.down('md')]: {
width: '100%',
paddingLeft: 0,
flex: 0,
},
}),
builtInDataSources: css({
flex: '1 1',
marginBottom: theme.spacing(4),
[theme.breakpoints.down('md')]: {
flex: 0,
},
}),
builtInDataSourcesList: css({
[theme.breakpoints.down('md')]: {
display: 'none',
marginBottom: 0,
},
marginBottom: theme.spacing(4),
}),
appendBuiltInDataSourcesList: css({
[theme.breakpoints.up('md')]: {
display: 'none',
},
}),
newDSSection: css({
display: 'flex',
flexDirection: 'row',
width: '100%',
justifyContent: 'space-between',
alignItems: 'center',
}),
newDSDescription: css({
flex: '1 0',
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
color: theme.colors.text.secondary,
}),
searchInput: css({
width: '100%',
minHeight: '32px',
marginBottom: theme.spacing(1),
}),
};
}