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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 124 additions and 61 deletions

View File

@ -30,5 +30,5 @@ export * from './legacyEvents';
export * from './live';
export * from './variables';
export * from './geometry';
export { isUnsignedPluginSignature } from './pluginSignature';
export { GrafanaConfig, BuildInfo, FeatureToggles, LicenseInfo } from './config';

View File

@ -0,0 +1,11 @@
import { PluginSignatureStatus } from './plugin';
/**
* Utility function to check if a plugin is unsigned.
*
* @param signature - the plugin meta signature
* @internal
*/
export function isUnsignedPluginSignature(signature?: PluginSignatureStatus) {
return signature && signature !== PluginSignatureStatus.valid && signature !== PluginSignatureStatus.internal;
}

View File

@ -24,6 +24,7 @@
"dependencies": {
"@grafana/data": "7.5.0-pre.0",
"@grafana/ui": "7.5.0-pre.0",
"@grafana/e2e-selectors": "7.5.0-pre.0",
"systemjs": "0.20.19",
"systemjs-plugin-css": "0.1.37",
"history": "4.10.1"

View File

@ -20,7 +20,7 @@ const buildCjsPackage = ({ env }) => {
globals: {},
},
],
external: ['lodash', '@grafana/ui', '@grafana/data'], // Use Lodash from grafana
external: ['lodash', 'react', '@grafana/ui', '@grafana/data', '@grafana/e2e-selectors'], // Use Lodash from grafana
plugins: [
commonjs({
include: /node_modules/,

View File

@ -2,13 +2,17 @@
import React, { PureComponent } from 'react';
// Components
import { HorizontalGroup, Select } from '@grafana/ui';
import { DataSourceInstanceSettings, SelectableValue } from '@grafana/data';
import { HorizontalGroup, PluginSignatureBadge, Select } from '@grafana/ui';
import { DataSourceInstanceSettings, isUnsignedPluginSignature, SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { isUnsignedPluginSignature, PluginSignatureBadge } from '../../../features/plugins/PluginSignatureBadge';
import { getDataSourceSrv } from '@grafana/runtime';
import { getDataSourceSrv } from '../services/dataSourceSrv';
export interface Props {
/**
* Component props description for the {@link DataSourcePicker}
*
* @internal
*/
export interface DataSourcePickerProps {
onChange: (ds: DataSourceInstanceSettings) => void;
current: string | null;
hideTextValue?: boolean;
@ -26,22 +30,33 @@ export interface Props {
noDefault?: boolean;
}
export interface State {
/**
* Component state description for the {@link DataSourcePicker}
*
* @internal
*/
export interface DataSourcePickerState {
error?: string;
}
export class DataSourcePicker extends PureComponent<Props, State> {
/**
* Component to be able to select a datasource from the list of installed and enabled
* datasources in the current Grafana instance.
*
* @internal
*/
export class DataSourcePicker extends PureComponent<DataSourcePickerProps, DataSourcePickerState> {
dataSourceSrv = getDataSourceSrv();
static defaultProps: Partial<Props> = {
static defaultProps: Partial<DataSourcePickerProps> = {
autoFocus: false,
openMenuOnFocus: false,
placeholder: 'Select datasource',
};
state: State = {};
state: DataSourcePickerState = {};
constructor(props: Props) {
constructor(props: DataSourcePickerProps) {
super(props);
}
@ -62,11 +77,11 @@ export class DataSourcePicker extends PureComponent<Props, State> {
}
};
private getCurrentValue() {
private getCurrentValue(): SelectableValue<string> | undefined {
const { current, hideTextValue, noDefault } = this.props;
if (!current && noDefault) {
return null;
return;
}
const ds = this.dataSourceSrv.getInstanceSettings(current);
@ -83,7 +98,7 @@ export class DataSourcePicker extends PureComponent<Props, State> {
return {
label: (current ?? 'no name') + ' - not found',
value: current,
value: current === null ? undefined : current,
imgUrl: '',
hideText: hideTextValue,
};

View File

@ -12,3 +12,4 @@ export { reportMetaAnalytics } from './utils/analytics';
export { logInfo, logDebug, logWarning, logError } from './utils/logging';
export { DataSourceWithBackend, HealthCheckResult, HealthStatus } from './utils/DataSourceWithBackend';
export { toDataQueryError, toDataQueryResponse, frameToMetricFindValue } from './utils/queryResponse';
export { DataSourcePicker, DataSourcePickerProps, DataSourcePickerState } from './components/DataSourcePicker';

View File

@ -0,0 +1,32 @@
import React from 'react';
import { select } from '@storybook/addon-knobs';
import { PluginSignatureBadge } from '@grafana/ui';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { PluginSignatureStatus } from '@grafana/data';
export default {
title: 'Data Display/PluginSignatureBadge',
decorators: [withCenteredStory],
component: PluginSignatureBadge,
};
const getKnobs = () => {
return {
status: select(
'status',
[
PluginSignatureStatus.missing,
PluginSignatureStatus.invalid,
PluginSignatureStatus.modified,
PluginSignatureStatus.valid,
PluginSignatureStatus.internal,
],
PluginSignatureStatus.valid
),
};
};
export const basic = () => {
const { status } = getKnobs();
return <PluginSignatureBadge status={status} />;
};

View File

@ -1,12 +1,18 @@
import React, { HTMLAttributes } from 'react';
import { Badge, BadgeProps } from '@grafana/ui';
import { PluginErrorCode, PluginSignatureStatus } from '@grafana/data';
import { PluginSignatureStatus } from '@grafana/data';
import { Badge, BadgeProps } from '../Badge/Badge';
interface Props extends HTMLAttributes<HTMLDivElement> {
/**
* @public
*/
export interface PluginSignatureBadgeProps extends HTMLAttributes<HTMLDivElement> {
status?: PluginSignatureStatus;
}
export const PluginSignatureBadge: React.FC<Props> = ({ status, ...otherProps }) => {
/**
* @public
*/
export const PluginSignatureBadge: React.FC<PluginSignatureBadgeProps> = ({ status, ...otherProps }) => {
const display = getSignatureDisplayModel(status);
return (
<Badge
@ -19,22 +25,7 @@ export const PluginSignatureBadge: React.FC<Props> = ({ status, ...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;
}
}
PluginSignatureBadge.displayName = 'PluginSignatureBadge';
function getSignatureDisplayModel(signature?: PluginSignatureStatus): BadgeProps {
if (!signature) {
@ -67,9 +58,12 @@ function getSignatureDisplayModel(signature?: PluginSignatureStatus): BadgeProps
color: 'red',
tooltip: 'Missing plugin signature',
};
default:
return {
text: 'Unsigned',
icon: 'exclamation-triangle',
color: 'red',
tooltip: 'Unsigned external plugin',
};
}
return { text: 'Unsigned', icon: 'exclamation-triangle', color: 'red', tooltip: 'Unsigned external plugin' };
}
PluginSignatureBadge.displayName = 'PluginSignatureBadge';

View File

@ -179,6 +179,7 @@ export { Card, Props as CardProps, ContainerProps, CardInnerProps, getCardStyles
export { FormattedValueDisplay } from './FormattedValueDisplay/FormattedValueDisplay';
export { ButtonSelect } from './Dropdown/ButtonSelect';
export { PluginSignatureBadge, PluginSignatureBadgeProps } from './PluginSignatureBadge/PluginSignatureBadge';
// Legacy forms

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,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';