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>
pull/91833/head
Yulia Shanyrova 11 months ago committed by GitHub
parent bac68069e0
commit 8044cb50f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 9
      .betterer.results
  2. 1
      .github/CODEOWNERS
  3. 1
      docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
  4. 1
      packages/grafana-data/src/types/featureToggles.gen.ts
  5. 37
      packages/grafana-ui/src/components/PluginSignatureBadge/PluginSignatureBadge.tsx
  6. 7
      pkg/services/featuremgmt/registry.go
  7. 1
      pkg/services/featuremgmt/toggles_gen.csv
  8. 4
      pkg/services/featuremgmt/toggles_gen.go
  9. 16
      pkg/services/featuremgmt/toggles_gen.json
  10. 1
      public/app/features/plugins/admin/__mocks__/remotePlugin.mock.ts
  11. 18
      public/app/features/plugins/admin/api.ts
  12. 44
      public/app/features/plugins/admin/components/Changelog.tsx
  13. 93
      public/app/features/plugins/admin/components/InstallControls/InstallControlsWarning.tsx
  14. 5
      public/app/features/plugins/admin/components/PluginDetailsBody.tsx
  15. 15
      public/app/features/plugins/admin/components/PluginDetailsHeaderSignature.tsx
  16. 44
      public/app/features/plugins/admin/components/PluginDetailsPage.tsx
  17. 55
      public/app/features/plugins/admin/components/PluginDetailsRightPanel.tsx
  18. 3
      public/app/features/plugins/admin/components/PluginSubtitle.tsx
  19. 9
      public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx
  20. 15
      public/app/features/plugins/admin/hooks/usePluginInfo.tsx
  21. 40
      public/app/features/plugins/admin/pages/PluginDetails.test.tsx
  22. 4
      public/app/features/plugins/admin/types.ts
  23. 12
      public/locales/en-US/grafana.json
  24. 12
      public/locales/pseudo-LOCALE/grafana.json

@ -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"],

@ -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

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
58 lokiPredefinedOperations experimental @grafana/observability-logs false false true
59 pluginsFrontendSandbox experimental @grafana/plugins-platform-backend false false true
60 frontendSandboxMonitorOnly experimental @grafana/plugins-platform-backend false false true
61 pluginsDetailsRightPanel experimental @grafana/plugins-platform-backend false false true
62 sqlDatasourceDatabaseSelection preview @grafana/dataviz-squad false false true
63 recordedQueriesMulti GA @grafana/observability-metrics false false false
64 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}`,

@ -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&#39;t be uninstalled.</div>
<Alert
severity="warning"
title="This is a development build of the plugin and can&#39;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&#39;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&#39;t be managed via the catalog.
</div>
</Alert>
);
}
if (!isCompatible) {
return (
<div className={styles.message}>
<Icon name="exclamation-triangle" />
&nbsp;This plugin doesn&#39;t support your version of Grafana.
</div>
<Alert
severity="warning"
title="This plugin doesn&#39;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đ"
}

Loading…
Cancel
Save