From 8044cb50f17a021a32e7ac0b83cf8d723457a9ae Mon Sep 17 00:00:00 2001 From: Yulia Shanyrova Date: Tue, 13 Aug 2024 11:55:30 +0200 Subject: [PATCH] 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 * 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 --- .betterer.results | 9 +- .github/CODEOWNERS | 1 + .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 1 + .../PluginSignatureBadge.tsx | 37 +++++++- pkg/services/featuremgmt/registry.go | 7 ++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 16 ++++ .../admin/__mocks__/remotePlugin.mock.ts | 1 + public/app/features/plugins/admin/api.ts | 18 +++- .../plugins/admin/components/Changelog.tsx | 44 +++++++++ .../InstallControlsWarning.tsx | 93 ++++++++++++------- .../admin/components/PluginDetailsBody.tsx | 5 + .../PluginDetailsHeaderSignature.tsx | 15 ++- .../admin/components/PluginDetailsPage.tsx | 44 +++++---- .../components/PluginDetailsRightPanel.tsx | 55 +++++++++++ .../admin/components/PluginSubtitle.tsx | 3 +- .../admin/hooks/usePluginDetailsTabs.tsx | 9 ++ .../plugins/admin/hooks/usePluginInfo.tsx | 15 +-- .../admin/pages/PluginDetails.test.tsx | 40 +++++++- public/app/features/plugins/admin/types.ts | 4 + public/locales/en-US/grafana.json | 12 +++ public/locales/pseudo-LOCALE/grafana.json | 12 +++ 24 files changed, 362 insertions(+), 85 deletions(-) create mode 100644 public/app/features/plugins/admin/components/Changelog.tsx create mode 100644 public/app/features/plugins/admin/components/PluginDetailsRightPanel.tsx diff --git a/.betterer.results b/.betterer.results index 6e962654114..576dfc83f2e 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4848,17 +4848,12 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] ], "public/app/features/plugins/admin/components/InstallControls/InstallControlsWarning.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "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 ", "1"], [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "8"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "9"], - [0, 0, 0, "Styles should be written using objects.", "10"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] ], "public/app/features/plugins/admin/components/InstallControls/index.tsx:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./InstallControlsWarning\`)", "0"], diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index caf8cb2ac84..2f250370c42 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 31128abc74b..74f26f97f6f 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -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 | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 84290064f75..e009b3da487 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -77,6 +77,7 @@ export interface FeatureToggles { lokiPredefinedOperations?: boolean; pluginsFrontendSandbox?: boolean; frontendSandboxMonitorOnly?: boolean; + pluginsDetailsRightPanel?: boolean; sqlDatasourceDatabaseSelection?: boolean; recordedQueriesMulti?: boolean; vizAndWidgetSplit?: boolean; diff --git a/packages/grafana-ui/src/components/PluginSignatureBadge/PluginSignatureBadge.tsx b/packages/grafana-ui/src/components/PluginSignatureBadge/PluginSignatureBadge.tsx index af8720cca61..403a7c4b408 100644 --- a/packages/grafana-ui/src/components/PluginSignatureBadge/PluginSignatureBadge.tsx +++ b/packages/grafana-ui/src/components/PluginSignatureBadge/PluginSignatureBadge.tsx @@ -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 = { + [PluginSignatureType.grafana]: 'grafana', + [PluginSignatureType.commercial]: 'shield', + [PluginSignatureType.community]: 'shield', + DEFAULT: 'shield-exclamation', +}; + /** * @public */ export interface PluginSignatureBadgeProps extends HTMLAttributes { 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 ( ); @@ -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', diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 70023e4e489..0426f7fcf4a 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -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", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index edd8800cd30..344650d1c7b 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -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 diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 84381836572..90e3724901d 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -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" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 931fa9b8524..16585bbf70b 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -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", diff --git a/public/app/features/plugins/admin/__mocks__/remotePlugin.mock.ts b/public/app/features/plugins/admin/__mocks__/remotePlugin.mock.ts index 52b9d0a825c..d649c990042 100644 --- a/public/app/features/plugins/admin/__mocks__/remotePlugin.mock.ts +++ b/public/app/features/plugins/admin/__mocks__/remotePlugin.mock.ts @@ -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, diff --git a/public/app/features/plugins/admin/api.ts b/public/app/features/plugins/admin/api.ts index a86e32d6a1a..74415b39779 100644 --- a/public/app/features/plugins/admin/api.ts +++ b/public/app/features/plugins/admin/api.ts @@ -18,10 +18,11 @@ export async function getPluginDetails(id: string): Promise p.id === id); @@ -35,6 +36,7 @@ export async function getPluginDetails(id: string): Promise { } } +async function getLocalPluginChangelog(id: string): Promise { + 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 { const localPlugins: LocalPlugin[] = await getBackendSrv().get( `${API_ROOT}`, diff --git a/public/app/features/plugins/admin/components/Changelog.tsx b/public/app/features/plugins/admin/components/Changelog.tsx new file mode 100644 index 00000000000..97b9c78f132 --- /dev/null +++ b/public/app/features/plugins/admin/components/Changelog.tsx @@ -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 ( +
+ ); +}; + +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', + }, + }, + }), +}); diff --git a/public/app/features/plugins/admin/components/InstallControls/InstallControlsWarning.tsx b/public/app/features/plugins/admin/components/InstallControls/InstallControlsWarning.tsx index 9d10b8a6728..0cbf52ca151 100644 --- a/public/app/features/plugins/admin/components/InstallControls/InstallControlsWarning.tsx +++ b/public/app/features/plugins/admin/components/InstallControls/InstallControlsWarning.tsx @@ -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
Renderer plugins cannot be managed by the Plugin Catalog.
; + return ( + + ); } if (plugin.type === PluginType.secretsmanager) { - return
Secrets manager plugins cannot be managed by the Plugin Catalog.
; + return ( + + ); } if (plugin.isEnterprise && !featureEnabled('enterprise.plugins')) { return ( - - No valid Grafana Enterprise license detected. - - Learn more - - + + + No valid Grafana Enterprise license detected. + + Learn more + + + ); } if (plugin.isDev) { return ( -
This is a development build of the plugin and can't be uninstalled.
+ ); } if (!hasPermission && !isExternallyManaged) { - return
{statusToMessage(pluginStatus)}
; + return ; } if (!plugin.isPublished) { return ( -
- This plugin is not published to{' '} - - grafana.com/plugins - {' '} - and can't be managed via the catalog. -
+ +
+ This plugin is not published to{' '} + + grafana.com/plugins + {' '} + and can't be managed via the catalog. +
+
); } if (!isCompatible) { return ( -
- -  This plugin doesn't support your version of Grafana. -
+ ); } if (!isRemotePluginsAvailable) { return ( -
- The install controls have been disabled because the Grafana server cannot access grafana.com. -
+ ); } @@ -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)}`, + }), }; }; diff --git a/public/app/features/plugins/admin/components/PluginDetailsBody.tsx b/public/app/features/plugins/admin/components/PluginDetailsBody.tsx index 1c31ffb7b15..a905dbe30ee 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsBody.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsBody.tsx @@ -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 ; + } + if (pageId === PluginTabIds.CONFIG && pluginConfig?.angularConfigCtrl) { return (
diff --git a/public/app/features/plugins/admin/components/PluginDetailsHeaderSignature.tsx b/public/app/features/plugins/admin/components/PluginDetailsHeaderSignature.tsx index 375bb27171a..5314def26a6 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsHeaderSignature.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsHeaderSignature.tsx @@ -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 (
@@ -25,12 +22,12 @@ export function PluginDetailsHeaderSignature({ plugin }: Props): React.ReactElem rel="noreferrer" className={styles.link} > - + - - {isSignatureValid && ( - - )}
); } diff --git a/public/app/features/plugins/admin/components/PluginDetailsPage.tsx b/public/app/features/plugins/admin/components/PluginDetailsPage.tsx index c2dbbea44f3..47d03e5c5fd 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsPage.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsPage.tsx @@ -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 ( - - - - {plugin.angularDetected && ( - - )} - - - - - - + + + + + {plugin.angularDetected && ( + + )} + + + + + + + {config.featureToggles.pluginsDetailsRightPanel && } + ); } diff --git a/public/app/features/plugins/admin/components/PluginDetailsRightPanel.tsx b/public/app/features/plugins/admin/components/PluginDetailsRightPanel.tsx new file mode 100644 index 00000000000..61303e61e33 --- /dev/null +++ b/public/app/features/plugins/admin/components/PluginDetailsRightPanel.tsx @@ -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 ( + + {info.map((infoItem, index) => { + return ( + + {infoItem.label + ':'} +
{infoItem.value}
+
+ ); + })} + + {plugin.updatedAt && ( +
+ + Last updated: + {' '} + {formatDate(new Date(plugin.updatedAt))} +
+ )} + + {plugin?.details?.links && plugin.details?.links?.length > 0 && ( + + {plugin.details.links.map((link, index) => ( +
+ + {link.name} + +
+ ))} +
+ )} + + {!plugin?.isCore && ( + + Report Abuse + + )} +
+ ); +} diff --git a/public/app/features/plugins/admin/components/PluginSubtitle.tsx b/public/app/features/plugins/admin/components/PluginSubtitle.tsx index ee1f42c18e8..53e604bd110 100644 --- a/public/app/features/plugins/admin/components/PluginSubtitle.tsx +++ b/public/app/features/plugins/admin/components/PluginSubtitle.tsx @@ -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) => { )} {plugin?.description &&
{plugin?.description}
} - {plugin?.details?.links && plugin.details.links.length > 0 && ( + {!config.featureToggles.pluginsDetailsRightPanel && !!plugin?.details?.links?.length && ( {plugin.details.links.map((link, index) => ( diff --git a/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx b/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx index 5ff258b5916..3173e5df198 100644 --- a/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx +++ b/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx @@ -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) { diff --git a/public/app/features/plugins/admin/hooks/usePluginInfo.tsx b/public/app/features/plugins/admin/hooks/usePluginInfo.tsx index e50b393e022..54ccebbb21a 100644 --- a/public/app/features/plugins/admin/hooks/usePluginInfo.tsx +++ b/public/app/features/plugins/admin/hooks/usePluginInfo.tsx @@ -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: , }); } if (plugin.isDisabled) { info.push({ - label: 'Status', + label: t('plugins.details.labels.status', 'Status'), value: , }); } info.push({ - label: 'Signature', + label: t('plugins.details.labels.signature', 'Signature'), value: , }); diff --git a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx index e13a855336b..931d20cb16a 100644 --- a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx +++ b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx @@ -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(); + }); + }); }); diff --git a/public/app/features/plugins/admin/types.ts b/public/app/features/plugins/admin/types.ts index aa6b41c7737..0991c0921bb 100644 --- a/public/app/features/plugins/admin/types.ts +++ b/public/app/features/plugins/admin/types.ts @@ -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 { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index f5a96871f45..2cefb4996dd 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -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" } diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 729d80a2982..3ca86e26862 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -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đ" }