Peter Holmberg 7 лет назад
Родитель
Сommit
e8cc0f3fff

+ 24 - 0
public/app/features/plugins/PluginActionBar.tsx

@@ -0,0 +1,24 @@
+import React from 'react';
+
+export default function({ searchQuery, onQueryChange }) {
+  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={onQueryChange}
+            placeholder="Filter by name or type"
+          />
+          <i className="gf-form-input-icon fa fa-search" />
+        </label>
+      </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>
+  );
+}

+ 21 - 0
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 (
+    <section className={listStyle}>
+      <ol className="card-list">
+        {plugins.map((plugin, index) => {
+          return <PluginListItem plugin={plugin} key={`${plugin.name}-${index}`} />;
+        })}
+      </ol>
+    </section>
+  );
+}

+ 30 - 0
public/app/features/plugins/PluginListItem.tsx

@@ -0,0 +1,30 @@
+import React from 'react';
+
+export default function PluginListItem({ plugin }) {
+  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>
+  );
+}

+ 53 - 0
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<Props> {
+  componentDidMount() {
+    this.fetchPlugins();
+  }
+
+  async fetchPlugins() {
+    await this.props.loadPlugins();
+  }
+
+  render() {
+    const { navModel, plugins } = this.props;
+
+    return (
+      <div>
+        <PageHeader model={navModel} />
+        <div className="page-container page-body">
+          <PluginActionBar searchQuery="" onQueryChange={() => {}} />
+          {plugins && <PluginList plugins={plugins} layout="grid" />}
+        </div>
+      </div>
+    );
+  }
+}
+
+function mapStateToProps(state) {
+  return {
+    navModel: getNavModel(state.navIndex, 'plugins'),
+    plugins: getPlugins(state.plugins),
+  };
+}
+
+const mapDispatchToProps = {
+  loadPlugins,
+};
+
+export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(PluginListPage));

+ 28 - 0
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<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));
+  };
+}

+ 16 - 0
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,
+};

+ 1 - 0
public/app/features/plugins/state/selectors.ts

@@ -0,0 +1 @@
+export const getPlugins = state => state.plugins;

+ 5 - 3
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: '<react-container />',
+      resolve: {
+        component: () => PluginListPage,
+      },
     })
     .when('/plugins/:pluginId/edit', {
       templateUrl: 'public/app/features/plugins/partials/plugin_edit.html',

+ 2 - 0
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;

+ 4 - 1
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 {

+ 30 - 0
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[];
+}