mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugins: Plugin details right panel is added. All the details were moved from thee top to the right panel (#90325)
* PluginDetailsRight panel is added. All the details were moved from the top to the right panel * Add feature toggle pluginsDetailsRightPanel,Fix build, fix review comments * Fix the typo Co-authored-by: Giuseppe Guerra <giuseppe.guerra@grafana.com> * hasAccessToExplore * changes after review, add translations * fix betterer * fix betterer * fix css error * fix betterer * fix translation labels, fix position of the right panel * fix the build * add condition to show updatedAt for plugin details * add test to check 2 new fields at plugin details right panel; * change the gap and remove report abuse button from core plugins * add more tests --------- Co-authored-by: Giuseppe Guerra <giuseppe.guerra@grafana.com>
This commit is contained in:
@@ -4848,17 +4848,12 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"]
|
||||
],
|
||||
"public/app/features/plugins/admin/components/InstallControls/InstallControlsWarning.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "7"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "8"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "9"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "10"]
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"]
|
||||
],
|
||||
"public/app/features/plugins/admin/components/InstallControls/index.tsx:5381": [
|
||||
[0, 0, 0, "Do not re-export imported variable (\`./InstallControlsWarning\`)", "0"],
|
||||
|
||||
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -344,6 +344,7 @@
|
||||
/packages/grafana-ui/src/components/DataLinks/ @grafana/dataviz-squad
|
||||
/packages/grafana-ui/src/components/DateTimePickers/ @grafana/grafana-frontend-platform
|
||||
/packages/grafana-ui/src/components/Gauge/ @grafana/dataviz-squad
|
||||
/packages/grafana-ui/src/components/PluginSignatureBadge/ @grafana/plugins-platform-frontend
|
||||
/packages/grafana-ui/src/components/Sparkline/ @grafana/grafana-frontend-platform @grafana/app-o11y-visualizations
|
||||
/packages/grafana-ui/src/components/Table/ @grafana/dataviz-squad
|
||||
/packages/grafana-ui/src/components/Table/SparklineCell.tsx @grafana/dataviz-squad @grafana/app-o11y-visualizations
|
||||
|
||||
@@ -137,6 +137,7 @@ Experimental features might be changed or removed without prior notice.
|
||||
| `lokiPredefinedOperations` | Adds predefined query operations to Loki query editor |
|
||||
| `pluginsFrontendSandbox` | Enables the plugins frontend sandbox |
|
||||
| `frontendSandboxMonitorOnly` | Enables monitor only in the plugin frontend sandbox (if enabled) |
|
||||
| `pluginsDetailsRightPanel` | Enables right panel for the plugins details page |
|
||||
| `vizAndWidgetSplit` | Split panels between visualizations and widgets |
|
||||
| `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers |
|
||||
| `mlExpressions` | Enable support for Machine Learning in server-side expressions |
|
||||
|
||||
@@ -77,6 +77,7 @@ export interface FeatureToggles {
|
||||
lokiPredefinedOperations?: boolean;
|
||||
pluginsFrontendSandbox?: boolean;
|
||||
frontendSandboxMonitorOnly?: boolean;
|
||||
pluginsDetailsRightPanel?: boolean;
|
||||
sqlDatasourceDatabaseSelection?: boolean;
|
||||
recordedQueriesMulti?: boolean;
|
||||
vizAndWidgetSplit?: boolean;
|
||||
|
||||
@@ -1,21 +1,37 @@
|
||||
import { HTMLAttributes } from 'react';
|
||||
|
||||
import { PluginSignatureStatus } from '@grafana/data';
|
||||
import { PluginSignatureStatus, PluginSignatureType } from '@grafana/data';
|
||||
|
||||
import { IconName } from '../../types';
|
||||
import { Badge, BadgeProps } from '../Badge/Badge';
|
||||
|
||||
const SIGNATURE_ICONS: Record<string, IconName> = {
|
||||
[PluginSignatureType.grafana]: 'grafana',
|
||||
[PluginSignatureType.commercial]: 'shield',
|
||||
[PluginSignatureType.community]: 'shield',
|
||||
DEFAULT: 'shield-exclamation',
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface PluginSignatureBadgeProps extends HTMLAttributes<HTMLDivElement> {
|
||||
status?: PluginSignatureStatus;
|
||||
signatureType?: PluginSignatureType;
|
||||
signatureOrg?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const PluginSignatureBadge = ({ status, color, ...otherProps }: PluginSignatureBadgeProps) => {
|
||||
const display = getSignatureDisplayModel(status);
|
||||
export const PluginSignatureBadge = ({
|
||||
status,
|
||||
color,
|
||||
signatureType,
|
||||
signatureOrg,
|
||||
...otherProps
|
||||
}: PluginSignatureBadgeProps) => {
|
||||
const display = getSignatureDisplayModel(status, signatureType, signatureOrg);
|
||||
return (
|
||||
<Badge text={display.text} color={display.color} icon={display.icon} tooltip={display.tooltip} {...otherProps} />
|
||||
);
|
||||
@@ -23,16 +39,27 @@ export const PluginSignatureBadge = ({ status, color, ...otherProps }: PluginSig
|
||||
|
||||
PluginSignatureBadge.displayName = 'PluginSignatureBadge';
|
||||
|
||||
function getSignatureDisplayModel(signature?: PluginSignatureStatus): BadgeProps {
|
||||
function getSignatureDisplayModel(
|
||||
signature?: PluginSignatureStatus,
|
||||
signatureType?: PluginSignatureType,
|
||||
signatureOrg?: string
|
||||
): BadgeProps {
|
||||
if (!signature) {
|
||||
signature = PluginSignatureStatus.invalid;
|
||||
}
|
||||
|
||||
const signatureIcon = SIGNATURE_ICONS[signatureType || ''] || SIGNATURE_ICONS.DEFAULT;
|
||||
|
||||
switch (signature) {
|
||||
case PluginSignatureStatus.internal:
|
||||
return { text: 'Core', color: 'blue', tooltip: 'Core plugin that is bundled with Grafana' };
|
||||
case PluginSignatureStatus.valid:
|
||||
return { text: 'Signed', icon: 'lock', color: 'green', tooltip: 'Signed and verified plugin' };
|
||||
return {
|
||||
text: signatureType ? signatureType : 'Signed',
|
||||
icon: signatureType ? signatureIcon : 'lock',
|
||||
color: 'green',
|
||||
tooltip: 'Signed and verified plugin',
|
||||
};
|
||||
case PluginSignatureStatus.invalid:
|
||||
return {
|
||||
text: 'Invalid signature',
|
||||
|
||||
@@ -443,6 +443,13 @@ var (
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaPluginsPlatformSquad,
|
||||
},
|
||||
{
|
||||
Name: "pluginsDetailsRightPanel",
|
||||
Description: "Enables right panel for the plugins details page",
|
||||
Stage: FeatureStageExperimental,
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaPluginsPlatformSquad,
|
||||
},
|
||||
{
|
||||
Name: "sqlDatasourceDatabaseSelection",
|
||||
Description: "Enables previous SQL data source dataset dropdown behavior",
|
||||
|
||||
@@ -58,6 +58,7 @@ extraThemes,experimental,@grafana/grafana-frontend-platform,false,false,true
|
||||
lokiPredefinedOperations,experimental,@grafana/observability-logs,false,false,true
|
||||
pluginsFrontendSandbox,experimental,@grafana/plugins-platform-backend,false,false,true
|
||||
frontendSandboxMonitorOnly,experimental,@grafana/plugins-platform-backend,false,false,true
|
||||
pluginsDetailsRightPanel,experimental,@grafana/plugins-platform-backend,false,false,true
|
||||
sqlDatasourceDatabaseSelection,preview,@grafana/dataviz-squad,false,false,true
|
||||
recordedQueriesMulti,GA,@grafana/observability-metrics,false,false,false
|
||||
vizAndWidgetSplit,experimental,@grafana/dashboards-squad,false,false,true
|
||||
|
||||
|
@@ -243,6 +243,10 @@ const (
|
||||
// Enables monitor only in the plugin frontend sandbox (if enabled)
|
||||
FlagFrontendSandboxMonitorOnly = "frontendSandboxMonitorOnly"
|
||||
|
||||
// FlagPluginsDetailsRightPanel
|
||||
// Enables right panel for the plugins details page
|
||||
FlagPluginsDetailsRightPanel = "pluginsDetailsRightPanel"
|
||||
|
||||
// FlagSqlDatasourceDatabaseSelection
|
||||
// Enables previous SQL data source dataset dropdown behavior
|
||||
FlagSqlDatasourceDatabaseSelection = "sqlDatasourceDatabaseSelection"
|
||||
|
||||
@@ -2038,6 +2038,22 @@
|
||||
"frontend": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "pluginsDetailsRightPanel",
|
||||
"resourceVersion": "1720788722220",
|
||||
"creationTimestamp": "2024-07-12T08:39:21Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2024-07-12 12:52:02.22099 +0000 UTC"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables right panel for the plugins details page",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/plugins-platform-backend",
|
||||
"frontend": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "pluginsFrontendSandbox",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { RemotePlugin } from '../types';
|
||||
|
||||
// Copied from /api/gnet/plugins/alexanderzobnin-zabbix-app
|
||||
export default {
|
||||
changelog: '',
|
||||
createdAt: '2016-04-06T20:23:41.000Z',
|
||||
description: 'Zabbix plugin for Grafana',
|
||||
downloads: 33645089,
|
||||
|
||||
@@ -18,10 +18,11 @@ export async function getPluginDetails(id: string): Promise<CatalogPluginDetails
|
||||
const remote = await getRemotePlugin(id);
|
||||
const isPublished = Boolean(remote);
|
||||
|
||||
const [localPlugins, versions, localReadme] = await Promise.all([
|
||||
const [localPlugins, versions, localReadme, localChangelog] = await Promise.all([
|
||||
getLocalPlugins(),
|
||||
getPluginVersions(id, isPublished),
|
||||
getLocalPluginReadme(id),
|
||||
getLocalPluginChangelog(id),
|
||||
]);
|
||||
|
||||
const local = localPlugins.find((p) => p.id === id);
|
||||
@@ -35,6 +36,7 @@ export async function getPluginDetails(id: string): Promise<CatalogPluginDetails
|
||||
versions,
|
||||
statusContext: remote?.statusContext ?? '',
|
||||
iam: remote?.json?.iam,
|
||||
changelog: localChangelog || remote?.changelog,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -116,6 +118,20 @@ async function getLocalPluginReadme(id: string): Promise<string> {
|
||||
}
|
||||
}
|
||||
|
||||
async function getLocalPluginChangelog(id: string): Promise<string> {
|
||||
try {
|
||||
const markdown: string = await getBackendSrv().get(`${API_ROOT}/${id}/markdown/CHANGELOG`);
|
||||
const markdownAsHtml = markdown ? renderMarkdown(markdown) : '';
|
||||
|
||||
return markdownAsHtml;
|
||||
} catch (error) {
|
||||
if (isFetchError(error)) {
|
||||
error.isHandled = true;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLocalPlugins(): Promise<LocalPlugin[]> {
|
||||
const localPlugins: LocalPlugin[] = await getBackendSrv().get(
|
||||
`${API_ROOT}`,
|
||||
|
||||
44
public/app/features/plugins/admin/components/Changelog.tsx
Normal file
44
public/app/features/plugins/admin/components/Changelog.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
interface Props {
|
||||
sanitizedHTML: string;
|
||||
}
|
||||
|
||||
export const Changelog = ({ sanitizedHTML }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: sanitizedHTML ?? 'No changelog was found' }}
|
||||
className={cx(styles.changelog)}
|
||||
></div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
changelog: css({
|
||||
'h1:first-of-type': {
|
||||
display: 'none',
|
||||
},
|
||||
'h2:first-of-type': {
|
||||
marginTop: 0,
|
||||
},
|
||||
h2: {
|
||||
marginTop: theme.spacing(2),
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
li: {
|
||||
marginLeft: theme.spacing(4),
|
||||
},
|
||||
a: {
|
||||
color: theme.colors.text.link,
|
||||
'&:hover': {
|
||||
color: theme.colors.text.link,
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2, PluginType } from '@grafana/data';
|
||||
import { config, featureEnabled } from '@grafana/runtime';
|
||||
import { Icon, LinkButton, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { HorizontalGroup, LinkButton, useStyles2, Alert } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
@@ -24,67 +24,90 @@ export const InstallControlsWarning = ({ plugin, pluginStatus, latestCompatibleV
|
||||
const isCompatible = Boolean(latestCompatibleVersion);
|
||||
|
||||
if (plugin.type === PluginType.renderer) {
|
||||
return <div className={styles.message}>Renderer plugins cannot be managed by the Plugin Catalog.</div>;
|
||||
return (
|
||||
<Alert
|
||||
severity="warning"
|
||||
title="Renderer plugins cannot be managed by the Plugin Catalog."
|
||||
className={styles.alert}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (plugin.type === PluginType.secretsmanager) {
|
||||
return <div className={styles.message}>Secrets manager plugins cannot be managed by the Plugin Catalog.</div>;
|
||||
return (
|
||||
<Alert
|
||||
severity="warning"
|
||||
title="Secrets manager plugins cannot be managed by the Plugin Catalog."
|
||||
className={styles.alert}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (plugin.isEnterprise && !featureEnabled('enterprise.plugins')) {
|
||||
return (
|
||||
<Stack height="auto" alignItems="center">
|
||||
<span className={styles.message}>No valid Grafana Enterprise license detected.</span>
|
||||
<LinkButton
|
||||
href={`${getExternalManageLink(plugin.id)}?utm_source=grafana_catalog_learn_more`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
size="sm"
|
||||
fill="text"
|
||||
icon="external-link-alt"
|
||||
>
|
||||
Learn more
|
||||
</LinkButton>
|
||||
</Stack>
|
||||
<Alert severity="warning" title="" className={styles.alert}>
|
||||
<HorizontalGroup height="auto" align="center">
|
||||
<span>No valid Grafana Enterprise license detected.</span>
|
||||
<LinkButton
|
||||
href={`${getExternalManageLink(plugin.id)}?utm_source=grafana_catalog_learn_more`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
size="sm"
|
||||
fill="text"
|
||||
icon="external-link-alt"
|
||||
>
|
||||
Learn more
|
||||
</LinkButton>
|
||||
</HorizontalGroup>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (plugin.isDev) {
|
||||
return (
|
||||
<div className={styles.message}>This is a development build of the plugin and can't be uninstalled.</div>
|
||||
<Alert
|
||||
severity="warning"
|
||||
title="This is a development build of the plugin and can't be uninstalled."
|
||||
className={styles.alert}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasPermission && !isExternallyManaged) {
|
||||
return <div className={styles.message}>{statusToMessage(pluginStatus)}</div>;
|
||||
return <Alert severity="warning" title={statusToMessage(pluginStatus)} className={styles.alert} />;
|
||||
}
|
||||
|
||||
if (!plugin.isPublished) {
|
||||
return (
|
||||
<div className={styles.message}>
|
||||
<Icon name="exclamation-triangle" /> This plugin is not published to{' '}
|
||||
<a href="https://www.grafana.com/plugins" target="__blank" rel="noreferrer">
|
||||
grafana.com/plugins
|
||||
</a>{' '}
|
||||
and can't be managed via the catalog.
|
||||
</div>
|
||||
<Alert severity="warning" title="" className={styles.alert}>
|
||||
<div>
|
||||
This plugin is not published to{' '}
|
||||
<a href="https://www.grafana.com/plugins" target="__blank" rel="noreferrer">
|
||||
grafana.com/plugins
|
||||
</a>{' '}
|
||||
and can't be managed via the catalog.
|
||||
</div>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isCompatible) {
|
||||
return (
|
||||
<div className={styles.message}>
|
||||
<Icon name="exclamation-triangle" />
|
||||
This plugin doesn't support your version of Grafana.
|
||||
</div>
|
||||
<Alert
|
||||
severity="warning"
|
||||
title="This plugin doesn't support your version of Grafana."
|
||||
className={styles.alert}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isRemotePluginsAvailable) {
|
||||
return (
|
||||
<div className={styles.message}>
|
||||
The install controls have been disabled because the Grafana server cannot access grafana.com.
|
||||
</div>
|
||||
<Alert
|
||||
severity="warning"
|
||||
title="The install controls have been disabled because the Grafana server cannot access grafana.com."
|
||||
className={styles.alert}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -93,9 +116,9 @@ export const InstallControlsWarning = ({ plugin, pluginStatus, latestCompatibleV
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
message: css`
|
||||
color: ${theme.colors.text.secondary};
|
||||
`,
|
||||
alert: css({
|
||||
marginTop: `${theme.spacing(2)}`,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { AppPlugin, GrafanaTheme2, PluginContextProvider, UrlQueryMap } from '@g
|
||||
import { config } from '@grafana/runtime';
|
||||
import { CellProps, Column, InteractiveTable, Stack, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { Changelog } from '../components/Changelog';
|
||||
import { VersionList } from '../components/VersionList';
|
||||
import { usePluginConfig } from '../hooks/usePluginConfig';
|
||||
import { CatalogPlugin, Permission, PluginTabIds } from '../types';
|
||||
@@ -60,6 +61,10 @@ export function PluginDetailsBody({ plugin, queryParams, pageId }: Props): JSX.E
|
||||
);
|
||||
}
|
||||
|
||||
if (pageId === PluginTabIds.CHANGELOG && plugin?.details?.changelog) {
|
||||
return <Changelog sanitizedHTML={plugin?.details?.changelog} />;
|
||||
}
|
||||
|
||||
if (pageId === PluginTabIds.CONFIG && pluginConfig?.angularConfigCtrl) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { css } from '@emotion/css';
|
||||
import * as React from 'react';
|
||||
|
||||
import { GrafanaTheme2, PluginSignatureStatus } from '@grafana/data';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { PluginSignatureBadge, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { CatalogPlugin } from '../types';
|
||||
|
||||
import { PluginSignatureDetailsBadge } from './PluginSignatureDetailsBadge';
|
||||
|
||||
type Props = {
|
||||
plugin: CatalogPlugin;
|
||||
};
|
||||
@@ -15,7 +13,6 @@ type Props = {
|
||||
// Designed to show plugin signature information in the header on the plugin's details page
|
||||
export function PluginDetailsHeaderSignature({ plugin }: Props): React.ReactElement {
|
||||
const styles = useStyles2(getStyles);
|
||||
const isSignatureValid = plugin.signature === PluginSignatureStatus.valid;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
@@ -25,12 +22,12 @@ export function PluginDetailsHeaderSignature({ plugin }: Props): React.ReactElem
|
||||
rel="noreferrer"
|
||||
className={styles.link}
|
||||
>
|
||||
<PluginSignatureBadge status={plugin.signature} />
|
||||
<PluginSignatureBadge
|
||||
status={plugin.signature}
|
||||
signatureType={plugin.signatureType}
|
||||
signatureOrg={plugin.signatureOrg}
|
||||
/>
|
||||
</a>
|
||||
|
||||
{isSignatureValid && (
|
||||
<PluginSignatureDetailsBadge signatureType={plugin.signatureType} signatureOrg={plugin.signatureOrg} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { AngularDeprecationPluginNotice } from '../../angularDeprecation/Angular
|
||||
import { Loader } from '../components/Loader';
|
||||
import { PluginDetailsBody } from '../components/PluginDetailsBody';
|
||||
import { PluginDetailsDisabledError } from '../components/PluginDetailsDisabledError';
|
||||
import { PluginDetailsRightPanel } from '../components/PluginDetailsRightPanel';
|
||||
import { PluginDetailsSignature } from '../components/PluginDetailsSignature';
|
||||
import { usePluginDetailsTabs } from '../hooks/usePluginDetailsTabs';
|
||||
import { usePluginPageExtensions } from '../hooks/usePluginPageExtensions';
|
||||
@@ -72,26 +73,31 @@ export function PluginDetailsPage({
|
||||
);
|
||||
}
|
||||
|
||||
const conditionalProps = !config.featureToggles.pluginsDetailsRightPanel ? { info: info } : {};
|
||||
|
||||
return (
|
||||
<Page navId={navId} pageNav={navModel} actions={actions} subTitle={subtitle} info={info}>
|
||||
<Page.Contents>
|
||||
<TabContent className={styles.tabContent}>
|
||||
{plugin.angularDetected && (
|
||||
<AngularDeprecationPluginNotice
|
||||
className={styles.alert}
|
||||
angularSupportEnabled={config?.angularSupportEnabled}
|
||||
pluginId={plugin.id}
|
||||
pluginType={plugin.type}
|
||||
showPluginDetailsLink={false}
|
||||
interactionElementId="plugin-details-page"
|
||||
/>
|
||||
)}
|
||||
<PluginDetailsSignature plugin={plugin} className={styles.alert} />
|
||||
<PluginDetailsDisabledError plugin={plugin} className={styles.alert} />
|
||||
<PluginDetailsDeprecatedWarning plugin={plugin} className={styles.alert} />
|
||||
<PluginDetailsBody queryParams={Object.fromEntries(queryParams)} plugin={plugin} pageId={activePageId} />
|
||||
</TabContent>
|
||||
</Page.Contents>
|
||||
<Page navId={navId} pageNav={navModel} actions={actions} subTitle={subtitle} {...conditionalProps}>
|
||||
<Stack gap={4} justifyContent="space-between" direction={{ xs: 'column-reverse', sm: 'row' }}>
|
||||
<Page.Contents>
|
||||
<TabContent className={styles.tabContent}>
|
||||
{plugin.angularDetected && (
|
||||
<AngularDeprecationPluginNotice
|
||||
className={styles.alert}
|
||||
angularSupportEnabled={config?.angularSupportEnabled}
|
||||
pluginId={plugin.id}
|
||||
pluginType={plugin.type}
|
||||
showPluginDetailsLink={false}
|
||||
interactionElementId="plugin-details-page"
|
||||
/>
|
||||
)}
|
||||
<PluginDetailsSignature plugin={plugin} className={styles.alert} />
|
||||
<PluginDetailsDisabledError plugin={plugin} className={styles.alert} />
|
||||
<PluginDetailsDeprecatedWarning plugin={plugin} className={styles.alert} />
|
||||
<PluginDetailsBody queryParams={Object.fromEntries(queryParams)} plugin={plugin} pageId={activePageId} />
|
||||
</TabContent>
|
||||
</Page.Contents>
|
||||
{config.featureToggles.pluginsDetailsRightPanel && <PluginDetailsRightPanel info={info} plugin={plugin} />}
|
||||
</Stack>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { PageInfoItem } from '@grafana/runtime/src/components/PluginPage';
|
||||
import { TextLink, Stack, Text } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { formatDate } from 'app/core/internationalization/dates';
|
||||
|
||||
import { CatalogPlugin } from '../types';
|
||||
|
||||
type Props = {
|
||||
info: PageInfoItem[];
|
||||
plugin: CatalogPlugin;
|
||||
};
|
||||
|
||||
export function PluginDetailsRightPanel(props: Props): React.ReactElement | null {
|
||||
const { info, plugin } = props;
|
||||
|
||||
return (
|
||||
<Stack direction="column" gap={1} grow={0} shrink={0} maxWidth={'250px'}>
|
||||
{info.map((infoItem, index) => {
|
||||
return (
|
||||
<Stack key={index} wrap>
|
||||
<Text color="secondary">{infoItem.label + ':'}</Text>
|
||||
<div>{infoItem.value}</div>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
|
||||
{plugin.updatedAt && (
|
||||
<div>
|
||||
<Text color="secondary">
|
||||
<Trans i18nKey="plugins.details.labels.updatedAt">Last updated: </Trans>
|
||||
</Text>{' '}
|
||||
<Text>{formatDate(new Date(plugin.updatedAt))}</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{plugin?.details?.links && plugin.details?.links?.length > 0 && (
|
||||
<Stack direction="column" gap={2}>
|
||||
{plugin.details.links.map((link, index) => (
|
||||
<div key={index}>
|
||||
<TextLink href={link.url} external>
|
||||
{link.name}
|
||||
</TextLink>
|
||||
</div>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{!plugin?.isCore && (
|
||||
<TextLink href="mailto:integrations@grafana.com" external>
|
||||
<Trans i18nKey="plugins.details.labels.reportAbuse">Report Abuse</Trans>
|
||||
</TextLink>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
||||
import { Fragment } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Alert, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { InstallControlsWarning } from '../components/InstallControls';
|
||||
@@ -35,7 +36,7 @@ export const PluginSubtitle = ({ plugin }: Props) => {
|
||||
</Alert>
|
||||
)}
|
||||
{plugin?.description && <div>{plugin?.description}</div>}
|
||||
{plugin?.details?.links && plugin.details.links.length > 0 && (
|
||||
{!config.featureToggles.pluginsDetailsRightPanel && !!plugin?.details?.links?.length && (
|
||||
<span>
|
||||
{plugin.details.links.map((link, index) => (
|
||||
<Fragment key={index}>
|
||||
|
||||
@@ -35,6 +35,15 @@ export const usePluginDetailsTabs = (plugin?: CatalogPlugin, pageId?: PluginTabI
|
||||
active: PluginTabIds.VERSIONS === currentPageId,
|
||||
});
|
||||
}
|
||||
if (isPublished && plugin?.details?.changelog) {
|
||||
navModelChildren.push({
|
||||
text: PluginTabLabels.CHANGELOG,
|
||||
id: PluginTabIds.CHANGELOG,
|
||||
icon: 'rocket',
|
||||
url: `${pathname}?page=${PluginTabIds.CHANGELOG}`,
|
||||
active: PluginTabIds.CHANGELOG === currentPageId,
|
||||
});
|
||||
}
|
||||
|
||||
// Not extending the tabs with the config pages if the plugin is not installed
|
||||
if (!pluginConfig) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2, PluginSignatureType } from '@grafana/data';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { PageInfoItem } from '../../../../core/components/Page/types';
|
||||
import { PluginDisabledBadge } from '../components/Badges';
|
||||
@@ -27,12 +28,12 @@ export const usePluginInfo = (plugin?: CatalogPlugin): PageInfoItem[] => {
|
||||
if (Boolean(version)) {
|
||||
if (plugin.isManaged) {
|
||||
info.push({
|
||||
label: 'Version',
|
||||
label: t('plugins.details.labels.version', 'Version'),
|
||||
value: 'Managed by Grafana',
|
||||
});
|
||||
} else {
|
||||
info.push({
|
||||
label: 'Version',
|
||||
label: t('plugins.details.labels.version', 'Version'),
|
||||
value: version,
|
||||
});
|
||||
}
|
||||
@@ -40,7 +41,7 @@ export const usePluginInfo = (plugin?: CatalogPlugin): PageInfoItem[] => {
|
||||
|
||||
if (Boolean(plugin.orgName)) {
|
||||
info.push({
|
||||
label: 'From',
|
||||
label: t('plugins.details.labels.from', 'From'),
|
||||
value: plugin.orgName,
|
||||
});
|
||||
}
|
||||
@@ -51,7 +52,7 @@ export const usePluginInfo = (plugin?: CatalogPlugin): PageInfoItem[] => {
|
||||
plugin.signatureType === PluginSignatureType.commercial;
|
||||
if (showDownloads && Boolean(plugin.downloads > 0)) {
|
||||
info.push({
|
||||
label: 'Downloads',
|
||||
label: t('plugins.details.labels.downloads', 'Downloads'),
|
||||
value: new Intl.NumberFormat().format(plugin.downloads),
|
||||
});
|
||||
}
|
||||
@@ -65,20 +66,20 @@ export const usePluginInfo = (plugin?: CatalogPlugin): PageInfoItem[] => {
|
||||
|
||||
if (!hasNoDependencyInfo) {
|
||||
info.push({
|
||||
label: 'Dependencies',
|
||||
label: t('plugins.details.labels.dependencies', 'Dependencies'),
|
||||
value: <PluginDetailsHeaderDependencies plugin={plugin} grafanaDependency={grafanaDependency} />,
|
||||
});
|
||||
}
|
||||
|
||||
if (plugin.isDisabled) {
|
||||
info.push({
|
||||
label: 'Status',
|
||||
label: t('plugins.details.labels.status', 'Status'),
|
||||
value: <PluginDisabledBadge error={plugin.error!} />,
|
||||
});
|
||||
}
|
||||
|
||||
info.push({
|
||||
label: 'Signature',
|
||||
label: t('plugins.details.labels.signature', 'Signature'),
|
||||
value: <PluginDetailsHeaderSignature plugin={plugin} />,
|
||||
});
|
||||
|
||||
|
||||
@@ -215,7 +215,7 @@ describe('Plugin details page', () => {
|
||||
it('should display a "Signed" badge if the plugin signature is verified', async () => {
|
||||
const { queryByText } = renderPluginDetails({ id, signature: PluginSignatureStatus.valid });
|
||||
|
||||
expect(await queryByText('Signed')).toBeInTheDocument();
|
||||
expect(await queryByText('community')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display a "Missing signature" badge if the plugin signature is missing', async () => {
|
||||
@@ -880,4 +880,42 @@ describe('Plugin details page', () => {
|
||||
expect(queryByText('Add new data source')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Display plugin details right panel', () => {
|
||||
beforeAll(() => {
|
||||
mockUserPermissions({
|
||||
isAdmin: true,
|
||||
isDataSourceEditor: false,
|
||||
isOrgAdmin: true,
|
||||
});
|
||||
config.featureToggles.pluginsDetailsRightPanel = true;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
config.featureToggles.pluginsDetailsRightPanel = false;
|
||||
});
|
||||
|
||||
it('should display Last updated and report abuse information', async () => {
|
||||
const id = 'right-panel-test-plugin';
|
||||
const updatedAt = '2023-10-26T16:54:55.000Z';
|
||||
const { queryByText } = renderPluginDetails({ id, updatedAt });
|
||||
expect(queryByText('Last updated:')).toBeVisible();
|
||||
expect(queryByText('10/26/2023')).toBeVisible();
|
||||
expect(queryByText('Report Abuse')).toBeVisible();
|
||||
});
|
||||
|
||||
it('should not display Last updated if there is no updated At data', async () => {
|
||||
const id = 'right-panel-test-plugin';
|
||||
const updatedAt = undefined;
|
||||
const { queryByText } = renderPluginDetails({ id, updatedAt });
|
||||
expect(queryByText('Last updated:')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not display Report Abuse if the plugin is Core', async () => {
|
||||
const id = 'right-panel-test-plugin';
|
||||
const isCore = true;
|
||||
const { queryByText } = renderPluginDetails({ id, isCore });
|
||||
expect(queryByText('Report Abuse')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,6 +80,7 @@ export interface CatalogPluginDetails {
|
||||
pluginDependencies?: PluginDependencies['plugins'];
|
||||
statusContext?: string;
|
||||
iam?: IdentityAccessManagement;
|
||||
changelog?: string;
|
||||
}
|
||||
|
||||
export interface CatalogPluginInfo {
|
||||
@@ -91,6 +92,7 @@ export interface CatalogPluginInfo {
|
||||
}
|
||||
|
||||
export type RemotePlugin = {
|
||||
changelog: string;
|
||||
createdAt: string;
|
||||
description: string;
|
||||
downloads: number;
|
||||
@@ -252,6 +254,7 @@ export enum PluginTabLabels {
|
||||
DASHBOARDS = 'Dashboards',
|
||||
USAGE = 'Usage',
|
||||
IAM = 'IAM',
|
||||
CHANGELOG = 'Changelog',
|
||||
}
|
||||
|
||||
export enum PluginTabIds {
|
||||
@@ -261,6 +264,7 @@ export enum PluginTabIds {
|
||||
DASHBOARDS = 'dashboards',
|
||||
USAGE = 'usage',
|
||||
IAM = 'iam',
|
||||
CHANGELOG = 'changelog',
|
||||
}
|
||||
|
||||
export enum RequestStatus {
|
||||
|
||||
@@ -1656,6 +1656,18 @@
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"details": {
|
||||
"labels": {
|
||||
"dependencies": "Dependencies",
|
||||
"downloads": "Downloads",
|
||||
"from": "From",
|
||||
"reportAbuse": "Report Abuse",
|
||||
"signature": "Signature",
|
||||
"status": "Status",
|
||||
"updatedAt": "Last updated: ",
|
||||
"version": "Version"
|
||||
}
|
||||
},
|
||||
"empty-state": {
|
||||
"message": "No plugins found"
|
||||
}
|
||||
|
||||
@@ -1656,6 +1656,18 @@
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"details": {
|
||||
"labels": {
|
||||
"dependencies": "Đępęʼnđęʼnčįęş",
|
||||
"downloads": "Đőŵʼnľőäđş",
|
||||
"from": "Fřőm",
|
||||
"reportAbuse": "Ŗępőřŧ Åþūşę",
|
||||
"signature": "Ŝįģʼnäŧūřę",
|
||||
"status": "Ŝŧäŧūş",
|
||||
"updatedAt": "Ŀäşŧ ūpđäŧęđ: ",
|
||||
"version": "Vęřşįőʼn"
|
||||
}
|
||||
},
|
||||
"empty-state": {
|
||||
"message": "Ńő pľūģįʼnş ƒőūʼnđ"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user