mirror of https://github.com/grafana/grafana
commit
3081e0f84a
@ -0,0 +1,39 @@ |
||||
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; |
@ -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(); |
||||
}); |
||||
}); |
@ -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); |
@ -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(); |
||||
}); |
||||
}); |
@ -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; |
@ -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(); |
||||
}); |
||||
}); |
@ -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; |
@ -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(); |
||||
}); |
||||
}); |
@ -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)); |
@ -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,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); |
@ -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)); |
||||
}; |
||||
} |
@ -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, |
||||
}; |
@ -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); |
||||
}); |
||||
}); |
@ -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; |
@ -0,0 +1,58 @@ |
||||
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', |
||||
}; |
Loading…
Reference in new issue