mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge remote-tracking branch 'origin/master' into stackdriver-plugin
This commit is contained in:
@@ -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(() => ({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
31
public/app/features/plugins/PluginActionBar.test.tsx
Normal file
31
public/app/features/plugins/PluginActionBar.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
62
public/app/features/plugins/PluginActionBar.tsx
Normal file
62
public/app/features/plugins/PluginActionBar.tsx
Normal 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);
|
||||
25
public/app/features/plugins/PluginList.test.tsx
Normal file
25
public/app/features/plugins/PluginList.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
32
public/app/features/plugins/PluginList.tsx
Normal file
32
public/app/features/plugins/PluginList.tsx
Normal 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;
|
||||
33
public/app/features/plugins/PluginListItem.test.tsx
Normal file
33
public/app/features/plugins/PluginListItem.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
39
public/app/features/plugins/PluginListItem.tsx
Normal file
39
public/app/features/plugins/PluginListItem.tsx
Normal 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;
|
||||
32
public/app/features/plugins/PluginListPage.test.tsx
Normal file
32
public/app/features/plugins/PluginListPage.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
56
public/app/features/plugins/PluginListPage.tsx
Normal file
56
public/app/features/plugins/PluginListPage.tsx
Normal 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));
|
||||
59
public/app/features/plugins/__mocks__/pluginMocks.ts
Normal file
59
public/app/features/plugins/__mocks__/pluginMocks.ts
Normal 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: '',
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
51
public/app/features/plugins/state/actions.ts
Normal file
51
public/app/features/plugins/state/actions.ts
Normal 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));
|
||||
};
|
||||
}
|
||||
27
public/app/features/plugins/state/reducers.ts
Normal file
27
public/app/features/plugins/state/reducers.ts
Normal 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,
|
||||
};
|
||||
31
public/app/features/plugins/state/selectors.test.ts
Normal file
31
public/app/features/plugins/state/selectors.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
10
public/app/features/plugins/state/selectors.ts
Normal file
10
public/app/features/plugins/state/selectors.ts
Normal 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;
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user