Commit 3081e0f8 by Peter Holmberg

Merge branch 'master' into 13411-react-api-key

parents 362010c4 abefadb3
......@@ -14,6 +14,7 @@
* **Prometheus**: Adhoc-filtering for Prometheus dashboards [#13212](https://github.com/grafana/grafana/issues/13212)
* **Singlestat**: Fix gauge display accuracy for percents [#13270](https://github.com/grafana/grafana/issues/13270), thx [@tianon](https://github.com/tianon)
* **Dashboard**: Prevent auto refresh from starting when loading dashboard with absolute time range [#12030](https://github.com/grafana/grafana/issues/12030)
* **Templating**: New templating variable type `Text box` that allows free text input [#3173](https://github.com/grafana/grafana/issues/3173)
# 5.3.0 (unreleased)
......
......@@ -235,7 +235,7 @@ func parseMultiSelectValue(input string) []string {
func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
regions := []string{
"ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-south-1", "ca-central-1", "cn-north-1", "cn-northwest-1",
"eu-central-1", "eu-west-1", "eu-west-2", "eu-west-3", "sa-east-1", "us-east-1", "us-east-2", "us-gov-west-1", "us-west-1", "us-west-2",
"eu-central-1", "eu-west-1", "eu-west-2", "eu-west-3", "sa-east-1", "us-east-1", "us-east-2", "us-gov-west-1", "us-west-1", "us-west-2", "us-isob-east-1", "us-iso-east-1",
}
result := make([]suggestData, 0)
......
import React, { SFC } from 'react';
export type LayoutMode = LayoutModes.Grid | LayoutModes.List;
export enum LayoutModes {
Grid = 'grid',
List = 'list',
}
interface Props {
mode: LayoutMode;
onLayoutModeChanged: (mode: LayoutMode) => {};
}
const LayoutSelector: SFC<Props> = props => {
const { mode, onLayoutModeChanged } = props;
return (
<div className="layout-selector">
<button
onClick={() => {
onLayoutModeChanged(LayoutModes.List);
}}
className={mode === LayoutModes.List ? 'active' : ''}
>
<i className="fa fa-list" />
</button>
<button
onClick={() => {
onLayoutModeChanged(LayoutModes.Grid);
}}
className={mode === LayoutModes.Grid ? 'active' : ''}
>
<i className="fa fa-th" />
</button>
</div>
);
};
export default LayoutSelector;
......@@ -4,7 +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="variable" on-updated="ctrl.variableUpdated(variable)"></value-select-dropdown>
<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>
......
......@@ -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}
......
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();
});
});
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);
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();
});
});
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;
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();
});
});
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;
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();
});
});
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));
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: '',
};
};
// 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>
`;
// 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>
`;
// 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>
`;
// 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>
`;
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';
......
<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>
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);
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));
};
}
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,
};
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);
});
});
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;
import { Variable, assignModelProperties, variableTypes } from './variable';
export class TextBoxVariable implements Variable {
query: string;
current: any;
options: any[];
skipUrlSync: boolean;
defaults = {
type: 'textbox',
name: '',
hide: 2,
label: '',
query: '',
current: {},
options: [],
skipUrlSync: false,
};
/** @ngInject */
constructor(private model, private variableSrv) {
assignModelProperties(this, model, this.defaults);
}
getSaveModel() {
assignModelProperties(this.model, this, this.defaults);
return this.model;
}
setValue(option) {
this.variableSrv.setOptionAsCurrent(this, option);
}
updateOptions() {
this.options = [{ text: this.query.trim(), value: this.query.trim() }];
this.current = this.options[0];
return Promise.resolve();
}
dependsOn(variable) {
return false;
}
setValueFromUrl(urlValue) {
this.query = urlValue;
return this.variableSrv.setOptionFromUrl(this, urlValue);
}
getValueForUrl() {
return this.current.value;
}
}
variableTypes['textbox'] = {
name: 'Text box',
ctor: TextBoxVariable,
description: 'Define a textbox variable, where users can enter any arbitrary string',
};
......@@ -9,6 +9,7 @@ import { DatasourceVariable } from './datasource_variable';
import { CustomVariable } from './custom_variable';
import { ConstantVariable } from './constant_variable';
import { AdhocVariable } from './adhoc_variable';
import { TextBoxVariable } from './TextBoxVariable';
coreModule.factory('templateSrv', () => {
return templateSrv;
......@@ -22,4 +23,5 @@ export {
CustomVariable,
ConstantVariable,
AdhocVariable,
TextBoxVariable,
};
......@@ -155,6 +155,14 @@
</div>
</div>
<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">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>
<div ng-if="current.type === 'query'" class="gf-form-group">
<h5 class="section-heading">Query Options</h5>
......
......@@ -39,7 +39,7 @@
<div class="gf-form">
<label class="gf-form-label width-13">Default Region</label>
<div class="gf-form-select-wrapper max-width-18 gf-form-select-wrapper--has-help-icon">
<select class="gf-form-input" ng-model="ctrl.current.jsonData.defaultRegion" ng-options="region for region in ['ap-northeast-1', 'ap-northeast-2', 'ap-southeast-1', 'ap-southeast-2', 'ap-south-1', 'ca-central-1', 'cn-north-1', 'cn-northwest-1', 'eu-central-1', 'eu-west-1', 'eu-west-2', 'eu-west-3', 'sa-east-1', 'us-east-1', 'us-east-2', 'us-gov-west-1', 'us-west-1', 'us-west-2']"></select>
<select class="gf-form-input" ng-model="ctrl.current.jsonData.defaultRegion" ng-options="region for region in ['ap-northeast-1', 'ap-northeast-2', 'ap-southeast-1', 'ap-southeast-2', 'ap-south-1', 'ca-central-1', 'cn-north-1', 'cn-northwest-1', 'eu-central-1', 'eu-west-1', 'eu-west-2', 'eu-west-3', 'sa-east-1', 'us-east-1', 'us-east-2', 'us-gov-west-1', 'us-west-1', 'us-west-2', 'us-isob-east-1', 'us-iso-east-1']"></select>
<info-popover mode="right-absolute">
Specify the region, such as for US West (Oregon) use ` us-west-2 ` as the region.
</info-popover>
......
......@@ -6,6 +6,7 @@ import AlertRuleList from 'app/features/alerting/AlertRuleList';
import TeamPages from 'app/features/teams/TeamPages';
import TeamList from 'app/features/teams/TeamList';
import ApiKeys from 'app/features/api-keys/ApiKeysPage';
import PluginListPage from 'app/features/plugins/PluginListPage';
import FolderSettingsPage from 'app/features/folders/FolderSettingsPage';
import FolderPermissions from 'app/features/folders/FolderPermissions';
......@@ -249,9 +250,10 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
controllerAs: 'ctrl',
})
.when('/plugins', {
templateUrl: 'public/app/features/plugins/partials/plugin_list.html',
controller: 'PluginListCtrl',
controllerAs: 'ctrl',
template: '<react-container />',
resolve: {
component: () => PluginListPage,
},
})
.when('/plugins/:pluginId/edit', {
templateUrl: 'public/app/features/plugins/partials/plugin_edit.html',
......
......@@ -7,6 +7,7 @@ import teamsReducers from 'app/features/teams/state/reducers';
import apiKeysReducers from 'app/features/api-keys/state/reducers';
import foldersReducers from 'app/features/folders/state/reducers';
import dashboardReducers from 'app/features/dashboard/state/reducers';
import pluginReducers from 'app/features/plugins/state/reducers';
const rootReducer = combineReducers({
...sharedReducers,
......@@ -15,6 +16,7 @@ const rootReducer = combineReducers({
...apiKeysReducers,
...foldersReducers,
...dashboardReducers,
...pluginReducers,
});
export let store;
......
......@@ -6,9 +6,9 @@ import { FolderDTO, FolderState, FolderInfo } from './folders';
import { DashboardState } from './dashboard';
import { DashboardAcl, OrgRole, PermissionLevel } from './acl';
import { DataSource } from './datasources';
import { PluginMeta } from './plugins';
import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys';
import { User } from './user';
import { PluginMeta, Plugin, PluginsState } from './plugins';
export {
Team,
......@@ -39,6 +39,8 @@ export {
ApiKeysState,
NewApiKey,
User,
Plugin,
PluginsState,
};
export interface StoreState {
......
......@@ -12,8 +12,36 @@ export interface PluginInclude {
}
export interface PluginMetaInfo {
author: {
name: string;
url: string;
};
description: string;
links: string[];
logos: {
large: string;
small: string;
};
screenshots: string;
updated: string;
version: string;
}
export interface Plugin {
defaultNavUrl: string;
enabled: boolean;
hasUpdate: boolean;
id: string;
info: PluginMetaInfo;
latestVersion: string;
name: string;
pinned: boolean;
state: string;
type: string;
}
export interface PluginsState {
plugins: Plugin[];
searchQuery: string;
layoutMode: string;
}
......@@ -3,7 +3,7 @@ $select-menu-max-height: 300px;
$select-item-font-size: $font-size-base;
$select-item-bg: $dropdownBackground;
$select-item-fg: $input-color;
$select-option-bg: $dropdownBackground;
$select-option-bg: $menu-dropdown-bg;
$select-option-color: $input-color;
$select-noresults-color: $text-color;
$select-input-bg: $input-bg;
......@@ -82,20 +82,14 @@ $select-option-selected-bg: $dropdownLinkBackgroundActive;
width: auto;
}
.Select-option {
border-left: 2px solid transparent;
}
.Select-option.is-focused {
background-color: $dropdownLinkBackgroundHover;
color: $dropdownLinkColorHover;
&::before {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 2px;
display: block;
content: '';
background-image: linear-gradient(to bottom, #ffd500 0%, #ff4400 99%, #ff4400 100%);
}
@include left-brand-border-gradient();
}
}
......
......@@ -69,7 +69,7 @@
}
.datasource-picker {
min-width: 10rem;
min-width: 200px;
}
.timepicker {
......
......@@ -275,7 +275,10 @@
document.head.insertBefore(myCSS, document.head.childNodes[document.head.childNodes.length - 1].nextSibling);
// switch loader to show all has loaded
window.onload = function() {
document.getElementsByClassName("preloader")[0].className = "preloader preloader--done";
var preloader = document.getElementsByClassName("preloader");
if (preloader.length) {
preloader[0].className = "preloader preloader--done";
}
};
</script>
......
// Lint and build CSS
module.exports = function(grunt) {
module.exports = function (grunt) {
'use strict';
grunt.registerTask('default', [
......@@ -18,15 +18,16 @@ module.exports = function(grunt) {
grunt.registerTask('precommit', [
'sasslint',
'exec:tslint',
'exec:tsc',
'no-only-tests'
]);
grunt.registerTask('no-only-tests', function() {
grunt.registerTask('no-only-tests', function () {
var files = grunt.file.expand('public/**/*_specs\.ts', 'public/**/*_specs\.js');
files.forEach(function(spec) {
files.forEach(function (spec) {
var rows = grunt.file.read(spec).split('\n');
rows.forEach(function(row) {
rows.forEach(function (row) {
if (row.indexOf('.only(') > 0) {
grunt.log.errorlns(row);
grunt.fail.warn('found only statement in test: ' + spec)
......
module.exports = function(config, grunt) {
module.exports = function (config, grunt) {
'use strict';
return {
tslint: 'node ./node_modules/tslint/lib/tslintCli.js -c tslint.json --project ./tsconfig.json',
tsc: 'yarn tsc --noEmit',
jest: 'node ./node_modules/jest-cli/bin/jest.js --maxWorkers 2',
webpack: 'node ./node_modules/webpack/bin/webpack.js --config scripts/webpack/webpack.prod.js',
};
......
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