Commit e7807499 by Torkel Ödegaard Committed by GitHub

AddDatasourcePage: Refactoring & more Phantom plugins (#21261)

* WIP: New types and state refactoring

* Added buildCategories & tests

* Added phantom plugins

* fixed tests

* Minor changes, force enterprise plugins to enterprise category

* Fixed tests
parent f93f1c4b
import React, { FC, PureComponent } from 'react'; import React, { FC, PureComponent } from 'react';
import classNames from 'classnames';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { hot } from 'react-hot-loader'; import { hot } from 'react-hot-loader';
import { DataSourcePluginMeta, NavModel, PluginType } from '@grafana/data'; import { DataSourcePluginMeta, NavModel } from '@grafana/data';
import { List } from '@grafana/ui'; import { List } from '@grafana/ui';
import { e2e } from '@grafana/e2e'; import { e2e } from '@grafana/e2e';
import Page from 'app/core/components/Page/Page'; import Page from 'app/core/components/Page/Page';
import { StoreState } from 'app/types'; import { StoreState, DataSourcePluginCategory } from 'app/types';
import { addDataSource, loadDataSourceTypes, setDataSourceTypeSearchQuery } from './state/actions'; import { addDataSource, loadDataSourcePlugins, setDataSourceTypeSearchQuery } from './state/actions';
import { getDataSourceTypes } from './state/selectors'; import { getDataSourcePlugins } from './state/selectors';
import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
export interface Props { export interface Props {
navModel: NavModel; navModel: NavModel;
dataSourceTypes: DataSourcePluginMeta[]; plugins: DataSourcePluginMeta[];
categories: DataSourcePluginCategory[];
isLoading: boolean; isLoading: boolean;
addDataSource: typeof addDataSource; addDataSource: typeof addDataSource;
loadDataSourceTypes: typeof loadDataSourceTypes; loadDataSourcePlugins: typeof loadDataSourcePlugins;
searchQuery: string; searchQuery: string;
setDataSourceTypeSearchQuery: typeof setDataSourceTypeSearchQuery; setDataSourceTypeSearchQuery: typeof setDataSourceTypeSearchQuery;
} }
interface DataSourceCategories {
[key: string]: DataSourcePluginMeta[];
}
interface DataSourceCategoryInfo {
id: string;
title: string;
}
class NewDataSourcePage extends PureComponent<Props> { class NewDataSourcePage extends PureComponent<Props> {
searchInput: HTMLElement; searchInput: HTMLElement;
categoryInfoList: DataSourceCategoryInfo[] = [
{ id: 'tsdb', title: 'Time series databases' },
{ id: 'logging', title: 'Logging & document databases' },
{ id: 'sql', title: 'SQL' },
{ id: 'cloud', title: 'Cloud' },
{ id: 'other', title: 'Others' },
];
sortingRules: { [id: string]: number } = {
prometheus: 100,
graphite: 95,
loki: 90,
mysql: 80,
postgres: 79,
gcloud: -1,
};
componentDidMount() { componentDidMount() {
this.props.loadDataSourceTypes(); this.props.loadDataSourcePlugins();
this.searchInput.focus(); this.searchInput.focus();
} }
...@@ -62,28 +39,14 @@ class NewDataSourcePage extends PureComponent<Props> { ...@@ -62,28 +39,14 @@ class NewDataSourcePage extends PureComponent<Props> {
this.props.setDataSourceTypeSearchQuery(value); this.props.setDataSourceTypeSearchQuery(value);
}; };
renderTypes(types: DataSourcePluginMeta[]) { renderPlugins(plugins: DataSourcePluginMeta[]) {
if (!types) { if (!plugins || !plugins.length) {
return null; return null;
} }
// apply custom sort ranking
types.sort((a, b) => {
const aSort = this.sortingRules[a.id] || 0;
const bSort = this.sortingRules[b.id] || 0;
if (aSort > bSort) {
return -1;
}
if (aSort < bSort) {
return 1;
}
return a.name > b.name ? -1 : 1;
});
return ( return (
<List <List
items={types} items={plugins}
getItemKey={item => item.id.toString()} getItemKey={item => item.id.toString()}
renderItem={item => ( renderItem={item => (
<DataSourceTypeCard <DataSourceTypeCard
...@@ -100,35 +63,21 @@ class NewDataSourcePage extends PureComponent<Props> { ...@@ -100,35 +63,21 @@ class NewDataSourcePage extends PureComponent<Props> {
evt.stopPropagation(); evt.stopPropagation();
}; };
renderGroupedList() { renderCategories() {
const { dataSourceTypes } = this.props; const { categories } = this.props;
if (dataSourceTypes.length === 0) {
return null;
}
const categories = dataSourceTypes.reduce((accumulator, item) => {
const category = item.category || 'other';
const list = accumulator[category] || [];
list.push(item);
accumulator[category] = list;
return accumulator;
}, {} as DataSourceCategories);
categories['cloud'].push(getGrafanaCloudPhantomPlugin());
return ( return (
<> <>
{this.categoryInfoList.map(category => ( {categories.map(category => (
<div className="add-data-source-category" key={category.id}> <div className="add-data-source-category" key={category.id}>
<div className="add-data-source-category__header">{category.title}</div> <div className="add-data-source-category__header">{category.title}</div>
{this.renderTypes(categories[category.id])} {this.renderPlugins(category.plugins)}
</div> </div>
))} ))}
<div className="add-data-source-more"> <div className="add-data-source-more">
<a <a
className="btn btn-inverse" className="btn btn-inverse"
href="https://grafana.com/plugins?type=datasource&utm_source=new-data-source" href="https://grafana.com/plugins?type=datasource&utm_source=grafana_add_ds"
target="_blank" target="_blank"
rel="noopener" rel="noopener"
> >
...@@ -140,7 +89,7 @@ class NewDataSourcePage extends PureComponent<Props> { ...@@ -140,7 +89,7 @@ class NewDataSourcePage extends PureComponent<Props> {
} }
render() { render() {
const { navModel, isLoading, searchQuery, dataSourceTypes } = this.props; const { navModel, isLoading, searchQuery, plugins } = this.props;
return ( return (
<Page navModel={navModel}> <Page navModel={navModel}>
...@@ -162,8 +111,8 @@ class NewDataSourcePage extends PureComponent<Props> { ...@@ -162,8 +111,8 @@ class NewDataSourcePage extends PureComponent<Props> {
</a> </a>
</div> </div>
<div> <div>
{searchQuery && this.renderTypes(dataSourceTypes)} {searchQuery && this.renderPlugins(plugins)}
{!searchQuery && this.renderGroupedList()} {!searchQuery && this.renderCategories()}
</div> </div>
</Page.Contents> </Page.Contents>
</Page> </Page>
...@@ -179,15 +128,18 @@ interface DataSourceTypeCardProps { ...@@ -179,15 +128,18 @@ interface DataSourceTypeCardProps {
const DataSourceTypeCard: FC<DataSourceTypeCardProps> = props => { const DataSourceTypeCard: FC<DataSourceTypeCardProps> = props => {
const { plugin, onLearnMoreClick } = props; const { plugin, onLearnMoreClick } = props;
const canSelect = plugin.id !== 'gcloud'; const isPhantom = plugin.module === 'phantom';
const onClick = canSelect ? props.onClick : () => {}; const onClick = !isPhantom ? props.onClick : () => {};
// find first plugin info link // find first plugin info link
const learnMoreLink = plugin.info.links && plugin.info.links.length > 0 ? plugin.info.links[0].url : null; const learnMoreLink = plugin.info.links && plugin.info.links.length > 0 ? plugin.info.links[0] : null;
const mainClassName = classNames('add-data-source-item', {
'add-data-source-item--phantom': isPhantom,
});
return ( return (
<div <div
className="add-data-source-item" className={mainClassName}
onClick={onClick} onClick={onClick}
aria-label={e2e.pages.AddDataSource.selectors.dataSourcePlugins(plugin.name)} aria-label={e2e.pages.AddDataSource.selectors.dataSourcePlugins(plugin.name)}
> >
...@@ -200,44 +152,20 @@ const DataSourceTypeCard: FC<DataSourceTypeCardProps> = props => { ...@@ -200,44 +152,20 @@ const DataSourceTypeCard: FC<DataSourceTypeCardProps> = props => {
{learnMoreLink && ( {learnMoreLink && (
<a <a
className="btn btn-inverse" className="btn btn-inverse"
href={`${learnMoreLink}?utm_source=grafana_add_ds`} href={`${learnMoreLink.url}?utm_source=grafana_add_ds`}
target="_blank" target="_blank"
rel="noopener" rel="noopener"
onClick={onLearnMoreClick} onClick={onLearnMoreClick}
> >
Learn more <i className="fa fa-external-link add-datasource-item-actions__btn-icon" /> {learnMoreLink.name} <i className="fa fa-external-link add-datasource-item-actions__btn-icon" />
</a> </a>
)} )}
{canSelect && <button className="btn btn-primary">Select</button>} {!isPhantom && <button className="btn btn-primary">Select</button>}
</div> </div>
</div> </div>
); );
}; };
function getGrafanaCloudPhantomPlugin(): DataSourcePluginMeta {
return {
id: 'gcloud',
name: 'Grafana Cloud',
type: PluginType.datasource,
module: '',
baseUrl: '',
info: {
description: 'Hosted Graphite, Prometheus and Loki',
logos: { small: 'public/img/grafana_icon.svg', large: 'asd' },
author: { name: 'Grafana Labs' },
links: [
{
url: 'https://grafana.com/products/cloud/',
name: 'Learn more',
},
],
screenshots: [],
updated: '2019-05-10',
version: '1.0.0',
},
};
}
export function getNavModel(): NavModel { export function getNavModel(): NavModel {
const main = { const main = {
icon: 'gicon gicon-add-datasources', icon: 'gicon gicon-add-datasources',
...@@ -256,15 +184,16 @@ export function getNavModel(): NavModel { ...@@ -256,15 +184,16 @@ export function getNavModel(): NavModel {
function mapStateToProps(state: StoreState) { function mapStateToProps(state: StoreState) {
return { return {
navModel: getNavModel(), navModel: getNavModel(),
dataSourceTypes: getDataSourceTypes(state.dataSources), plugins: getDataSourcePlugins(state.dataSources),
searchQuery: state.dataSources.dataSourceTypeSearchQuery, searchQuery: state.dataSources.dataSourceTypeSearchQuery,
categories: state.dataSources.categories,
isLoading: state.dataSources.isLoadingDataSources, isLoading: state.dataSources.isLoadingDataSources,
}; };
} }
const mapDispatchToProps = { const mapDispatchToProps = {
addDataSource, addDataSource,
loadDataSourceTypes, loadDataSourcePlugins,
setDataSourceTypeSearchQuery, setDataSourceTypeSearchQuery,
}; };
......
...@@ -83,6 +83,7 @@ exports[`Render should render alpha info text 1`] = ` ...@@ -83,6 +83,7 @@ exports[`Render should render alpha info text 1`] = `
}, },
"screenshots": Array [ "screenshots": Array [
Object { Object {
"name": "test",
"path": "screenshot", "path": "screenshot",
}, },
], ],
...@@ -272,6 +273,7 @@ exports[`Render should render is ready only message 1`] = ` ...@@ -272,6 +273,7 @@ exports[`Render should render is ready only message 1`] = `
}, },
"screenshots": Array [ "screenshots": Array [
Object { Object {
"name": "test",
"path": "screenshot", "path": "screenshot",
}, },
], ],
......
import { ThunkAction } from 'redux-thunk';
import config from '../../../core/config'; import config from '../../../core/config';
import { getBackendSrv } from '@grafana/runtime'; import { getBackendSrv } from '@grafana/runtime';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { LayoutMode } from 'app/core/components/LayoutSelector/LayoutSelector'; import { LayoutMode } from 'app/core/components/LayoutSelector/LayoutSelector';
import { updateLocation, updateNavIndex, UpdateNavIndexAction } from 'app/core/actions'; import { updateLocation, updateNavIndex } from 'app/core/actions';
import { buildNavModel } from './navModel'; import { buildNavModel } from './navModel';
import { DataSourceSettings, DataSourcePluginMeta } from '@grafana/data'; import { DataSourceSettings, DataSourcePluginMeta } from '@grafana/data';
import { StoreState } from 'app/types'; import { ThunkResult, DataSourcePluginCategory } from 'app/types';
import { LocationUpdate } from '@grafana/runtime';
import { actionCreatorFactory } from 'app/core/redux'; import { actionCreatorFactory } from 'app/core/redux';
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
import { getPluginSettings } from 'app/features/plugins/PluginSettingsCache'; import { getPluginSettings } from 'app/features/plugins/PluginSettingsCache';
import { importDataSourcePlugin } from 'app/features/plugins/plugin_loader'; import { importDataSourcePlugin } from 'app/features/plugins/plugin_loader';
import { buildCategories } from './buildCategories';
export const dataSourceLoaded = actionCreatorFactory<DataSourceSettings>('LOAD_DATA_SOURCE').create(); export const dataSourceLoaded = actionCreatorFactory<DataSourceSettings>('LOAD_DATA_SOURCE').create();
export const dataSourcesLoaded = actionCreatorFactory<DataSourceSettings[]>('LOAD_DATA_SOURCES').create(); export const dataSourcesLoaded = actionCreatorFactory<DataSourceSettings[]>('LOAD_DATA_SOURCES').create();
export const dataSourceMetaLoaded = actionCreatorFactory<DataSourcePluginMeta>('LOAD_DATA_SOURCE_META').create(); export const dataSourceMetaLoaded = actionCreatorFactory<DataSourcePluginMeta>('LOAD_DATA_SOURCE_META').create();
export const dataSourcePluginsLoad = actionCreatorFactory('LOAD_DATA_SOURCE_PLUGINS').create();
export const dataSourceTypesLoad = actionCreatorFactory('LOAD_DATA_SOURCE_TYPES').create(); export const dataSourcePluginsLoaded = actionCreatorFactory<DataSourceTypesLoadedPayload>(
'LOADED_DATA_SOURCE_PLUGINS'
export const dataSourceTypesLoaded = actionCreatorFactory<DataSourcePluginMeta[]>('LOADED_DATA_SOURCE_TYPES').create(); ).create();
export const setDataSourcesSearchQuery = actionCreatorFactory<string>('SET_DATA_SOURCES_SEARCH_QUERY').create(); export const setDataSourcesSearchQuery = actionCreatorFactory<string>('SET_DATA_SOURCES_SEARCH_QUERY').create();
export const setDataSourcesLayoutMode = actionCreatorFactory<LayoutMode>('SET_DATA_SOURCES_LAYOUT_MODE').create(); export const setDataSourcesLayoutMode = actionCreatorFactory<LayoutMode>('SET_DATA_SOURCES_LAYOUT_MODE').create();
export const setDataSourceTypeSearchQuery = actionCreatorFactory<string>('SET_DATA_SOURCE_TYPE_SEARCH_QUERY').create(); export const setDataSourceTypeSearchQuery = actionCreatorFactory<string>('SET_DATA_SOURCE_TYPE_SEARCH_QUERY').create();
export const setDataSourceName = actionCreatorFactory<string>('SET_DATA_SOURCE_NAME').create(); export const setDataSourceName = actionCreatorFactory<string>('SET_DATA_SOURCE_NAME').create();
export const setIsDefault = actionCreatorFactory<boolean>('SET_IS_DEFAULT').create(); export const setIsDefault = actionCreatorFactory<boolean>('SET_IS_DEFAULT').create();
export type Action = export interface DataSourceTypesLoadedPayload {
| UpdateNavIndexAction plugins: DataSourcePluginMeta[];
| ActionOf<DataSourceSettings> categories: DataSourcePluginCategory[];
| ActionOf<DataSourceSettings[]> }
| ActionOf<DataSourcePluginMeta>
| ActionOf<DataSourcePluginMeta[]>
| ActionOf<LocationUpdate>;
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
export function loadDataSources(): ThunkResult<void> { export function loadDataSources(): ThunkResult<void> {
return async dispatch => { return async dispatch => {
...@@ -84,11 +70,12 @@ export function addDataSource(plugin: DataSourcePluginMeta): ThunkResult<void> { ...@@ -84,11 +70,12 @@ export function addDataSource(plugin: DataSourcePluginMeta): ThunkResult<void> {
}; };
} }
export function loadDataSourceTypes(): ThunkResult<void> { export function loadDataSourcePlugins(): ThunkResult<void> {
return async dispatch => { return async dispatch => {
dispatch(dataSourceTypesLoad()); dispatch(dataSourcePluginsLoad());
const result = await getBackendSrv().get('/api/plugins', { enabled: 1, type: 'datasource' }); const plugins = await getBackendSrv().get('/api/plugins', { enabled: 1, type: 'datasource' });
dispatch(dataSourceTypesLoaded(result as DataSourcePluginMeta[])); const categories = buildCategories(plugins);
dispatch(dataSourcePluginsLoaded({ plugins, categories }));
}; };
} }
......
import { buildCategories } from './buildCategories';
import { getMockPlugin } from '../../plugins/__mocks__/pluginMocks';
import { DataSourcePluginMeta } from '@grafana/data';
const plugins: DataSourcePluginMeta[] = [
{
...getMockPlugin({ id: 'graphite' }),
category: 'tsdb',
},
{
...getMockPlugin({ id: 'prometheus' }),
category: 'tsdb',
},
{
...getMockPlugin({ id: 'elasticsearch' }),
category: 'logging',
},
{
...getMockPlugin({ id: 'loki' }),
category: 'logging',
},
{
...getMockPlugin({ id: 'azure' }),
category: 'cloud',
},
];
describe('buildCategories', () => {
const categories = buildCategories(plugins);
it('should group plugins into categories', () => {
expect(categories.length).toBe(6);
expect(categories[0].title).toBe('Time series databases');
expect(categories[0].plugins.length).toBe(2);
expect(categories[1].title).toBe('Logging & document databases');
});
it('should sort plugins according to hard coded sorting rules', () => {
expect(categories[1].plugins[0].id).toBe('loki');
});
it('should add phantom plugin for Grafana cloud', () => {
expect(categories[3].title).toBe('Cloud');
expect(categories[3].plugins.length).toBe(2);
expect(categories[3].plugins[1].id).toBe('gcloud');
});
it('should set module to phantom on phantom plugins', () => {
expect(categories[4].plugins[0].module).toBe('phantom');
});
it('should add enterprise phantom plugins', () => {
expect(categories[4].title).toBe('Enterprise plugins');
expect(categories[4].plugins.length).toBe(5);
});
});
import { DataSourcePluginMeta, PluginType } from '@grafana/data';
import { DataSourcePluginCategory } from 'app/types';
export function buildCategories(plugins: DataSourcePluginMeta[]): DataSourcePluginCategory[] {
const categories: DataSourcePluginCategory[] = [
{ id: 'tsdb', title: 'Time series databases', plugins: [] },
{ id: 'logging', title: 'Logging & document databases', plugins: [] },
{ id: 'sql', title: 'SQL', plugins: [] },
{ id: 'cloud', title: 'Cloud', plugins: [] },
{ id: 'enterprise', title: 'Enterprise plugins', plugins: [] },
{ id: 'other', title: 'Others', plugins: [] },
];
const categoryIndex: Record<string, DataSourcePluginCategory> = {};
const pluginIndex: Record<string, DataSourcePluginMeta> = {};
const enterprisePlugins = getEnterprisePhantomPlugins();
// build indices
for (const category of categories) {
categoryIndex[category.id] = category;
}
for (const plugin of plugins) {
// Force category for enterprise plugins
if (enterprisePlugins.find(item => item.id === plugin.id)) {
plugin.category = 'enterprise';
}
// Fix link name
if (plugin.info.links) {
for (const link of plugin.info.links) {
link.name = 'Learn more';
}
}
const category = categories.find(item => item.id === plugin.category) || categoryIndex['other'];
category.plugins.push(plugin);
// add to plugin index
pluginIndex[plugin.id] = plugin;
}
for (const category of categories) {
// add phantom plugin
if (category.id === 'cloud') {
category.plugins.push(getGrafanaCloudPhantomPlugin());
}
// add phantom plugins
if (category.id === 'enterprise') {
for (const plugin of enterprisePlugins) {
if (!pluginIndex[plugin.id]) {
category.plugins.push(plugin);
}
}
}
sortPlugins(category.plugins);
}
return categories;
}
function sortPlugins(plugins: DataSourcePluginMeta[]) {
const sortingRules: { [id: string]: number } = {
prometheus: 100,
graphite: 95,
loki: 90,
mysql: 80,
postgres: 79,
gcloud: -1,
};
plugins.sort((a, b) => {
const aSort = sortingRules[a.id] || 0;
const bSort = sortingRules[b.id] || 0;
if (aSort > bSort) {
return -1;
}
if (aSort < bSort) {
return 1;
}
return a.name > b.name ? -1 : 1;
});
}
function getEnterprisePhantomPlugins(): DataSourcePluginMeta[] {
return [
getPhantomPlugin({
id: 'grafana-splunk-datasource',
name: 'Splunk',
description: 'Visualize & explore Splunk logs',
imgUrl: 'public/img/plugins/splunk_logo_128.png',
}),
getPhantomPlugin({
id: 'grafana-oracle-datasource',
name: 'Oracle',
description: 'Visualize & explore Oracle SQL',
imgUrl: 'public/img/plugins/oracle.png',
}),
getPhantomPlugin({
id: 'grafana-dynatrace-datasource',
name: 'Dynatrace',
description: 'Visualize & explore Dynatrace data',
imgUrl: 'public/img/plugins/dynatrace.png',
}),
getPhantomPlugin({
id: 'grafana-servicenow-datasource',
description: 'ServiceNow integration & data source',
name: 'ServiceNow',
imgUrl: 'public/img/plugins/servicenow.svg',
}),
getPhantomPlugin({
id: 'grafana-datadog-datasource',
description: 'DataDog integration & data source',
name: 'DataDog',
imgUrl: 'public/img/plugins/datadog.png',
}),
];
}
function getGrafanaCloudPhantomPlugin(): DataSourcePluginMeta {
return {
id: 'gcloud',
name: 'Grafana Cloud',
type: PluginType.datasource,
module: 'phantom',
baseUrl: '',
info: {
description: 'Hosted Graphite, Prometheus and Loki',
logos: { small: 'public/img/grafana_icon.svg', large: 'asd' },
author: { name: 'Grafana Labs' },
links: [
{
url: 'https://grafana.com/products/cloud/',
name: 'Learn more',
},
],
screenshots: [],
updated: '2019-05-10',
version: '1.0.0',
},
};
}
interface GetPhantomPluginOptions {
id: string;
name: string;
description: string;
imgUrl: string;
}
function getPhantomPlugin(options: GetPhantomPluginOptions): DataSourcePluginMeta {
return {
id: options.id,
name: options.name,
type: PluginType.datasource,
module: 'phantom',
baseUrl: '',
info: {
description: options.description,
logos: { small: options.imgUrl, large: options.imgUrl },
author: { name: 'Grafana Labs' },
links: [
{
url: 'https://grafana.com/grafana/plugins/' + options.id,
name: 'Install now',
},
],
screenshots: [],
updated: '2019-05-10',
version: '1.0.0',
},
};
}
...@@ -5,8 +5,8 @@ import { ...@@ -5,8 +5,8 @@ import {
dataSourceLoaded, dataSourceLoaded,
setDataSourcesSearchQuery, setDataSourcesSearchQuery,
setDataSourcesLayoutMode, setDataSourcesLayoutMode,
dataSourceTypesLoad, dataSourcePluginsLoad,
dataSourceTypesLoaded, dataSourcePluginsLoaded,
setDataSourceTypeSearchQuery, setDataSourceTypeSearchQuery,
dataSourceMetaLoaded, dataSourceMetaLoaded,
setDataSourceName, setDataSourceName,
...@@ -76,12 +76,12 @@ describe('dataSourcesReducer', () => { ...@@ -76,12 +76,12 @@ describe('dataSourcesReducer', () => {
describe('when dataSourceTypesLoad is dispatched', () => { describe('when dataSourceTypesLoad is dispatched', () => {
it('then state should be correct', () => { it('then state should be correct', () => {
const state: DataSourcesState = { ...initialState, dataSourceTypes: [mockPlugin()] }; const state: DataSourcesState = { ...initialState, plugins: [mockPlugin()] };
reducerTester() reducerTester()
.givenReducer(dataSourcesReducer, state) .givenReducer(dataSourcesReducer, state)
.whenActionIsDispatched(dataSourceTypesLoad()) .whenActionIsDispatched(dataSourcePluginsLoad())
.thenStateShouldEqual({ ...initialState, dataSourceTypes: [], isLoadingDataSources: true }); .thenStateShouldEqual({ ...initialState, isLoadingDataSources: true });
}); });
}); });
...@@ -92,8 +92,8 @@ describe('dataSourcesReducer', () => { ...@@ -92,8 +92,8 @@ describe('dataSourcesReducer', () => {
reducerTester() reducerTester()
.givenReducer(dataSourcesReducer, state) .givenReducer(dataSourcesReducer, state)
.whenActionIsDispatched(dataSourceTypesLoaded(dataSourceTypes)) .whenActionIsDispatched(dataSourcePluginsLoaded({ plugins: dataSourceTypes, categories: [] }))
.thenStateShouldEqual({ ...initialState, dataSourceTypes, isLoadingDataSources: false }); .thenStateShouldEqual({ ...initialState, plugins: dataSourceTypes, isLoadingDataSources: false });
}); });
}); });
......
...@@ -5,8 +5,8 @@ import { ...@@ -5,8 +5,8 @@ import {
dataSourcesLoaded, dataSourcesLoaded,
setDataSourcesSearchQuery, setDataSourcesSearchQuery,
setDataSourcesLayoutMode, setDataSourcesLayoutMode,
dataSourceTypesLoad, dataSourcePluginsLoad,
dataSourceTypesLoaded, dataSourcePluginsLoaded,
setDataSourceTypeSearchQuery, setDataSourceTypeSearchQuery,
dataSourceMetaLoaded, dataSourceMetaLoaded,
setDataSourceName, setDataSourceName,
...@@ -17,11 +17,12 @@ import { reducerFactory } from 'app/core/redux'; ...@@ -17,11 +17,12 @@ import { reducerFactory } from 'app/core/redux';
export const initialState: DataSourcesState = { export const initialState: DataSourcesState = {
dataSources: [], dataSources: [],
plugins: [],
categories: [],
dataSource: {} as DataSourceSettings, dataSource: {} as DataSourceSettings,
layoutMode: LayoutModes.List, layoutMode: LayoutModes.List,
searchQuery: '', searchQuery: '',
dataSourcesCount: 0, dataSourcesCount: 0,
dataSourceTypes: [],
dataSourceTypeSearchQuery: '', dataSourceTypeSearchQuery: '',
hasFetched: false, hasFetched: false,
isLoadingDataSources: false, isLoadingDataSources: false,
...@@ -51,14 +52,15 @@ export const dataSourcesReducer = reducerFactory(initialState) ...@@ -51,14 +52,15 @@ export const dataSourcesReducer = reducerFactory(initialState)
mapper: (state, action) => ({ ...state, layoutMode: action.payload }), mapper: (state, action) => ({ ...state, layoutMode: action.payload }),
}) })
.addMapper({ .addMapper({
filter: dataSourceTypesLoad, filter: dataSourcePluginsLoad,
mapper: state => ({ ...state, dataSourceTypes: [], isLoadingDataSources: true }), mapper: state => ({ ...state, plugins: [], isLoadingDataSources: true }),
}) })
.addMapper({ .addMapper({
filter: dataSourceTypesLoaded, filter: dataSourcePluginsLoaded,
mapper: (state, action) => ({ mapper: (state, action) => ({
...state, ...state,
dataSourceTypes: action.payload, plugins: action.payload.plugins,
categories: action.payload.categories,
isLoadingDataSources: false, isLoadingDataSources: false,
}), }),
}) })
......
...@@ -10,10 +10,10 @@ export const getDataSources = (state: DataSourcesState) => { ...@@ -10,10 +10,10 @@ export const getDataSources = (state: DataSourcesState) => {
}); });
}; };
export const getDataSourceTypes = (state: DataSourcesState) => { export const getDataSourcePlugins = (state: DataSourcesState) => {
const regex = new RegExp(state.dataSourceTypeSearchQuery, 'i'); const regex = new RegExp(state.dataSourceTypeSearchQuery, 'i');
return state.dataSourceTypes.filter((type: DataSourcePluginMeta) => { return state.plugins.filter((type: DataSourcePluginMeta) => {
return regex.test(type.name); return regex.test(type.name);
}); });
}; };
......
import { defaultsDeep } from 'lodash';
import { PanelPluginMeta, PluginMeta, PluginType, PanelPlugin, PanelProps } from '@grafana/data'; import { PanelPluginMeta, PluginMeta, PluginType, PanelPlugin, PanelProps } from '@grafana/data';
import { ComponentType } from 'enzyme'; import { ComponentType } from 'enzyme';
...@@ -67,8 +68,8 @@ export const getPanelPlugin = ( ...@@ -67,8 +68,8 @@ export const getPanelPlugin = (
return plugin; return plugin;
}; };
export const getMockPlugin = () => { export function getMockPlugin(overrides?: Partial<PluginMeta>): PluginMeta {
return { const defaults: PluginMeta = {
defaultNavUrl: 'some/url', defaultNavUrl: 'some/url',
enabled: false, enabled: false,
hasUpdate: false, hasUpdate: false,
...@@ -81,7 +82,7 @@ export const getMockPlugin = () => { ...@@ -81,7 +82,7 @@ export const getMockPlugin = () => {
description: 'pretty decent plugin', description: 'pretty decent plugin',
links: [{ name: 'project', url: 'one link' }], links: [{ name: 'project', url: 'one link' }],
logos: { small: 'small/logo', large: 'large/logo' }, logos: { small: 'small/logo', large: 'large/logo' },
screenshots: [{ path: `screenshot` }], screenshots: [{ path: `screenshot`, name: 'test' }],
updated: '2018-09-26', updated: '2018-09-26',
version: '1', version: '1',
}, },
...@@ -91,5 +92,7 @@ export const getMockPlugin = () => { ...@@ -91,5 +92,7 @@ export const getMockPlugin = () => {
pinned: false, pinned: false,
type: PluginType.panel, type: PluginType.panel,
module: 'path/to/module', module: 'path/to/module',
} as PluginMeta; };
};
return defaultsDeep(overrides || {}, defaults) as PluginMeta;
}
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
"large": "img/logo.jpg" "large": "img/logo.jpg"
}, },
"links": [ "links": [
{ "name": "Project site", "url": "https://github.com/grafana/azure-monitor-datasource" }, { "name": "Learn more", "url": "https://github.com/grafana/azure-monitor-datasource" },
{ "name": "Apache License", "url": "https://github.com/grafana/azure-monitor-datasource/blob/master/LICENSE" } { "name": "Apache License", "url": "https://github.com/grafana/azure-monitor-datasource/blob/master/LICENSE" }
], ],
"screenshots": [ "screenshots": [
......
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
"large": "img/graphite_logo.png" "large": "img/graphite_logo.png"
}, },
"links": [ "links": [
{ "name": "Graphite", "url": "https://graphiteapp.org/" }, { "name": "Learn more", "url": "https://graphiteapp.org/" },
{ {
"name": "Graphite 1.1 Release", "name": "Graphite 1.1 Release",
"url": "https://grafana.com/blog/2018/01/11/graphite-1.1-teaching-an-old-dog-new-tricks/" "url": "https://grafana.com/blog/2018/01/11/graphite-1.1-teaching-an-old-dog-new-tricks/"
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
}, },
"links": [ "links": [
{ {
"name": "Prometheus", "name": "Learn more",
"url": "https://prometheus.io/" "url": "https://prometheus.io/"
} }
] ]
......
...@@ -7,9 +7,16 @@ export interface DataSourcesState { ...@@ -7,9 +7,16 @@ export interface DataSourcesState {
dataSourceTypeSearchQuery: string; dataSourceTypeSearchQuery: string;
layoutMode: LayoutMode; layoutMode: LayoutMode;
dataSourcesCount: number; dataSourcesCount: number;
dataSourceTypes: DataSourcePluginMeta[];
dataSource: DataSourceSettings; dataSource: DataSourceSettings;
dataSourceMeta: DataSourcePluginMeta; dataSourceMeta: DataSourcePluginMeta;
hasFetched: boolean; hasFetched: boolean;
isLoadingDataSources: boolean; isLoadingDataSources: boolean;
plugins: DataSourcePluginMeta[];
categories: DataSourcePluginCategory[];
}
export interface DataSourcePluginCategory {
id: string;
title: string;
plugins: DataSourcePluginMeta[];
} }
<?xml version="1.0" encoding="utf-8"?>
<svg width="45.155556mm" height="45.155556mm" viewBox="0 0 160 160" id="svg2" version="1.1" xmlns="http://www.w3.org/2000/svg">
<defs id="defs4"></defs>
<metadata id="metadata7">
image/svg+xml
</metadata>
<g id="layer1" transform="translate(-28.571445,-58.076511)">
<rect style="opacity:1;fill:#43af49;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" id="rect4229" width="160" height="160" x="28.571445" y="58.076511"></rect>
<path style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m 31.214848,129.47656 8.574218,-0.24414 30.628907,-40.91992 9.433593,-6.529297 9.433594,6.529297 30.62695,40.91992 8.57617,0.24414 L 79.846221,31.941075 C 63.384998,64.975987 46.182247,99.463396 31.214848,129.47656 Z" transform="translate(28.571445,58.076511)" id="path4252"></path>
</g>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 500 150" style="enable-background:new 0 0 500 150;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#293E40;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#81B5A1;}
.st2{fill:#293E40;}
</style>
<path class="st0" d="M8.3,141V12.7h33.2v10.2c9.9-8.3,23-13.1,36-13.1c16.9,0,33.2,7.7,44.3,20.7c8.6,10.2,13.1,23,13.1,45V141
h-34.4V73c1-8.9-1.6-17.5-7.3-24.2c-5.4-4.8-12.1-7.3-19.5-7c-12.1,0.3-23.6,6.1-30.9,15.6V141"/>
<path class="st1" d="M223,122.5c-9.9,0.3-19.5-3.8-26.5-10.8c-7-7-10.8-16.6-10.8-26.5c-0.6-13.7,6.4-26.8,18.2-33.8
c11.8-7.3,26.8-7.3,38.6,0c11.8,7,18.5,20.1,17.9,33.8c0.3,9.9-3.5,19.5-10.5,26.5C242.8,119,233.2,122.8,223,122.5 M223,9.9
c-30.6,0-58.4,18.8-69.5,47.2c-11.5,28.7-4.5,61.2,17.9,82.3c5.4,5.1,13.7,5.7,19.5,1.3c18.8-14.4,45-14.4,63.8,0
c6.1,4.5,14,3.8,19.5-1.3c22-21,29.3-53.3,18.2-82C281,29,253.6,10.2,223,9.9z"/>
<polyline class="st0" points="365.2,141 339.7,141 288.7,12.7 322.8,12.7 350.9,86.1 378.3,12.7 406.7,12.7 433.8,86.1 461.9,12.7
496,12.7 445.3,141 419.8,141 392.3,67.9 365.2,141 "/>
<polyline class="st2" points="477.5,124.7 477.5,127.2 473,127.2 473,141 469.8,141 469.8,127.2 465.7,127.2 465.7,124.7
477.5,124.7 "/>
<polyline class="st2" points="488,134.3 493.8,124.7 496,124.7 496,141 493.1,141 493.1,131.7 489,138.1 486.8,138.1 482.9,131.7
482.9,141 479.7,141 479.7,124.7 482,124.7 488,134.3 "/>
</svg>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment