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 ( +
+
+ +
+
+ + Find more plugins on Grafana.com + +
+ ); +} 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[]; +}