Merge remote-tracking branch 'origin/master' into stackdriver-plugin

This commit is contained in:
Daniel Lee
2018-09-28 16:20:40 +02:00
68 changed files with 1966 additions and 189 deletions

View File

@@ -29,6 +29,7 @@ describe('timeSrv', () => {
beforeEach(() => {
timeSrv = new TimeSrv(rootScope, jest.fn(), location, timer, { isGrafanaVisibile: jest.fn() });
timeSrv.init(_dashboard);
_dashboard.refresh = false;
});
describe('timeRange', () => {
@@ -79,6 +80,23 @@ describe('timeSrv', () => {
expect(time.to.valueOf()).toEqual(new Date('2014-05-20T03:10:22Z').getTime());
});
it('should ignore refresh if time absolute', () => {
location = {
search: jest.fn(() => ({
from: '20140410T052010',
to: '20140520T031022',
})),
};
timeSrv = new TimeSrv(rootScope, jest.fn(), location, timer, { isGrafanaVisibile: jest.fn() });
// dashboard saved with refresh on
_dashboard.refresh = true;
timeSrv.init(_dashboard);
expect(timeSrv.refresh).toBe(false);
});
it('should handle formatted dates without time', () => {
location = {
search: jest.fn(() => ({

View File

@@ -4,8 +4,8 @@
<label class="gf-form-label template-variable" ng-hide="variable.hide === 1">
{{variable.label || variable.name}}
</label>
<value-select-dropdown ng-if="variable.type !== 'adhoc' && variable.type !== 'text'" variable="variable" on-updated="ctrl.variableUpdated(variable)"></value-select-dropdown>
<input type="text" ng-if="variable.type === 'text'" ng-model="variable.query" class="gf-form-input width-9" ng-blur="variable.current.value != variable.query && variable.updateOptions() && ctrl.variableUpdated(variable);" ng-keydown="$event.keyCode === 13 && variable.current.value != variable.query && variable.updateOptions() && ctrl.variableUpdated(variable);" ></input>
<value-select-dropdown ng-if="variable.type !== 'adhoc' && variable.type !== 'textbox'" variable="variable" on-updated="ctrl.variableUpdated(variable)"></value-select-dropdown>
<input type="text" ng-if="variable.type === 'textbox'" ng-model="variable.query" class="gf-form-input width-12" ng-blur="variable.current.value != variable.query && variable.updateOptions() && ctrl.variableUpdated(variable);" ng-keydown="$event.keyCode === 13 && variable.current.value != variable.query && variable.updateOptions() && ctrl.variableUpdated(variable);" ></input>
</div>
<ad-hoc-filters ng-if="variable.type === 'adhoc'" variable="variable"></ad-hoc-filters>
</div>

View File

@@ -85,6 +85,12 @@ export class TimeSrv {
if (params.to) {
this.time.to = this.parseUrlParam(params.to) || this.time.to;
}
// if absolute ignore refresh option saved to dashboard
if (params.to && params.to.indexOf('now') === -1) {
this.refresh = false;
this.dashboard.refresh = false;
}
// but if refresh explicitly set then use that
if (params.refresh) {
this.refresh = params.refresh || this.refresh;
}

View File

@@ -528,10 +528,11 @@ export class Explore extends React.Component<any, ExploreState> {
{!datasourceMissing ? (
<div className="navbar-buttons">
<Select
className="datasource-picker"
clearable={false}
className="gf-form-input gf-form-input--form-dropdown datasource-picker"
onChange={this.onChangeDatasource}
options={datasources}
isOpen={true}
placeholder="Loading datasources..."
value={selectedDatasource}
/>
@@ -586,17 +587,17 @@ export class Explore extends React.Component<any, ExploreState> {
/>
<div className="result-options">
{supportsGraph ? (
<button className={`btn navbar-button ${graphButtonActive}`} onClick={this.onClickGraphButton}>
<button className={`btn toggle-btn ${graphButtonActive}`} onClick={this.onClickGraphButton}>
Graph
</button>
) : null}
{supportsTable ? (
<button className={`btn navbar-button ${tableButtonActive}`} onClick={this.onClickTableButton}>
<button className={`btn toggle-btn ${tableButtonActive}`} onClick={this.onClickTableButton}>
Table
</button>
) : null}
{supportsLogs ? (
<button className={`btn navbar-button ${logsButtonActive}`} onClick={this.onClickLogsButton}>
<button className={`btn toggle-btn ${logsButtonActive}`} onClick={this.onClickLogsButton}>
Logs
</button>
) : null}

View File

@@ -2,16 +2,25 @@
<div class="page-container page-body">
<div class="page-action-bar">
<label class="gf-form gf-form--grow gf-form--has-input-icon">
<label class="gf-form gf-form--has-input-icon">
<input type="text" class="gf-form-input width-20" ng-model="ctrl.searchQuery" ng-change="ctrl.onQueryUpdated()" placeholder="Filter by username or email" />
<i class="gf-form-input-icon fa fa-search"></i>
</label>
<div class="page-action-bar__spacer"></div>
<div ng-if="ctrl.pendingInvites.length" style="margin-left: 1rem">
<button class="btn toggle-btn active" ng-if="!ctrl.showInvites">
Users
</button><button class="btn toggle-btn" ng-if="!ctrl.showInvites" ng-click="ctrl.showInvites = true">
Pending Invites ({{ctrl.pendingInvites.length}})
</button>
<button class="btn toggle-btn" ng-if="ctrl.showInvites" ng-click="ctrl.showInvites = false">
Users
</button><button class="btn toggle-btn active" ng-if="ctrl.showInvites">
Pending Invites ({{ctrl.pendingInvites.length}})
</button>
</div>
<button class="btn btn-inverse" ng-show="ctrl.pendingInvites.length" ng-click="ctrl.showInvites = true">
Pending Invites ({{ctrl.pendingInvites.length}})
</button>
<div class="page-action-bar__spacer"></div>
<a class="btn btn-success" href="org/users/invite" ng-show="ctrl.canInvite">
<i class="fa fa-plus"></i>

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { shallow } from 'enzyme';
import { PluginActionBar, Props } from './PluginActionBar';
import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
const setup = (propOverrides?: object) => {
const props: Props = {
searchQuery: '',
layoutMode: LayoutModes.Grid,
setLayoutMode: jest.fn(),
setPluginsSearchQuery: jest.fn(),
};
Object.assign(props, propOverrides);
const wrapper = shallow(<PluginActionBar {...props} />);
const instance = wrapper.instance() as PluginActionBar;
return {
wrapper,
instance,
};
};
describe('Render', () => {
it('should render component', () => {
const { wrapper } = setup();
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,62 @@
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import LayoutSelector, { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector';
import { setLayoutMode, setPluginsSearchQuery } from './state/actions';
import { getPluginsSearchQuery, getLayoutMode } from './state/selectors';
export interface Props {
searchQuery: string;
layoutMode: LayoutMode;
setLayoutMode: typeof setLayoutMode;
setPluginsSearchQuery: typeof setPluginsSearchQuery;
}
export class PluginActionBar extends PureComponent<Props> {
onSearchQueryChange = event => {
this.props.setPluginsSearchQuery(event.target.value);
};
render() {
const { searchQuery, layoutMode, setLayoutMode } = this.props;
return (
<div className="page-action-bar">
<div className="gf-form gf-form--grow">
<label className="gf-form--has-input-icon">
<input
type="text"
className="gf-form-input width-20"
value={searchQuery}
onChange={this.onSearchQueryChange}
placeholder="Filter by name or type"
/>
<i className="gf-form-input-icon fa fa-search" />
</label>
<LayoutSelector mode={layoutMode} onLayoutModeChanged={(mode: LayoutMode) => setLayoutMode(mode)} />
</div>
<div className="page-action-bar__spacer" />
<a
className="btn btn-success"
href="https://grafana.com/plugins?utm_source=grafana_plugin_list"
target="_blank"
>
Find more plugins on Grafana.com
</a>
</div>
);
}
}
function mapStateToProps(state) {
return {
searchQuery: getPluginsSearchQuery(state.plugins),
layoutMode: getLayoutMode(state.plugins),
};
}
const mapDispatchToProps = {
setPluginsSearchQuery,
setLayoutMode,
};
export default connect(mapStateToProps, mapDispatchToProps)(PluginActionBar);

View File

@@ -0,0 +1,25 @@
import React from 'react';
import { shallow } from 'enzyme';
import PluginList from './PluginList';
import { getMockPlugins } from './__mocks__/pluginMocks';
import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
const setup = (propOverrides?: object) => {
const props = Object.assign(
{
plugins: getMockPlugins(5),
layoutMode: LayoutModes.Grid,
},
propOverrides
);
return shallow(<PluginList {...props} />);
};
describe('Render', () => {
it('should render component', () => {
const wrapper = setup();
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,32 @@
import React, { SFC } from 'react';
import classNames from 'classnames/bind';
import PluginListItem from './PluginListItem';
import { Plugin } from 'app/types';
import { LayoutMode, LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
interface Props {
plugins: Plugin[];
layoutMode: LayoutMode;
}
const PluginList: SFC<Props> = props => {
const { plugins, layoutMode } = props;
const listStyle = classNames({
'card-section': true,
'card-list-layout-grid': layoutMode === LayoutModes.Grid,
'card-list-layout-list': layoutMode === LayoutModes.List,
});
return (
<section className={listStyle}>
<ol className="card-list">
{plugins.map((plugin, index) => {
return <PluginListItem plugin={plugin} key={`${plugin.name}-${index}`} />;
})}
</ol>
</section>
);
};
export default PluginList;

View File

@@ -0,0 +1,33 @@
import React from 'react';
import { shallow } from 'enzyme';
import PluginListItem from './PluginListItem';
import { getMockPlugin } from './__mocks__/pluginMocks';
const setup = (propOverrides?: object) => {
const props = Object.assign(
{
plugin: getMockPlugin(),
},
propOverrides
);
return shallow(<PluginListItem {...props} />);
};
describe('Render', () => {
it('should render component', () => {
const wrapper = setup();
expect(wrapper).toMatchSnapshot();
});
it('should render has plugin section', () => {
const mockPlugin = getMockPlugin();
mockPlugin.hasUpdate = true;
const wrapper = setup({
plugin: mockPlugin,
});
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,39 @@
import React, { SFC } from 'react';
import { Plugin } from 'app/types';
interface Props {
plugin: Plugin;
}
const PluginListItem: SFC<Props> = props => {
const { plugin } = props;
return (
<li className="card-item-wrapper">
<a className="card-item" href={`plugins/${plugin.id}/edit`}>
<div className="card-item-header">
<div className="card-item-type">
<i className={`icon-gf icon-gf-${plugin.type}`} />
{plugin.type}
</div>
{plugin.hasUpdate && (
<div className="card-item-notice">
<span bs-tooltip="plugin.latestVersion">Update available!</span>
</div>
)}
</div>
<div className="card-item-body">
<figure className="card-item-figure">
<img src={plugin.info.logos.small} />
</figure>
<div className="card-item-details">
<div className="card-item-name">{plugin.name}</div>
<div className="card-item-sub-name">{`By ${plugin.info.author.name}`}</div>
</div>
</div>
</a>
</li>
);
};
export default PluginListItem;

View File

@@ -0,0 +1,32 @@
import React from 'react';
import { shallow } from 'enzyme';
import { PluginListPage, Props } from './PluginListPage';
import { NavModel, Plugin } from '../../types';
import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
const setup = (propOverrides?: object) => {
const props: Props = {
navModel: {} as NavModel,
plugins: [] as Plugin[],
layoutMode: LayoutModes.Grid,
loadPlugins: jest.fn(),
};
Object.assign(props, propOverrides);
const wrapper = shallow(<PluginListPage {...props} />);
const instance = wrapper.instance() as PluginListPage;
return {
wrapper,
instance,
};
};
describe('Render', () => {
it('should render component', () => {
const { wrapper } = setup();
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,56 @@
import React, { PureComponent } from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import PageHeader from '../../core/components/PageHeader/PageHeader';
import PluginActionBar from './PluginActionBar';
import PluginList from './PluginList';
import { NavModel, Plugin } from '../../types';
import { loadPlugins } from './state/actions';
import { getNavModel } from '../../core/selectors/navModel';
import { getLayoutMode, getPlugins } from './state/selectors';
import { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector';
export interface Props {
navModel: NavModel;
plugins: Plugin[];
layoutMode: LayoutMode;
loadPlugins: typeof loadPlugins;
}
export class PluginListPage extends PureComponent<Props> {
componentDidMount() {
this.fetchPlugins();
}
async fetchPlugins() {
await this.props.loadPlugins();
}
render() {
const { navModel, plugins, layoutMode } = this.props;
return (
<div>
<PageHeader model={navModel} />
<div className="page-container page-body">
<PluginActionBar />
{plugins && <PluginList plugins={plugins} layoutMode={layoutMode} />}
</div>
</div>
);
}
}
function mapStateToProps(state) {
return {
navModel: getNavModel(state.navIndex, 'plugins'),
plugins: getPlugins(state.plugins),
layoutMode: getLayoutMode(state.plugins),
};
}
const mapDispatchToProps = {
loadPlugins,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(PluginListPage));

View File

@@ -0,0 +1,59 @@
import { Plugin } from 'app/types';
export const getMockPlugins = (amount: number): Plugin[] => {
const plugins = [];
for (let i = 0; i <= amount; i++) {
plugins.push({
defaultNavUrl: 'some/url',
enabled: false,
hasUpdate: false,
id: `${i}`,
info: {
author: {
name: 'Grafana Labs',
url: 'url/to/GrafanaLabs',
},
description: 'pretty decent plugin',
links: ['one link'],
logos: { small: 'small/logo', large: 'large/logo' },
screenshots: `screenshot/${i}`,
updated: '2018-09-26',
version: '1',
},
latestVersion: `1.${i}`,
name: `pretty cool plugin-${i}`,
pinned: false,
state: '',
type: '',
});
}
return plugins;
};
export const getMockPlugin = () => {
return {
defaultNavUrl: 'some/url',
enabled: false,
hasUpdate: false,
id: '1',
info: {
author: {
name: 'Grafana Labs',
url: 'url/to/GrafanaLabs',
},
description: 'pretty decent plugin',
links: ['one link'],
logos: { small: 'small/logo', large: 'large/logo' },
screenshots: 'screenshot/1',
updated: '2018-09-26',
version: '1',
},
latestVersion: '1',
name: 'pretty cool plugin 1',
pinned: false,
state: '',
type: '',
};
};

View File

@@ -0,0 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<div
className="page-action-bar"
>
<div
className="gf-form gf-form--grow"
>
<label
className="gf-form--has-input-icon"
>
<input
className="gf-form-input width-20"
onChange={[Function]}
placeholder="Filter by name or type"
type="text"
value=""
/>
<i
className="gf-form-input-icon fa fa-search"
/>
</label>
<LayoutSelector
mode="grid"
onLayoutModeChanged={[Function]}
/>
</div>
<div
className="page-action-bar__spacer"
/>
<a
className="btn btn-success"
href="https://grafana.com/plugins?utm_source=grafana_plugin_list"
target="_blank"
>
Find more plugins on Grafana.com
</a>
</div>
`;

View File

@@ -0,0 +1,210 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<section
className="card-section card-list-layout-grid"
>
<ol
className="card-list"
>
<PluginListItem
key="pretty cool plugin-0-0"
plugin={
Object {
"defaultNavUrl": "some/url",
"enabled": false,
"hasUpdate": false,
"id": "0",
"info": Object {
"author": Object {
"name": "Grafana Labs",
"url": "url/to/GrafanaLabs",
},
"description": "pretty decent plugin",
"links": Array [
"one link",
],
"logos": Object {
"large": "large/logo",
"small": "small/logo",
},
"screenshots": "screenshot/0",
"updated": "2018-09-26",
"version": "1",
},
"latestVersion": "1.0",
"name": "pretty cool plugin-0",
"pinned": false,
"state": "",
"type": "",
}
}
/>
<PluginListItem
key="pretty cool plugin-1-1"
plugin={
Object {
"defaultNavUrl": "some/url",
"enabled": false,
"hasUpdate": false,
"id": "1",
"info": Object {
"author": Object {
"name": "Grafana Labs",
"url": "url/to/GrafanaLabs",
},
"description": "pretty decent plugin",
"links": Array [
"one link",
],
"logos": Object {
"large": "large/logo",
"small": "small/logo",
},
"screenshots": "screenshot/1",
"updated": "2018-09-26",
"version": "1",
},
"latestVersion": "1.1",
"name": "pretty cool plugin-1",
"pinned": false,
"state": "",
"type": "",
}
}
/>
<PluginListItem
key="pretty cool plugin-2-2"
plugin={
Object {
"defaultNavUrl": "some/url",
"enabled": false,
"hasUpdate": false,
"id": "2",
"info": Object {
"author": Object {
"name": "Grafana Labs",
"url": "url/to/GrafanaLabs",
},
"description": "pretty decent plugin",
"links": Array [
"one link",
],
"logos": Object {
"large": "large/logo",
"small": "small/logo",
},
"screenshots": "screenshot/2",
"updated": "2018-09-26",
"version": "1",
},
"latestVersion": "1.2",
"name": "pretty cool plugin-2",
"pinned": false,
"state": "",
"type": "",
}
}
/>
<PluginListItem
key="pretty cool plugin-3-3"
plugin={
Object {
"defaultNavUrl": "some/url",
"enabled": false,
"hasUpdate": false,
"id": "3",
"info": Object {
"author": Object {
"name": "Grafana Labs",
"url": "url/to/GrafanaLabs",
},
"description": "pretty decent plugin",
"links": Array [
"one link",
],
"logos": Object {
"large": "large/logo",
"small": "small/logo",
},
"screenshots": "screenshot/3",
"updated": "2018-09-26",
"version": "1",
},
"latestVersion": "1.3",
"name": "pretty cool plugin-3",
"pinned": false,
"state": "",
"type": "",
}
}
/>
<PluginListItem
key="pretty cool plugin-4-4"
plugin={
Object {
"defaultNavUrl": "some/url",
"enabled": false,
"hasUpdate": false,
"id": "4",
"info": Object {
"author": Object {
"name": "Grafana Labs",
"url": "url/to/GrafanaLabs",
},
"description": "pretty decent plugin",
"links": Array [
"one link",
],
"logos": Object {
"large": "large/logo",
"small": "small/logo",
},
"screenshots": "screenshot/4",
"updated": "2018-09-26",
"version": "1",
},
"latestVersion": "1.4",
"name": "pretty cool plugin-4",
"pinned": false,
"state": "",
"type": "",
}
}
/>
<PluginListItem
key="pretty cool plugin-5-5"
plugin={
Object {
"defaultNavUrl": "some/url",
"enabled": false,
"hasUpdate": false,
"id": "5",
"info": Object {
"author": Object {
"name": "Grafana Labs",
"url": "url/to/GrafanaLabs",
},
"description": "pretty decent plugin",
"links": Array [
"one link",
],
"logos": Object {
"large": "large/logo",
"small": "small/logo",
},
"screenshots": "screenshot/5",
"updated": "2018-09-26",
"version": "1",
},
"latestVersion": "1.5",
"name": "pretty cool plugin-5",
"pinned": false,
"state": "",
"type": "",
}
}
/>
</ol>
</section>
`;

View File

@@ -0,0 +1,106 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<li
className="card-item-wrapper"
>
<a
className="card-item"
href="plugins/1/edit"
>
<div
className="card-item-header"
>
<div
className="card-item-type"
>
<i
className="icon-gf icon-gf-"
/>
</div>
</div>
<div
className="card-item-body"
>
<figure
className="card-item-figure"
>
<img
src="small/logo"
/>
</figure>
<div
className="card-item-details"
>
<div
className="card-item-name"
>
pretty cool plugin 1
</div>
<div
className="card-item-sub-name"
>
By Grafana Labs
</div>
</div>
</div>
</a>
</li>
`;
exports[`Render should render has plugin section 1`] = `
<li
className="card-item-wrapper"
>
<a
className="card-item"
href="plugins/1/edit"
>
<div
className="card-item-header"
>
<div
className="card-item-type"
>
<i
className="icon-gf icon-gf-"
/>
</div>
<div
className="card-item-notice"
>
<span
bs-tooltip="plugin.latestVersion"
>
Update available!
</span>
</div>
</div>
<div
className="card-item-body"
>
<figure
className="card-item-figure"
>
<img
src="small/logo"
/>
</figure>
<div
className="card-item-details"
>
<div
className="card-item-name"
>
pretty cool plugin 1
</div>
<div
className="card-item-sub-name"
>
By Grafana Labs
</div>
</div>
</div>
</a>
</li>
`;

View File

@@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<div>
<PageHeader
model={Object {}}
/>
<div
className="page-container page-body"
>
<Connect(PluginActionBar) />
<PluginList
layoutMode="grid"
plugins={Array []}
/>
</div>
</div>
`;

View File

@@ -1,6 +1,5 @@
import './plugin_edit_ctrl';
import './plugin_page_ctrl';
import './plugin_list_ctrl';
import './import_list/import_list';
import './ds_edit_ctrl';
import './ds_dashboards_ctrl';

View File

@@ -1,45 +0,0 @@
<page-header model="ctrl.navModel"></page-header>
<div class="page-container page-body">
<div class="page-action-bar">
<div class="gf-form gf-form--grow">
<label class="gf-form--has-input-icon">
<input type="text" class="gf-form-input width-20" ng-model="ctrl.searchQuery" ng-change="ctrl.onQueryUpdated()" placeholder="Filter by name or type" />
<i class="gf-form-input-icon fa fa-search"></i>
</label>
<layout-selector />
</div>
<div class="page-action-bar__spacer"></div>
<a class="btn btn-success" href="https://grafana.com/plugins?utm_source=grafana_plugin_list" target="_blank">
Find more plugins on Grafana.com
</a>
</div>
<section class="card-section" layout-mode>
<ol class="card-list" >
<li class="card-item-wrapper" ng-repeat="plugin in ctrl.plugins">
<a class="card-item" href="plugins/{{plugin.id}}/edit">
<div class="card-item-header">
<div class="card-item-type">
<i class="icon-gf icon-gf-{{plugin.type}}"></i>
{{plugin.type}}
</div>
<div class="card-item-notice" ng-show="plugin.hasUpdate">
<span bs-tooltip="plugin.latestVersion">Update available!</span>
</div>
</div>
<div class="card-item-body">
<figure class="card-item-figure">
<img ng-src="{{plugin.info.logos.small}}">
</figure>
<div class="card-item-details">
<div class="card-item-name">{{plugin.name}}</div>
<div class="card-item-sub-name">By {{plugin.info.author.name}}</div>
</div>
</div>
</a>
</li>
</ol>
</section>
</div>

View File

@@ -1,30 +0,0 @@
import angular from 'angular';
import _ from 'lodash';
export class PluginListCtrl {
plugins: any[];
tabIndex: number;
navModel: any;
searchQuery: string;
allPlugins: any[];
/** @ngInject */
constructor(private backendSrv: any, $location, navModelSrv) {
this.tabIndex = 0;
this.navModel = navModelSrv.getNav('cfg', 'plugins', 0);
this.backendSrv.get('api/plugins', { embedded: 0 }).then(plugins => {
this.plugins = plugins;
this.allPlugins = plugins;
});
}
onQueryUpdated() {
const regex = new RegExp(this.searchQuery, 'ig');
this.plugins = _.filter(this.allPlugins, item => {
return regex.test(item.name) || regex.test(item.type);
});
}
}
angular.module('grafana.controllers').controller('PluginListCtrl', PluginListCtrl);

View File

@@ -0,0 +1,51 @@
import { Plugin, StoreState } from 'app/types';
import { ThunkAction } from 'redux-thunk';
import { getBackendSrv } from '../../../core/services/backend_srv';
import { LayoutMode } from '../../../core/components/LayoutSelector/LayoutSelector';
export enum ActionTypes {
LoadPlugins = 'LOAD_PLUGINS',
SetPluginsSearchQuery = 'SET_PLUGIN_SEARCH_QUERY',
SetLayoutMode = 'SET_LAYOUT_MODE',
}
export interface LoadPluginsAction {
type: ActionTypes.LoadPlugins;
payload: Plugin[];
}
export interface SetPluginsSearchQueryAction {
type: ActionTypes.SetPluginsSearchQuery;
payload: string;
}
export interface SetLayoutModeAction {
type: ActionTypes.SetLayoutMode;
payload: LayoutMode;
}
export const setLayoutMode = (mode: LayoutMode): SetLayoutModeAction => ({
type: ActionTypes.SetLayoutMode,
payload: mode,
});
export const setPluginsSearchQuery = (query: string): SetPluginsSearchQueryAction => ({
type: ActionTypes.SetPluginsSearchQuery,
payload: query,
});
const pluginsLoaded = (plugins: Plugin[]): LoadPluginsAction => ({
type: ActionTypes.LoadPlugins,
payload: plugins,
});
export type Action = LoadPluginsAction | SetPluginsSearchQueryAction | SetLayoutModeAction;
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
export function loadPlugins(): ThunkResult<void> {
return async dispatch => {
const result = await getBackendSrv().get('api/plugins', { embedded: 0 });
dispatch(pluginsLoaded(result));
};
}

View File

@@ -0,0 +1,27 @@
import { Action, ActionTypes } from './actions';
import { Plugin, PluginsState } from 'app/types';
import { LayoutModes } from '../../../core/components/LayoutSelector/LayoutSelector';
export const initialState: PluginsState = {
plugins: [] as Plugin[],
searchQuery: '',
layoutMode: LayoutModes.Grid,
};
export const pluginsReducer = (state = initialState, action: Action): PluginsState => {
switch (action.type) {
case ActionTypes.LoadPlugins:
return { ...state, plugins: action.payload };
case ActionTypes.SetPluginsSearchQuery:
return { ...state, searchQuery: action.payload };
case ActionTypes.SetLayoutMode:
return { ...state, layoutMode: action.payload };
}
return state;
};
export default {
plugins: pluginsReducer,
};

View File

@@ -0,0 +1,31 @@
import { getPlugins, getPluginsSearchQuery } from './selectors';
import { initialState } from './reducers';
import { getMockPlugins } from '../__mocks__/pluginMocks';
describe('Selectors', () => {
const mockState = initialState;
it('should return search query', () => {
mockState.searchQuery = 'test';
const query = getPluginsSearchQuery(mockState);
expect(query).toEqual(mockState.searchQuery);
});
it('should return plugins', () => {
mockState.plugins = getMockPlugins(5);
mockState.searchQuery = '';
const plugins = getPlugins(mockState);
expect(plugins).toEqual(mockState.plugins);
});
it('should filter plugins', () => {
mockState.searchQuery = 'plugin-1';
const plugins = getPlugins(mockState);
expect(plugins.length).toEqual(1);
});
});

View File

@@ -0,0 +1,10 @@
export const getPlugins = state => {
const regex = new RegExp(state.searchQuery, 'i');
return state.plugins.filter(item => {
return regex.test(item.name) || regex.test(item.info.author.name) || regex.test(item.info.description);
});
};
export const getPluginsSearchQuery = state => state.searchQuery;
export const getLayoutMode = state => state.layoutMode;

View File

@@ -1,13 +1,13 @@
import { Variable, assignModelProperties, variableTypes } from './variable';
export class TextVariable implements Variable {
export class TextBoxVariable implements Variable {
query: string;
current: any;
options: any[];
skipUrlSync: boolean;
defaults = {
type: 'text',
type: 'textbox',
name: '',
hide: 2,
label: '',
@@ -51,8 +51,8 @@ export class TextVariable implements Variable {
}
}
variableTypes['text'] = {
name: 'Text',
ctor: TextVariable,
variableTypes['textbox'] = {
name: 'Text box',
ctor: TextBoxVariable,
description: 'Define a textbox variable, where users can enter any arbitrary string',
};

View File

@@ -9,7 +9,7 @@ import { DatasourceVariable } from './datasource_variable';
import { CustomVariable } from './custom_variable';
import { ConstantVariable } from './constant_variable';
import { AdhocVariable } from './adhoc_variable';
import { TextVariable } from './text_variable';
import { TextBoxVariable } from './TextBoxVariable';
coreModule.factory('templateSrv', () => {
return templateSrv;
@@ -23,5 +23,5 @@ export {
CustomVariable,
ConstantVariable,
AdhocVariable,
TextVariable
TextBoxVariable,
};

View File

@@ -155,10 +155,10 @@
</div>
</div>
<div ng-if="current.type === 'text'" class="gf-form-group">
<div ng-if="current.type === 'textbox'" class="gf-form-group">
<h5 class="section-heading">Text options</h5>
<div class="gf-form">
<span class="gf-form-label">Value</span>
<span class="gf-form-label">Default value</span>
<input type="text" class="gf-form-input" ng-model='current.query' ng-blur="runQuery()" placeholder="default value, if any"></input>
</div>
</div>