Plugins: moving the DataSourcePicker to grafana/runtime so it can be reused in plugins (#31628)

* moved the datasource picker to grafana-runtime.

* fixed imports.

* added e2e selectors as an external package.

* adding react as external package.

* exposing dependent types for DataSourcePicker.

* added docs for ui components.

* moving component to components.
This commit is contained in:
Marcus Andersson
2021-03-18 10:44:26 +01:00
committed by GitHub
parent 8fafe95728
commit 69201bbf8c
23 changed files with 124 additions and 61 deletions

View File

@@ -1,151 +0,0 @@
// Libraries
import React, { PureComponent } from 'react';
// Components
import { HorizontalGroup, Select } from '@grafana/ui';
import { DataSourceInstanceSettings, SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { isUnsignedPluginSignature, PluginSignatureBadge } from '../../../features/plugins/PluginSignatureBadge';
import { getDataSourceSrv } from '@grafana/runtime';
export interface Props {
onChange: (ds: DataSourceInstanceSettings) => void;
current: string | null;
hideTextValue?: boolean;
onBlur?: () => void;
autoFocus?: boolean;
openMenuOnFocus?: boolean;
placeholder?: string;
tracing?: boolean;
mixed?: boolean;
dashboard?: boolean;
metrics?: boolean;
annotations?: boolean;
variables?: boolean;
pluginId?: string;
noDefault?: boolean;
}
export interface State {
error?: string;
}
export class DataSourcePicker extends PureComponent<Props, State> {
dataSourceSrv = getDataSourceSrv();
static defaultProps: Partial<Props> = {
autoFocus: false,
openMenuOnFocus: false,
placeholder: 'Select datasource',
};
state: State = {};
constructor(props: Props) {
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: SelectableValue<string>) => {
const dsSettings = this.dataSourceSrv.getInstanceSettings(item.value);
if (dsSettings) {
this.props.onChange(dsSettings);
this.setState({ error: undefined });
}
};
private getCurrentValue() {
const { current, hideTextValue, noDefault } = this.props;
if (!current && noDefault) {
return null;
}
const ds = this.dataSourceSrv.getInstanceSettings(current);
if (ds) {
return {
label: ds.name.substr(0, 37),
value: ds.name,
imgUrl: ds.meta.info.logos.small,
hideText: hideTextValue,
meta: ds.meta,
};
}
return {
label: (current ?? 'no name') + ' - not found',
value: current,
imgUrl: '',
hideText: hideTextValue,
};
}
getDataSourceOptions() {
const { tracing, metrics, mixed, dashboard, variables, annotations, pluginId } = this.props;
const options = this.dataSourceSrv
.getList({
tracing,
metrics,
dashboard,
mixed,
variables,
annotations,
pluginId,
})
.map((ds) => ({
value: ds.name,
label: `${ds.name}${ds.isDefault ? ' (default)' : ''}`,
imgUrl: ds.meta.info.logos.small,
meta: ds.meta,
}));
return options;
}
render() {
const { autoFocus, onBlur, openMenuOnFocus, placeholder } = this.props;
const { error } = this.state;
const options = this.getDataSourceOptions();
const value = this.getCurrentValue();
return (
<div aria-label={selectors.components.DataSourcePicker.container}>
<Select
className="ds-picker select-container"
isMulti={false}
isClearable={false}
backspaceRemovesValue={false}
onChange={this.onChange}
options={options}
autoFocus={autoFocus}
onBlur={onBlur}
openMenuOnFocus={openMenuOnFocus}
maxMenuHeight={500}
placeholder={placeholder}
noOptionsMessage="No datasources found"
value={value}
invalid={!!error}
getOptionLabel={(o) => {
if (o.meta && isUnsignedPluginSignature(o.meta.signature) && o !== value) {
return (
<HorizontalGroup align="center" justify="space-between">
<span>{o.label}</span> <PluginSignatureBadge status={o.meta.signature} />
</HorizontalGroup>
);
}
return o.label || '';
}}
/>
</div>
);
}
}

View File

@@ -4,10 +4,10 @@ import {
GrafanaTheme,
updateDatasourcePluginJsonDataOption,
} from '@grafana/data';
import { DataSourcePicker } from '@grafana/runtime';
import { InlineFormLabel, TagsInput, useStyles } from '@grafana/ui';
import { css } from 'emotion';
import React from 'react';
import { DataSourcePicker } from './Select/DataSourcePicker';
export interface TraceToLogsOptions {
datasourceUid?: string;

View File

@@ -1,9 +1,8 @@
import React from 'react';
import { GrafanaTheme, PanelPluginMeta, PluginState } from '@grafana/data';
import { Badge, BadgeProps, styleMixins, stylesFactory, useTheme } from '@grafana/ui';
import { GrafanaTheme, isUnsignedPluginSignature, PanelPluginMeta, PluginState } from '@grafana/data';
import { Badge, BadgeProps, PluginSignatureBadge, styleMixins, stylesFactory, useTheme } from '@grafana/ui';
import { css, cx } from 'emotion';
import { selectors } from '@grafana/e2e-selectors';
import { isUnsignedPluginSignature, PluginSignatureBadge } from '../../../plugins/PluginSignatureBadge';
interface Props {
isCurrent: boolean;

View File

@@ -2,7 +2,7 @@ import React, { FC, PureComponent } from 'react';
import { connect } from 'react-redux';
import { hot } from 'react-hot-loader';
import { DataSourcePluginMeta, NavModel } from '@grafana/data';
import { Button, LinkButton, List } from '@grafana/ui';
import { Button, LinkButton, List, PluginSignatureBadge } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors';
import Page from 'app/core/components/Page/Page';
@@ -11,7 +11,6 @@ import { addDataSource, loadDataSourcePlugins } from './state/actions';
import { getDataSourcePlugins } from './state/selectors';
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
import { setDataSourceTypeSearchQuery } from './state/reducers';
import { PluginSignatureBadge } from '../plugins/PluginSignatureBadge';
import { Card } from 'app/core/components/Card/Card';
import { PluginsErrorsInfo } from '../plugins/PluginsErrorsInfo';

View File

@@ -7,7 +7,7 @@ import { css } from 'emotion';
import { ExploreId, ExploreItemState } from 'app/types/explore';
import { Icon, IconButton, SetInterval, ToolbarButton, ToolbarButtonRow, Tooltip } from '@grafana/ui';
import { DataSourceInstanceSettings, RawTimeRange, TimeRange, TimeZone } from '@grafana/data';
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { DataSourcePicker } from '@grafana/runtime';
import { StoreState } from 'app/types/store';
import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
import { changeDatasource } from './state/datasource';

View File

@@ -10,8 +10,8 @@ import {
InputControl,
Legend,
} from '@grafana/ui';
import { DataSourcePicker } from '@grafana/runtime';
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { DashboardInput, DashboardInputs, DataSourceInput, ImportDashboardDTO } from '../state/reducers';
import { validateTitle, validateUid } from '../utils/validation';

View File

@@ -1,6 +1,6 @@
import React, { FC } from 'react';
import { PluginMeta } from '@grafana/data';
import { PluginSignatureBadge } from './PluginSignatureBadge';
import { PluginSignatureBadge } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors';
interface Props {

View File

@@ -17,7 +17,8 @@ import {
UrlQueryMap,
} from '@grafana/data';
import { AppNotificationSeverity } from 'app/types';
import { Alert, InfoBox, Tooltip } from '@grafana/ui';
import { Alert, InfoBox, Tooltip, PluginSignatureBadge } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { getPluginSettings } from './PluginSettingsCache';
import { importAppPlugin, importDataSourcePlugin, importPanelPlugin } from './plugin_loader';
@@ -29,7 +30,6 @@ import { appEvents } from 'app/core/core';
import { config } from 'app/core/config';
import { contextSrv } from '../../core/services/context_srv';
import { css } from 'emotion';
import { PluginSignatureBadge } from './PluginSignatureBadge';
import { selectors } from '@grafana/e2e-selectors';
import { ShowModalEvent } from 'app/types/events';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';

View File

@@ -1,75 +0,0 @@
import React, { HTMLAttributes } from 'react';
import { Badge, BadgeProps } from '@grafana/ui';
import { PluginErrorCode, PluginSignatureStatus } from '@grafana/data';
interface Props extends HTMLAttributes<HTMLDivElement> {
status?: PluginSignatureStatus;
}
export const PluginSignatureBadge: React.FC<Props> = ({ status, ...otherProps }) => {
const display = getSignatureDisplayModel(status);
return (
<Badge
text={display.text}
color={display.color as any}
icon={display.icon}
tooltip={display.tooltip}
{...otherProps}
/>
);
};
export function isUnsignedPluginSignature(signature?: PluginSignatureStatus) {
return signature && signature !== PluginSignatureStatus.valid && signature !== PluginSignatureStatus.internal;
}
export function mapPluginErrorCodeToSignatureStatus(code: PluginErrorCode) {
switch (code) {
case PluginErrorCode.invalidSignature:
return PluginSignatureStatus.invalid;
case PluginErrorCode.missingSignature:
return PluginSignatureStatus.missing;
case PluginErrorCode.modifiedSignature:
return PluginSignatureStatus.modified;
default:
return PluginSignatureStatus.missing;
}
}
function getSignatureDisplayModel(signature?: PluginSignatureStatus): BadgeProps {
if (!signature) {
signature = PluginSignatureStatus.invalid;
}
switch (signature) {
case PluginSignatureStatus.internal:
return { text: 'Core', color: 'blue', tooltip: 'Core plugin that is bundled with Grafana' };
case PluginSignatureStatus.valid:
return { text: 'Signed', icon: 'lock', color: 'green', tooltip: 'Signed and verified plugin' };
case PluginSignatureStatus.invalid:
return {
text: 'Invalid signature',
icon: 'exclamation-triangle',
color: 'red',
tooltip: 'Invalid plugin signature',
};
case PluginSignatureStatus.modified:
return {
text: 'Modified signature',
icon: 'exclamation-triangle',
color: 'red',
tooltip: 'Valid signature but content has been modified',
};
case PluginSignatureStatus.missing:
return {
text: 'Missing signature',
icon: 'exclamation-triangle',
color: 'red',
tooltip: 'Missing plugin signature',
};
}
return { text: 'Unsigned', icon: 'exclamation-triangle', color: 'red', tooltip: 'Unsigned external plugin' };
}
PluginSignatureBadge.displayName = 'PluginSignatureBadge';

View File

@@ -1,14 +1,13 @@
import React from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { HorizontalGroup, InfoBox, List, useTheme } from '@grafana/ui';
import { mapPluginErrorCodeToSignatureStatus, PluginSignatureBadge } from './PluginSignatureBadge';
import { HorizontalGroup, InfoBox, List, PluginSignatureBadge, useTheme } from '@grafana/ui';
import { StoreState } from '../../types';
import { getAllPluginsErrors } from './state/selectors';
import { loadPlugins, loadPluginsErrors } from './state/actions';
import useAsync from 'react-use/lib/useAsync';
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
import { hot } from 'react-hot-loader';
import { PluginError } from '@grafana/data';
import { PluginError, PluginErrorCode, PluginSignatureStatus } from '@grafana/data';
import { css } from 'emotion';
interface ConnectedProps {
@@ -102,3 +101,16 @@ const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {
export const PluginsErrorsInfo = hot(module)(
connect(mapStateToProps, mapDispatchToProps)(PluginsErrorsInfoUnconnected)
);
function mapPluginErrorCodeToSignatureStatus(code: PluginErrorCode) {
switch (code) {
case PluginErrorCode.invalidSignature:
return PluginSignatureStatus.invalid;
case PluginErrorCode.missingSignature:
return PluginSignatureStatus.missing;
case PluginErrorCode.modifiedSignature:
return PluginSignatureStatus.modified;
default:
return PluginSignatureStatus.missing;
}
}

View File

@@ -1,9 +1,8 @@
// Libraries
import React, { PureComponent } from 'react';
// Components
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { Button, CustomScrollbar, HorizontalGroup, Icon, Modal, stylesFactory, Tooltip } from '@grafana/ui';
import { getDataSourceSrv } from '@grafana/runtime';
import { getDataSourceSrv, DataSourcePicker } from '@grafana/runtime';
import { QueryEditorRows } from './QueryEditorRows';
// Services
import { backendSrv } from 'app/core/services/backend_srv';

View File

@@ -3,7 +3,7 @@ import { css } from 'emotion';
import { MapDispatchToProps, MapStateToProps } from 'react-redux';
import { InlineField, InlineFieldRow, VerticalGroup } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors';
import { getTemplateSrv } from '@grafana/runtime';
import { getTemplateSrv, DataSourcePicker } from '@grafana/runtime';
import { DataSourceInstanceSettings, LoadingState, SelectableValue } from '@grafana/data';
import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor';
@@ -23,7 +23,6 @@ import { VariableTextField } from '../editor/VariableTextField';
import { VariableSwitchField } from '../editor/VariableSwitchField';
import { QueryVariableRefreshSelect } from './QueryVariableRefreshSelect';
import { QueryVariableSortSelect } from './QueryVariableSortSelect';
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
export interface OwnProps extends VariableEditorProps<QueryVariableModel> {}

View File

@@ -1,11 +1,11 @@
import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { css } from 'emotion';
import { VariableSuggestion } from '@grafana/data';
import { DataSourcePicker } from '@grafana/runtime';
import { Button, LegacyForms, DataLinkInput, stylesFactory } from '@grafana/ui';
const { FormField, Switch } = LegacyForms;
import { DataLinkConfig } from '../types';
import { usePrevious } from 'react-use';
import { DataSourcePicker } from '../../../../core/components/Select/DataSourcePicker';
const getStyles = stylesFactory(() => ({
firstRow: css`

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { shallow } from 'enzyme';
import { DerivedField } from './DerivedField';
import { DataSourcePicker } from '../../../../core/components/Select/DataSourcePicker';
import { DataSourcePicker } from '@grafana/runtime';
import { DataSourceInstanceSettings } from '@grafana/data';
jest.mock('app/features/plugins/datasource_srv', () => ({

View File

@@ -2,8 +2,8 @@ import React, { useEffect, useState } from 'react';
import { css } from 'emotion';
import { Button, DataLinkInput, stylesFactory, LegacyForms } from '@grafana/ui';
import { VariableSuggestion } from '@grafana/data';
import { DataSourcePicker } from '@grafana/runtime';
import { DerivedFieldConfig } from '../types';
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { usePrevious } from 'react-use';
const { Switch, FormField } = LegacyForms;

View File

@@ -1,5 +1,5 @@
import { Button, InlineField, InlineSwitch, Input } from '@grafana/ui';
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { DataSourcePicker } from '@grafana/runtime';
import { css } from 'emotion';
import React, { useState } from 'react';
import { ExemplarTraceIdDestination } from '../types';