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 classNames from 'classnames';
import { connect } from 'react-redux';
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 { e2e } from '@grafana/e2e';
import Page from 'app/core/components/Page/Page';
import { StoreState } from 'app/types';
import { addDataSource, loadDataSourceTypes, setDataSourceTypeSearchQuery } from './state/actions';
import { getDataSourceTypes } from './state/selectors';
import { StoreState, DataSourcePluginCategory } from 'app/types';
import { addDataSource, loadDataSourcePlugins, setDataSourceTypeSearchQuery } from './state/actions';
import { getDataSourcePlugins } from './state/selectors';
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
export interface Props {
navModel: NavModel;
dataSourceTypes: DataSourcePluginMeta[];
plugins: DataSourcePluginMeta[];
categories: DataSourcePluginCategory[];
isLoading: boolean;
addDataSource: typeof addDataSource;
loadDataSourceTypes: typeof loadDataSourceTypes;
loadDataSourcePlugins: typeof loadDataSourcePlugins;
searchQuery: string;
setDataSourceTypeSearchQuery: typeof setDataSourceTypeSearchQuery;
}
interface DataSourceCategories {
[key: string]: DataSourcePluginMeta[];
}
interface DataSourceCategoryInfo {
id: string;
title: string;
}
class NewDataSourcePage extends PureComponent<Props> {
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() {
this.props.loadDataSourceTypes();
this.props.loadDataSourcePlugins();
this.searchInput.focus();
}
......@@ -62,28 +39,14 @@ class NewDataSourcePage extends PureComponent<Props> {
this.props.setDataSourceTypeSearchQuery(value);
};
renderTypes(types: DataSourcePluginMeta[]) {
if (!types) {
renderPlugins(plugins: DataSourcePluginMeta[]) {
if (!plugins || !plugins.length) {
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 (
<List
items={types}
items={plugins}
getItemKey={item => item.id.toString()}
renderItem={item => (
<DataSourceTypeCard
......@@ -100,35 +63,21 @@ class NewDataSourcePage extends PureComponent<Props> {
evt.stopPropagation();
};
renderGroupedList() {
const { dataSourceTypes } = 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());
renderCategories() {
const { categories } = this.props;
return (
<>
{this.categoryInfoList.map(category => (
{categories.map(category => (
<div className="add-data-source-category" key={category.id}>
<div className="add-data-source-category__header">{category.title}</div>
{this.renderTypes(categories[category.id])}
{this.renderPlugins(category.plugins)}
</div>
))}
<div className="add-data-source-more">
<a
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"
rel="noopener"
>
......@@ -140,7 +89,7 @@ class NewDataSourcePage extends PureComponent<Props> {
}
render() {
const { navModel, isLoading, searchQuery, dataSourceTypes } = this.props;
const { navModel, isLoading, searchQuery, plugins } = this.props;
return (
<Page navModel={navModel}>
......@@ -162,8 +111,8 @@ class NewDataSourcePage extends PureComponent<Props> {
</a>
</div>
<div>
{searchQuery && this.renderTypes(dataSourceTypes)}
{!searchQuery && this.renderGroupedList()}
{searchQuery && this.renderPlugins(plugins)}
{!searchQuery && this.renderCategories()}
</div>
</Page.Contents>
</Page>
......@@ -179,15 +128,18 @@ interface DataSourceTypeCardProps {
const DataSourceTypeCard: FC<DataSourceTypeCardProps> = props => {
const { plugin, onLearnMoreClick } = props;
const canSelect = plugin.id !== 'gcloud';
const onClick = canSelect ? props.onClick : () => {};
const isPhantom = plugin.module === 'phantom';
const onClick = !isPhantom ? props.onClick : () => {};
// 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 (
<div
className="add-data-source-item"
className={mainClassName}
onClick={onClick}
aria-label={e2e.pages.AddDataSource.selectors.dataSourcePlugins(plugin.name)}
>
......@@ -200,44 +152,20 @@ const DataSourceTypeCard: FC<DataSourceTypeCardProps> = props => {
{learnMoreLink && (
<a
className="btn btn-inverse"
href={`${learnMoreLink}?utm_source=grafana_add_ds`}
href={`${learnMoreLink.url}?utm_source=grafana_add_ds`}
target="_blank"
rel="noopener"
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>
)}
{canSelect && <button className="btn btn-primary">Select</button>}
{!isPhantom && <button className="btn btn-primary">Select</button>}
</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 {
const main = {
icon: 'gicon gicon-add-datasources',
......@@ -256,15 +184,16 @@ export function getNavModel(): NavModel {
function mapStateToProps(state: StoreState) {
return {
navModel: getNavModel(),
dataSourceTypes: getDataSourceTypes(state.dataSources),
plugins: getDataSourcePlugins(state.dataSources),
searchQuery: state.dataSources.dataSourceTypeSearchQuery,
categories: state.dataSources.categories,
isLoading: state.dataSources.isLoadingDataSources,
};
}
const mapDispatchToProps = {
addDataSource,
loadDataSourceTypes,
loadDataSourcePlugins,
setDataSourceTypeSearchQuery,
};
......
......@@ -83,6 +83,7 @@ exports[`Render should render alpha info text 1`] = `
},
"screenshots": Array [
Object {
"name": "test",
"path": "screenshot",
},
],
......@@ -272,6 +273,7 @@ exports[`Render should render is ready only message 1`] = `
},
"screenshots": Array [
Object {
"name": "test",
"path": "screenshot",
},
],
......
import { ThunkAction } from 'redux-thunk';
import config from '../../../core/config';
import { getBackendSrv } from '@grafana/runtime';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
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 { DataSourceSettings, DataSourcePluginMeta } from '@grafana/data';
import { StoreState } from 'app/types';
import { LocationUpdate } from '@grafana/runtime';
import { ThunkResult, DataSourcePluginCategory } from 'app/types';
import { actionCreatorFactory } from 'app/core/redux';
import { ActionOf } from 'app/core/redux/actionCreatorFactory';
import { getPluginSettings } from 'app/features/plugins/PluginSettingsCache';
import { importDataSourcePlugin } from 'app/features/plugins/plugin_loader';
import { buildCategories } from './buildCategories';
export const dataSourceLoaded = actionCreatorFactory<DataSourceSettings>('LOAD_DATA_SOURCE').create();
export const dataSourcesLoaded = actionCreatorFactory<DataSourceSettings[]>('LOAD_DATA_SOURCES').create();
export const dataSourceMetaLoaded = actionCreatorFactory<DataSourcePluginMeta>('LOAD_DATA_SOURCE_META').create();
export const dataSourceTypesLoad = actionCreatorFactory('LOAD_DATA_SOURCE_TYPES').create();
export const dataSourceTypesLoaded = actionCreatorFactory<DataSourcePluginMeta[]>('LOADED_DATA_SOURCE_TYPES').create();
export const dataSourcePluginsLoad = actionCreatorFactory('LOAD_DATA_SOURCE_PLUGINS').create();
export const dataSourcePluginsLoaded = actionCreatorFactory<DataSourceTypesLoadedPayload>(
'LOADED_DATA_SOURCE_PLUGINS'
).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 setDataSourceTypeSearchQuery = actionCreatorFactory<string>('SET_DATA_SOURCE_TYPE_SEARCH_QUERY').create();
export const setDataSourceName = actionCreatorFactory<string>('SET_DATA_SOURCE_NAME').create();
export const setIsDefault = actionCreatorFactory<boolean>('SET_IS_DEFAULT').create();
export type Action =
| UpdateNavIndexAction
| ActionOf<DataSourceSettings>
| ActionOf<DataSourceSettings[]>
| ActionOf<DataSourcePluginMeta>
| ActionOf<DataSourcePluginMeta[]>
| ActionOf<LocationUpdate>;
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
export interface DataSourceTypesLoadedPayload {
plugins: DataSourcePluginMeta[];
categories: DataSourcePluginCategory[];
}
export function loadDataSources(): ThunkResult<void> {
return async dispatch => {
......@@ -84,11 +70,12 @@ export function addDataSource(plugin: DataSourcePluginMeta): ThunkResult<void> {
};
}
export function loadDataSourceTypes(): ThunkResult<void> {
export function loadDataSourcePlugins(): ThunkResult<void> {
return async dispatch => {
dispatch(dataSourceTypesLoad());
const result = await getBackendSrv().get('/api/plugins', { enabled: 1, type: 'datasource' });
dispatch(dataSourceTypesLoaded(result as DataSourcePluginMeta[]));
dispatch(dataSourcePluginsLoad());
const plugins = await getBackendSrv().get('/api/plugins', { enabled: 1, type: 'datasource' });
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 {
dataSourceLoaded,
setDataSourcesSearchQuery,
setDataSourcesLayoutMode,
dataSourceTypesLoad,
dataSourceTypesLoaded,
dataSourcePluginsLoad,
dataSourcePluginsLoaded,
setDataSourceTypeSearchQuery,
dataSourceMetaLoaded,
setDataSourceName,
......@@ -76,12 +76,12 @@ describe('dataSourcesReducer', () => {
describe('when dataSourceTypesLoad is dispatched', () => {
it('then state should be correct', () => {
const state: DataSourcesState = { ...initialState, dataSourceTypes: [mockPlugin()] };
const state: DataSourcesState = { ...initialState, plugins: [mockPlugin()] };
reducerTester()
.givenReducer(dataSourcesReducer, state)
.whenActionIsDispatched(dataSourceTypesLoad())
.thenStateShouldEqual({ ...initialState, dataSourceTypes: [], isLoadingDataSources: true });
.whenActionIsDispatched(dataSourcePluginsLoad())
.thenStateShouldEqual({ ...initialState, isLoadingDataSources: true });
});
});
......@@ -92,8 +92,8 @@ describe('dataSourcesReducer', () => {
reducerTester()
.givenReducer(dataSourcesReducer, state)
.whenActionIsDispatched(dataSourceTypesLoaded(dataSourceTypes))
.thenStateShouldEqual({ ...initialState, dataSourceTypes, isLoadingDataSources: false });
.whenActionIsDispatched(dataSourcePluginsLoaded({ plugins: dataSourceTypes, categories: [] }))
.thenStateShouldEqual({ ...initialState, plugins: dataSourceTypes, isLoadingDataSources: false });
});
});
......
......@@ -5,8 +5,8 @@ import {
dataSourcesLoaded,
setDataSourcesSearchQuery,
setDataSourcesLayoutMode,
dataSourceTypesLoad,
dataSourceTypesLoaded,
dataSourcePluginsLoad,
dataSourcePluginsLoaded,
setDataSourceTypeSearchQuery,
dataSourceMetaLoaded,
setDataSourceName,
......@@ -17,11 +17,12 @@ import { reducerFactory } from 'app/core/redux';
export const initialState: DataSourcesState = {
dataSources: [],
plugins: [],
categories: [],
dataSource: {} as DataSourceSettings,
layoutMode: LayoutModes.List,
searchQuery: '',
dataSourcesCount: 0,
dataSourceTypes: [],
dataSourceTypeSearchQuery: '',
hasFetched: false,
isLoadingDataSources: false,
......@@ -51,14 +52,15 @@ export const dataSourcesReducer = reducerFactory(initialState)
mapper: (state, action) => ({ ...state, layoutMode: action.payload }),
})
.addMapper({
filter: dataSourceTypesLoad,
mapper: state => ({ ...state, dataSourceTypes: [], isLoadingDataSources: true }),
filter: dataSourcePluginsLoad,
mapper: state => ({ ...state, plugins: [], isLoadingDataSources: true }),
})
.addMapper({
filter: dataSourceTypesLoaded,
filter: dataSourcePluginsLoaded,
mapper: (state, action) => ({
...state,
dataSourceTypes: action.payload,
plugins: action.payload.plugins,
categories: action.payload.categories,
isLoadingDataSources: false,
}),
})
......
......@@ -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');
return state.dataSourceTypes.filter((type: DataSourcePluginMeta) => {
return state.plugins.filter((type: DataSourcePluginMeta) => {
return regex.test(type.name);
});
};
......
import { defaultsDeep } from 'lodash';
import { PanelPluginMeta, PluginMeta, PluginType, PanelPlugin, PanelProps } from '@grafana/data';
import { ComponentType } from 'enzyme';
......@@ -67,8 +68,8 @@ export const getPanelPlugin = (
return plugin;
};
export const getMockPlugin = () => {
return {
export function getMockPlugin(overrides?: Partial<PluginMeta>): PluginMeta {
const defaults: PluginMeta = {
defaultNavUrl: 'some/url',
enabled: false,
hasUpdate: false,
......@@ -81,7 +82,7 @@ export const getMockPlugin = () => {
description: 'pretty decent plugin',
links: [{ name: 'project', url: 'one link' }],
logos: { small: 'small/logo', large: 'large/logo' },
screenshots: [{ path: `screenshot` }],
screenshots: [{ path: `screenshot`, name: 'test' }],
updated: '2018-09-26',
version: '1',
},
......@@ -91,5 +92,7 @@ export const getMockPlugin = () => {
pinned: false,
type: PluginType.panel,
module: 'path/to/module',
} as PluginMeta;
};
};
return defaultsDeep(overrides || {}, defaults) as PluginMeta;
}
......@@ -16,7 +16,7 @@
"large": "img/logo.jpg"
},
"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" }
],
"screenshots": [
......
......@@ -29,7 +29,7 @@
"large": "img/graphite_logo.png"
},
"links": [
{ "name": "Graphite", "url": "https://graphiteapp.org/" },
{ "name": "Learn more", "url": "https://graphiteapp.org/" },
{
"name": "Graphite 1.1 Release",
"url": "https://grafana.com/blog/2018/01/11/graphite-1.1-teaching-an-old-dog-new-tricks/"
......
......@@ -39,7 +39,7 @@
},
"links": [
{
"name": "Prometheus",
"name": "Learn more",
"url": "https://prometheus.io/"
}
]
......
......@@ -7,9 +7,16 @@ export interface DataSourcesState {
dataSourceTypeSearchQuery: string;
layoutMode: LayoutMode;
dataSourcesCount: number;
dataSourceTypes: DataSourcePluginMeta[];
dataSource: DataSourceSettings;
dataSourceMeta: DataSourcePluginMeta;
hasFetched: 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