mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 10:20:29 -06:00
Plugins: Render app plugin config pages in catalog (#37063)
* feat(catalog): introduce PluginDetailsBody component and useLoadPluginConfig hook * feat(catalog): use PluginDetailsBody and useLoadPluginConfig hook in PluginDetails * feat(catalog): introduce usePluginDetails hook to handle PluginDetails state * feat(catalog): wire usePluginDetails hook to PluginDetails and InstallControls * refactor(catalog): fix typescript errors related to usePluginDetails hook * chore(catalog): update types for PluginDetailsActions
This commit is contained in:
parent
02b5a18da2
commit
1d37d675d7
@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { satisfies } from 'semver';
|
||||
|
||||
@ -7,19 +7,20 @@ import { Button, HorizontalGroup, Icon, LinkButton, useStyles2 } from '@grafana/
|
||||
import { AppEvents, GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { CatalogPluginDetails } from '../types';
|
||||
import { CatalogPluginDetails, ActionTypes } from '../types';
|
||||
import { api } from '../api';
|
||||
import { isGrafanaAdmin } from '../helpers';
|
||||
|
||||
interface Props {
|
||||
plugin: CatalogPluginDetails;
|
||||
isInflight: boolean;
|
||||
hasUpdate: boolean;
|
||||
hasInstalledPanel: boolean;
|
||||
isInstalled: boolean;
|
||||
dispatch: React.Dispatch<any>;
|
||||
}
|
||||
|
||||
export const InstallControls = ({ plugin }: Props) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isInstalled, setIsInstalled] = useState(plugin.isInstalled || false);
|
||||
const [shouldUpdate, setShouldUpdate] = useState(plugin.hasUpdate || false);
|
||||
const [hasInstalledPanel, setHasInstalledPanel] = useState(false);
|
||||
export const InstallControls = ({ plugin, isInflight, hasUpdate, isInstalled, hasInstalledPanel, dispatch }: Props) => {
|
||||
const isExternallyManaged = config.pluginAdminExternalManageEnabled;
|
||||
const externalManageLink = getExternalManageLink(plugin);
|
||||
|
||||
@ -30,39 +31,35 @@ export const InstallControls = ({ plugin }: Props) => {
|
||||
}
|
||||
|
||||
const onInstall = async () => {
|
||||
setLoading(true);
|
||||
dispatch({ type: ActionTypes.INFLIGHT });
|
||||
try {
|
||||
await api.installPlugin(plugin.id, plugin.version);
|
||||
appEvents.emit(AppEvents.alertSuccess, [`Installed ${plugin.name}`]);
|
||||
setLoading(false);
|
||||
setIsInstalled(true);
|
||||
setHasInstalledPanel(plugin.type === 'panel');
|
||||
dispatch({ type: ActionTypes.INSTALLED, payload: plugin.type === 'panel' });
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
dispatch({ type: ActionTypes.ERROR, payload: { error } });
|
||||
}
|
||||
};
|
||||
|
||||
const onUninstall = async () => {
|
||||
setLoading(true);
|
||||
dispatch({ type: ActionTypes.INFLIGHT });
|
||||
try {
|
||||
await api.uninstallPlugin(plugin.id);
|
||||
appEvents.emit(AppEvents.alertSuccess, [`Uninstalled ${plugin.name}`]);
|
||||
setLoading(false);
|
||||
setIsInstalled(false);
|
||||
dispatch({ type: ActionTypes.UNINSTALLED });
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
dispatch({ type: ActionTypes.ERROR, payload: error });
|
||||
}
|
||||
};
|
||||
|
||||
const onUpdate = async () => {
|
||||
setLoading(true);
|
||||
dispatch({ type: ActionTypes.INFLIGHT });
|
||||
try {
|
||||
await api.installPlugin(plugin.id, plugin.version);
|
||||
appEvents.emit(AppEvents.alertSuccess, [`Updated ${plugin.name}`]);
|
||||
setLoading(false);
|
||||
setShouldUpdate(false);
|
||||
dispatch({ type: ActionTypes.UPDATED });
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
dispatch({ type: ActionTypes.ERROR, payload: error });
|
||||
}
|
||||
};
|
||||
|
||||
@ -100,14 +97,14 @@ export const InstallControls = ({ plugin }: Props) => {
|
||||
if (isInstalled) {
|
||||
return (
|
||||
<HorizontalGroup height="auto">
|
||||
{shouldUpdate &&
|
||||
{hasUpdate &&
|
||||
(isExternallyManaged ? (
|
||||
<LinkButton href={externalManageLink} target="_blank" rel="noopener noreferrer">
|
||||
{'Update via grafana.com'}
|
||||
</LinkButton>
|
||||
) : (
|
||||
<Button disabled={loading || !hasPermission} onClick={onUpdate}>
|
||||
{loading ? 'Updating' : 'Update'}
|
||||
<Button disabled={isInflight || !hasPermission} onClick={onUpdate}>
|
||||
{isInflight ? 'Updating' : 'Update'}
|
||||
</Button>
|
||||
))}
|
||||
|
||||
@ -117,8 +114,8 @@ export const InstallControls = ({ plugin }: Props) => {
|
||||
</LinkButton>
|
||||
) : (
|
||||
<>
|
||||
<Button variant="destructive" disabled={loading || !hasPermission} onClick={onUninstall}>
|
||||
{loading && !shouldUpdate ? 'Uninstalling' : 'Uninstall'}
|
||||
<Button variant="destructive" disabled={isInflight || !hasPermission} onClick={onUninstall}>
|
||||
{isInflight && !hasUpdate ? 'Uninstalling' : 'Uninstall'}
|
||||
</Button>
|
||||
{hasInstalledPanel && (
|
||||
<div className={cx(styles.message, styles.messageMargin)}>
|
||||
@ -149,8 +146,8 @@ export const InstallControls = ({ plugin }: Props) => {
|
||||
</LinkButton>
|
||||
) : (
|
||||
<>
|
||||
<Button disabled={loading || !hasPermission} onClick={onInstall}>
|
||||
{loading ? 'Installing' : 'Install'}
|
||||
<Button disabled={isInflight || !hasPermission} onClick={onInstall}>
|
||||
{isInflight ? 'Installing' : 'Install'}
|
||||
</Button>
|
||||
{!hasPermission && <div className={styles.message}>You need admin privileges to install this plugin.</div>}
|
||||
</>
|
||||
|
@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
import { AppPlugin, GrafanaTheme2, GrafanaPlugin, PluginMeta } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { VersionList } from '../components/VersionList';
|
||||
import { AppConfigCtrlWrapper } from '../../wrappers/AppConfigWrapper';
|
||||
import { PluginDashboards } from '../../PluginDashboards';
|
||||
|
||||
type PluginDetailsBodyProps = {
|
||||
tab: { label: string };
|
||||
plugin: GrafanaPlugin<PluginMeta<{}>> | undefined;
|
||||
remoteVersions: Array<{ version: string; createdAt: string }>;
|
||||
readme: string;
|
||||
};
|
||||
|
||||
export function PluginDetailsBody({ tab, plugin, remoteVersions, readme }: PluginDetailsBodyProps): JSX.Element | null {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
if (tab?.label === 'Overview') {
|
||||
return (
|
||||
<div
|
||||
className={cx(styles.readme, styles.container)}
|
||||
dangerouslySetInnerHTML={{ __html: readme ?? 'No plugin help or readme markdown file was found' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (tab?.label === 'Version history') {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<VersionList versions={remoteVersions ?? []} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tab?.label === 'Config' && plugin?.angularConfigCtrl) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<AppConfigCtrlWrapper app={plugin as AppPlugin} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (plugin?.configPages) {
|
||||
for (const configPage of plugin.configPages) {
|
||||
if (tab?.label === configPage.title) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<configPage.body plugin={plugin} query={{}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tab?.label === 'Dashboards' && plugin) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<PluginDashboards plugin={plugin.meta} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => ({
|
||||
container: css`
|
||||
padding: ${theme.spacing(3, 4)};
|
||||
`,
|
||||
readme: css`
|
||||
& img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
margin-top: ${theme.spacing(3)};
|
||||
margin-bottom: ${theme.spacing(2)};
|
||||
}
|
||||
|
||||
*:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-left: ${theme.spacing(2)};
|
||||
& > p {
|
||||
margin: ${theme.spacing()} 0;
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
@ -13,30 +13,28 @@ export const VersionList = ({ versions }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
if (versions.length === 0) {
|
||||
return <div className={styles.container}>No version history was found.</div>;
|
||||
return <p>No version history was found.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Version</th>
|
||||
<th>Last updated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{versions.map((version) => {
|
||||
return (
|
||||
<tr key={version.version}>
|
||||
<td>{version.version}</td>
|
||||
<td>{dateTimeFormatTimeAgo(version.createdAt)}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Version</th>
|
||||
<th>Last updated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{versions.map((version) => {
|
||||
return (
|
||||
<tr key={version.version}>
|
||||
<td>{version.version}</td>
|
||||
<td>{dateTimeFormatTimeAgo(version.createdAt)}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
|
155
public/app/features/plugins/admin/hooks/usePluginDetails.tsx
Normal file
155
public/app/features/plugins/admin/hooks/usePluginDetails.tsx
Normal file
@ -0,0 +1,155 @@
|
||||
import { useReducer, useEffect } from 'react';
|
||||
import { PluginType, PluginIncludeType } from '@grafana/data';
|
||||
import { api } from '../api';
|
||||
import { loadPlugin } from '../../PluginPage';
|
||||
import { getCatalogPluginDetails, isGrafanaAdmin } from '../helpers';
|
||||
import { ActionTypes, PluginDetailsActions, PluginDetailsState } from '../types';
|
||||
|
||||
const defaultTabs = [{ label: 'Overview' }, { label: 'Version history' }];
|
||||
|
||||
const initialState = {
|
||||
hasInstalledPanel: false,
|
||||
hasUpdate: false,
|
||||
isAdmin: isGrafanaAdmin(),
|
||||
isInstalled: false,
|
||||
isInflight: false,
|
||||
loading: false,
|
||||
error: undefined,
|
||||
plugin: undefined,
|
||||
pluginConfig: undefined,
|
||||
tabs: defaultTabs,
|
||||
activeTab: 0,
|
||||
};
|
||||
|
||||
const reducer = (state: PluginDetailsState, action: PluginDetailsActions) => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.LOADING: {
|
||||
return { ...state, loading: true };
|
||||
}
|
||||
case ActionTypes.INFLIGHT: {
|
||||
return { ...state, isInflight: true };
|
||||
}
|
||||
case ActionTypes.ERROR: {
|
||||
return { ...state, loading: false, error: action.payload };
|
||||
}
|
||||
case ActionTypes.FETCHED_PLUGIN: {
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
plugin: action.payload,
|
||||
isInstalled: action.payload.isInstalled,
|
||||
hasUpdate: action.payload.hasUpdate,
|
||||
};
|
||||
}
|
||||
case ActionTypes.FETCHED_PLUGIN_CONFIG: {
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
pluginConfig: action.payload,
|
||||
};
|
||||
}
|
||||
case ActionTypes.UPDATE_TABS: {
|
||||
return {
|
||||
...state,
|
||||
tabs: action.payload,
|
||||
};
|
||||
}
|
||||
case ActionTypes.INSTALLED: {
|
||||
return {
|
||||
...state,
|
||||
isInflight: false,
|
||||
isInstalled: true,
|
||||
hasInstalledPanel: action.payload,
|
||||
};
|
||||
}
|
||||
case ActionTypes.UNINSTALLED: {
|
||||
return {
|
||||
...state,
|
||||
isInflight: false,
|
||||
isInstalled: false,
|
||||
};
|
||||
}
|
||||
case ActionTypes.UPDATED: {
|
||||
return {
|
||||
...state,
|
||||
hasUpdate: false,
|
||||
isInflight: false,
|
||||
};
|
||||
}
|
||||
case ActionTypes.SET_ACTIVE_TAB: {
|
||||
return {
|
||||
...state,
|
||||
activeTab: action.payload,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const usePluginDetails = (id: string) => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPlugin = async () => {
|
||||
dispatch({ type: ActionTypes.LOADING });
|
||||
try {
|
||||
const value = await api.getPlugin(id);
|
||||
const plugin = getCatalogPluginDetails(value?.local, value?.remote, value?.remoteVersions);
|
||||
dispatch({ type: ActionTypes.FETCHED_PLUGIN, payload: plugin });
|
||||
} catch (error) {
|
||||
dispatch({ type: ActionTypes.ERROR, payload: error });
|
||||
}
|
||||
};
|
||||
fetchPlugin();
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPluginConfig = async () => {
|
||||
if (state.isInstalled) {
|
||||
dispatch({ type: ActionTypes.LOADING });
|
||||
try {
|
||||
const pluginConfig = await loadPlugin(id);
|
||||
dispatch({ type: ActionTypes.FETCHED_PLUGIN_CONFIG, payload: pluginConfig });
|
||||
} catch (error) {
|
||||
dispatch({ type: ActionTypes.ERROR, payload: error });
|
||||
}
|
||||
} else {
|
||||
// reset tabs
|
||||
dispatch({ type: ActionTypes.FETCHED_PLUGIN_CONFIG, payload: undefined });
|
||||
dispatch({ type: ActionTypes.SET_ACTIVE_TAB, payload: 0 });
|
||||
}
|
||||
};
|
||||
fetchPluginConfig();
|
||||
}, [state.isInstalled, id]);
|
||||
|
||||
useEffect(() => {
|
||||
const pluginConfig = state.pluginConfig;
|
||||
const tabs: Array<{ label: string }> = [...defaultTabs];
|
||||
|
||||
if (pluginConfig && state.isAdmin) {
|
||||
if (pluginConfig.meta.type === PluginType.app) {
|
||||
if (pluginConfig.angularConfigCtrl) {
|
||||
tabs.push({
|
||||
label: 'Config',
|
||||
});
|
||||
}
|
||||
|
||||
if (pluginConfig.configPages) {
|
||||
for (const page of pluginConfig.configPages) {
|
||||
tabs.push({
|
||||
label: page.title,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (pluginConfig.meta.includes?.find((include) => include.type === PluginIncludeType.dashboard)) {
|
||||
tabs.push({
|
||||
label: 'Dashboards',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
dispatch({ type: ActionTypes.UPDATE_TABS, payload: tabs });
|
||||
}, [state.isAdmin, state.pluginConfig, id]);
|
||||
|
||||
return { state, dispatch };
|
||||
};
|
@ -1,8 +1,8 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { CatalogPlugin, CatalogPluginDetails } from '../types';
|
||||
import { CatalogPlugin } from '../types';
|
||||
import { api } from '../api';
|
||||
import { mapLocalToCatalog, mapRemoteToCatalog, getCatalogPluginDetails, applySearchFilter } from '../helpers';
|
||||
import { mapLocalToCatalog, mapRemoteToCatalog, applySearchFilter } from '../helpers';
|
||||
|
||||
type CatalogPluginsState = {
|
||||
loading: boolean;
|
||||
@ -77,21 +77,3 @@ export const usePluginsByFilter = (searchBy: string, filterBy: string): Filtered
|
||||
plugins: applySearchFilter(searchBy, plugins),
|
||||
};
|
||||
};
|
||||
|
||||
type PluginState = {
|
||||
isLoading: boolean;
|
||||
plugin?: CatalogPluginDetails;
|
||||
};
|
||||
|
||||
export const usePlugin = (slug: string): PluginState => {
|
||||
const { loading, value } = useAsync(async () => {
|
||||
return await api.getPlugin(slug);
|
||||
}, [slug]);
|
||||
|
||||
const plugin = getCatalogPluginDetails(value?.local, value?.remote, value?.remoteVersions);
|
||||
|
||||
return {
|
||||
isLoading: loading,
|
||||
plugin,
|
||||
};
|
||||
};
|
||||
|
@ -1,32 +1,41 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2, TabsBar, TabContent, Tab, Icon } from '@grafana/ui';
|
||||
import { useStyles2, TabsBar, TabContent, Tab, Icon, Alert } from '@grafana/ui';
|
||||
|
||||
import { VersionList } from '../components/VersionList';
|
||||
import { AppNotificationSeverity } from 'app/types';
|
||||
import { InstallControls } from '../components/InstallControls';
|
||||
import { usePlugin } from '../hooks/usePlugins';
|
||||
import { usePluginDetails } from '../hooks/usePluginDetails';
|
||||
import { Page as PluginPage } from '../components/Page';
|
||||
import { Loader } from '../components/Loader';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { PluginLogo } from '../components/PluginLogo';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
import { ActionTypes } from '../types';
|
||||
import { PluginDetailsBody } from '../components/PluginDetailsBody';
|
||||
|
||||
type PluginDetailsProps = GrafanaRouteComponentProps<{ pluginId?: string }>;
|
||||
|
||||
export default function PluginDetails({ match }: PluginDetailsProps): JSX.Element | null {
|
||||
const { pluginId } = match.params;
|
||||
|
||||
const [tabs, setTabs] = useState([
|
||||
{ label: 'Overview', active: true },
|
||||
{ label: 'Version history', active: false },
|
||||
]);
|
||||
|
||||
const { isLoading, plugin } = usePlugin(pluginId!);
|
||||
const { state, dispatch } = usePluginDetails(pluginId!);
|
||||
const {
|
||||
loading,
|
||||
error,
|
||||
plugin,
|
||||
pluginConfig,
|
||||
tabs,
|
||||
activeTab,
|
||||
isInflight,
|
||||
hasUpdate,
|
||||
isInstalled,
|
||||
hasInstalledPanel,
|
||||
} = state;
|
||||
const tab = tabs[activeTab];
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
if (isLoading) {
|
||||
if (loading) {
|
||||
return (
|
||||
<Page>
|
||||
<Loader />
|
||||
@ -69,33 +78,41 @@ export default function PluginDetails({ match }: PluginDetailsProps): JSX.Elemen
|
||||
{plugin.version && <span>{plugin.version}</span>}
|
||||
</div>
|
||||
<p>{plugin.description}</p>
|
||||
<InstallControls plugin={plugin} />
|
||||
<InstallControls
|
||||
plugin={plugin}
|
||||
isInflight={isInflight}
|
||||
hasUpdate={hasUpdate}
|
||||
isInstalled={isInstalled}
|
||||
hasInstalledPanel={hasInstalledPanel}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<TabsBar>
|
||||
{tabs.map((tab, key) => (
|
||||
{tabs.map((tab: { label: string }, idx: number) => (
|
||||
<Tab
|
||||
key={key}
|
||||
key={tab.label}
|
||||
label={tab.label}
|
||||
active={tab.active}
|
||||
onChangeTab={() => {
|
||||
setTabs(tabs.map((tab, index) => ({ ...tab, active: index === key })));
|
||||
}}
|
||||
active={idx === activeTab}
|
||||
onChangeTab={() => dispatch({ type: ActionTypes.SET_ACTIVE_TAB, payload: idx })}
|
||||
/>
|
||||
))}
|
||||
</TabsBar>
|
||||
<TabContent>
|
||||
{tabs.find((_) => _.label === 'Overview')?.active && (
|
||||
<div
|
||||
className={styles.readme}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: plugin?.readme ?? 'No plugin help or readme markdown file was found',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{tabs.find((_) => _.label === 'Version history')?.active && (
|
||||
<VersionList versions={plugin?.versions ?? []} />
|
||||
{error && (
|
||||
<Alert severity={AppNotificationSeverity.Error} title="Error Loading Plugin">
|
||||
<>
|
||||
Check the server startup logs for more information. <br />
|
||||
If this plugin was loaded from git, make sure it was compiled.
|
||||
</>
|
||||
</Alert>
|
||||
)}
|
||||
<PluginDetailsBody
|
||||
tab={tab}
|
||||
plugin={pluginConfig}
|
||||
remoteVersions={plugin.versions}
|
||||
readme={plugin.readme}
|
||||
/>
|
||||
</TabContent>
|
||||
</PluginPage>
|
||||
</Page>
|
||||
@ -139,30 +156,5 @@ export const getStyles = (theme: GrafanaTheme2) => {
|
||||
headerOrgName: css`
|
||||
font-size: ${theme.typography.h4.fontSize};
|
||||
`,
|
||||
readme: css`
|
||||
padding: ${theme.spacing(3, 4)};
|
||||
|
||||
& img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
margin-top: ${theme.spacing(3)};
|
||||
margin-bottom: ${theme.spacing(2)};
|
||||
}
|
||||
|
||||
*:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-left: ${theme.spacing(2)};
|
||||
& > p {
|
||||
margin: ${theme.spacing()} 0;
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { GrafanaPlugin, PluginMeta } from '@grafana/data';
|
||||
export type PluginTypeCode = 'app' | 'panel' | 'datasource';
|
||||
|
||||
export interface CatalogPlugin {
|
||||
description: string;
|
||||
downloads: number;
|
||||
@ -133,3 +133,44 @@ export interface Org {
|
||||
avatar: string;
|
||||
avatarUrl: string;
|
||||
}
|
||||
|
||||
export interface PluginDetailsState {
|
||||
hasInstalledPanel: boolean;
|
||||
hasUpdate: boolean;
|
||||
isAdmin: boolean;
|
||||
isInstalled: boolean;
|
||||
isInflight: boolean;
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
plugin?: CatalogPluginDetails;
|
||||
pluginConfig?: GrafanaPlugin<PluginMeta<{}>>;
|
||||
tabs: Array<{ label: string }>;
|
||||
activeTab: number;
|
||||
}
|
||||
|
||||
export enum ActionTypes {
|
||||
LOADING = 'LOADING',
|
||||
INFLIGHT = 'INFLIGHT',
|
||||
INSTALLED = 'INSTALLED',
|
||||
UNINSTALLED = 'UNINSTALLED',
|
||||
UPDATED = 'UPDATED',
|
||||
ERROR = 'ERROR',
|
||||
FETCHED_PLUGIN = 'FETCHED_PLUGIN',
|
||||
FETCHED_PLUGIN_CONFIG = 'FETCHED_PLUGIN_CONFIG',
|
||||
UPDATE_TABS = 'UPDATE_TABS',
|
||||
SET_ACTIVE_TAB = 'SET_ACTIVE_TAB',
|
||||
}
|
||||
|
||||
export type PluginDetailsActions =
|
||||
| { type: ActionTypes.FETCHED_PLUGIN; payload: CatalogPluginDetails }
|
||||
| { type: ActionTypes.ERROR; payload: Error }
|
||||
| { type: ActionTypes.FETCHED_PLUGIN_CONFIG; payload?: GrafanaPlugin<PluginMeta<{}>> }
|
||||
| {
|
||||
type: ActionTypes.UPDATE_TABS;
|
||||
payload: Array<{ label: string }>;
|
||||
}
|
||||
| { type: ActionTypes.INSTALLED; payload: boolean }
|
||||
| { type: ActionTypes.SET_ACTIVE_TAB; payload: number }
|
||||
| {
|
||||
type: ActionTypes.LOADING | ActionTypes.INFLIGHT | ActionTypes.UNINSTALLED | ActionTypes.UPDATED;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user