diff --git a/public/app/features/plugins/PluginActionBar.tsx b/public/app/features/plugins/PluginActionBar.tsx
new file mode 100644
index 00000000000..e420bc3eca6
--- /dev/null
+++ b/public/app/features/plugins/PluginActionBar.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+
+export default function({ searchQuery, onQueryChange }) {
+ return (
+
+ );
+}
diff --git a/public/app/features/plugins/PluginList.tsx b/public/app/features/plugins/PluginList.tsx
new file mode 100644
index 00000000000..02d7dac0dce
--- /dev/null
+++ b/public/app/features/plugins/PluginList.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import classNames from 'classnames/bind';
+import PluginListItem from './PluginListItem';
+
+export default function PluginList({ plugins, layout }) {
+ const listStyle = classNames({
+ 'card-section': true,
+ 'card-list-layout-grid': layout === 'grid',
+ 'card-list-layout-list': layout === 'list',
+ });
+
+ return (
+
+
+ {plugins.map((plugin, index) => {
+ return ;
+ })}
+
+
+ );
+}
diff --git a/public/app/features/plugins/PluginListItem.tsx b/public/app/features/plugins/PluginListItem.tsx
new file mode 100644
index 00000000000..a143625459a
--- /dev/null
+++ b/public/app/features/plugins/PluginListItem.tsx
@@ -0,0 +1,30 @@
+import React from 'react';
+
+export default function PluginListItem({ plugin }) {
+ return (
+
+
+
+
+
+ {plugin.type}
+
+ {plugin.hasUpdate && (
+
+ Update available!
+
+ )}
+
+
+
+
+
{plugin.name}
+
{`By ${plugin.info.author.name}`}
+
+
+
+
+ );
+}
diff --git a/public/app/features/plugins/PluginListPage.tsx b/public/app/features/plugins/PluginListPage.tsx
new file mode 100644
index 00000000000..73837cfe75f
--- /dev/null
+++ b/public/app/features/plugins/PluginListPage.tsx
@@ -0,0 +1,53 @@
+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 { getPlugins } from './state/selectors';
+
+interface Props {
+ navModel: NavModel;
+ plugins: Plugin[];
+ loadPlugins: typeof loadPlugins;
+}
+
+export class PluginListPage extends PureComponent {
+ componentDidMount() {
+ this.fetchPlugins();
+ }
+
+ async fetchPlugins() {
+ await this.props.loadPlugins();
+ }
+
+ render() {
+ const { navModel, plugins } = this.props;
+
+ return (
+
+
+
+
{}} />
+ {plugins && }
+
+
+ );
+ }
+}
+
+function mapStateToProps(state) {
+ return {
+ navModel: getNavModel(state.navIndex, 'plugins'),
+ plugins: getPlugins(state.plugins),
+ };
+}
+
+const mapDispatchToProps = {
+ loadPlugins,
+};
+
+export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(PluginListPage));
diff --git a/public/app/features/plugins/state/actions.ts b/public/app/features/plugins/state/actions.ts
new file mode 100644
index 00000000000..d044a7ba56f
--- /dev/null
+++ b/public/app/features/plugins/state/actions.ts
@@ -0,0 +1,28 @@
+import { Plugin, StoreState } from 'app/types';
+import { ThunkAction } from 'redux-thunk';
+import { getBackendSrv } from '../../../core/services/backend_srv';
+
+export enum ActionTypes {
+ LoadPlugins = 'LOAD_PLUGINS',
+}
+
+export interface LoadPluginsAction {
+ type: ActionTypes.LoadPlugins;
+ payload: Plugin[];
+}
+
+export const pluginsLoaded = (plugins: Plugin[]): LoadPluginsAction => ({
+ type: ActionTypes.LoadPlugins,
+ payload: plugins,
+});
+
+export type Action = LoadPluginsAction;
+
+type ThunkResult = ThunkAction;
+
+export function loadPlugins(): ThunkResult {
+ return async dispatch => {
+ const result = await getBackendSrv().get('api/plugins', { embedded: 0 });
+ dispatch(pluginsLoaded(result));
+ };
+}
diff --git a/public/app/features/plugins/state/reducers.ts b/public/app/features/plugins/state/reducers.ts
new file mode 100644
index 00000000000..af4089220b6
--- /dev/null
+++ b/public/app/features/plugins/state/reducers.ts
@@ -0,0 +1,16 @@
+import { Action, ActionTypes } from './actions';
+import { Plugin, PluginsState } from 'app/types';
+
+export const initialState: PluginsState = { plugins: [] as Plugin[] };
+
+export const pluginsReducer = (state = initialState, action: Action): PluginsState => {
+ switch (action.type) {
+ case ActionTypes.LoadPlugins:
+ return { ...state, plugins: action.payload };
+ }
+ return state;
+};
+
+export default {
+ plugins: pluginsReducer,
+};
diff --git a/public/app/features/plugins/state/selectors.ts b/public/app/features/plugins/state/selectors.ts
new file mode 100644
index 00000000000..d436e9fa016
--- /dev/null
+++ b/public/app/features/plugins/state/selectors.ts
@@ -0,0 +1 @@
+export const getPlugins = state => state.plugins;
diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts
index 015b4ae0b51..e4662c77367 100644
--- a/public/app/routes/routes.ts
+++ b/public/app/routes/routes.ts
@@ -5,6 +5,7 @@ import ServerStats from 'app/features/admin/ServerStats';
import AlertRuleList from 'app/features/alerting/AlertRuleList';
import TeamPages from 'app/features/teams/TeamPages';
import TeamList from 'app/features/teams/TeamList';
+import PluginListPage from 'app/features/plugins/PluginListPage';
import FolderSettingsPage from 'app/features/folders/FolderSettingsPage';
import FolderPermissions from 'app/features/folders/FolderPermissions';
@@ -245,9 +246,10 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
controllerAs: 'ctrl',
})
.when('/plugins', {
- templateUrl: 'public/app/features/plugins/partials/plugin_list.html',
- controller: 'PluginListCtrl',
- controllerAs: 'ctrl',
+ template: '',
+ resolve: {
+ component: () => PluginListPage,
+ },
})
.when('/plugins/:pluginId/edit', {
templateUrl: 'public/app/features/plugins/partials/plugin_edit.html',
diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts
index 8f6cf25043d..08d3d5bede0 100644
--- a/public/app/store/configureStore.ts
+++ b/public/app/store/configureStore.ts
@@ -6,6 +6,7 @@ import alertingReducers from 'app/features/alerting/state/reducers';
import teamsReducers from 'app/features/teams/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,
@@ -13,6 +14,7 @@ const rootReducer = combineReducers({
...teamsReducers,
...foldersReducers,
...dashboardReducers,
+ ...pluginReducers,
});
export let store;
diff --git a/public/app/types/index.ts b/public/app/types/index.ts
index 778a1b21b55..4ec5c6f02cc 100644
--- a/public/app/types/index.ts
+++ b/public/app/types/index.ts
@@ -6,7 +6,7 @@ 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 { PluginMeta, Plugin, PluginInfo, PluginsState } from './plugins';
export {
Team,
@@ -33,6 +33,9 @@ export {
PermissionLevel,
DataSource,
PluginMeta,
+ PluginInfo,
+ Plugin,
+ PluginsState,
};
export interface StoreState {
diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts
index d26085f8e73..e1594296acb 100644
--- a/public/app/types/plugins.ts
+++ b/public/app/types/plugins.ts
@@ -17,3 +17,33 @@ export interface PluginMetaInfo {
small: string;
};
}
+
+export interface PluginInfo {
+ author: {
+ name: string;
+ url: string;
+ };
+ description: string;
+ links: string[];
+ logos: { small: string; large: string };
+ screenshots: string;
+ updated: string;
+ version: string;
+}
+
+export interface Plugin {
+ defaultNavUrl: string;
+ enabled: boolean;
+ hasUpdate: boolean;
+ id: string;
+ info: PluginInfo;
+ latestVersion: string;
+ name: string;
+ pinned: boolean;
+ state: string;
+ type: string;
+}
+
+export interface PluginsState {
+ plugins: Plugin[];
+}