diff --git a/.betterer.results b/.betterer.results index 89b413d6023..8db179df663 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5501,7 +5501,8 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] ], "public/app/features/plugins/admin/components/PluginDetailsPanel.tsx:5381": [ - [0, 0, 0, "\'@grafana/runtime/src/components/PluginPage\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] + [0, 0, 0, "\'@grafana/runtime/src/components/PluginPage\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] ], "public/app/features/plugins/admin/components/PluginDetailsSignature.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], diff --git a/public/app/features/plugins/admin/api.ts b/public/app/features/plugins/admin/api.ts index 36d49c2dbd7..f79968d47ae 100644 --- a/public/app/features/plugins/admin/api.ts +++ b/public/app/features/plugins/admin/api.ts @@ -37,6 +37,8 @@ export async function getPluginDetails(id: string): Promise { it('should render report abuse section for non-core plugins', () => { render(); expect(screen.getByText('Report a concern')).toBeInTheDocument(); - expect(screen.getByText('Contact Grafana Labs')).toBeInTheDocument(); }); it('should not render report abuse section for core plugins', () => { @@ -117,6 +116,6 @@ describe('PluginDetailsPanel', () => { it('should respect custom width prop', () => { render(); const panel = screen.getByTestId('plugin-details-panel'); - expect(panel).toHaveStyle({ maxWidth: '300px' }); + expect(panel).toHaveStyle({ width: '300px' }); }); }); diff --git a/public/app/features/plugins/admin/components/PluginDetailsPanel.tsx b/public/app/features/plugins/admin/components/PluginDetailsPanel.tsx index 95b963097b7..7d7612dc814 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsPanel.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsPanel.tsx @@ -1,8 +1,22 @@ import { css } from '@emotion/css'; +import { useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; +import { reportInteraction } from '@grafana/runtime'; import { PageInfoItem } from '@grafana/runtime/src/components/PluginPage'; -import { Stack, Text, LinkButton, Box, TextLink, useStyles2 } from '@grafana/ui'; +import { + Stack, + Text, + LinkButton, + Box, + TextLink, + CollapsableSection, + Tooltip, + Icon, + Modal, + Button, + useStyles2, +} from '@grafana/ui'; import { Trans } from 'app/core/internationalization'; import { formatDate } from 'app/core/internationalization/dates'; @@ -16,73 +30,203 @@ type Props = { export function PluginDetailsPanel(props: Props): React.ReactElement | null { const { pluginExtentionsInfo, plugin, width = '250px' } = props; + const [reportAbuseModalOpen, setReportAbuseModalOpen] = useState(false); + + const normalizeURL = (url: string | undefined) => url?.replace(/\/$/, ''); + + const customLinks = plugin.details?.links?.filter((link) => { + const customLinksFiltered = ![plugin.url, plugin.details?.licenseUrl, plugin.details?.documentationUrl] + .map(normalizeURL) + .includes(normalizeURL(link.url)); + return customLinksFiltered; + }); + const shouldRenderLinks = plugin.url || plugin.details?.licenseUrl || plugin.details?.documentationUrl; + const styles = useStyles2(getStyles); - return ( - - - - {pluginExtentionsInfo.map((infoItem, index) => { - return ( - - {infoItem.label + ':'} -
{infoItem.value}
-
- ); - })} - {plugin.updatedAt && ( - - - Last updated: - {' '} - {formatDate(new Date(plugin.updatedAt), { day: 'numeric', month: 'short', year: 'numeric' })} - - )} - {plugin?.details?.lastCommitDate && ( - - - Last commit date: - {' '} - - {formatDate(new Date(plugin.details.lastCommitDate), { - day: 'numeric', - month: 'short', - year: 'numeric', - })} - - - )} -
-
+ const onClickReportConcern = (pluginId: string) => { + setReportAbuseModalOpen(true); + reportInteraction('plugin_detail_report_concern', { + plugin_id: pluginId, + }); + }; - {plugin?.details?.links && plugin.details?.links?.length > 0 && ( + return ( + <> + - - Links - - {plugin.details.links.map((link, index) => ( - - {link.name} - - ))} + {pluginExtentionsInfo.map((infoItem, index) => { + return ( + + {infoItem.label + ':'} +
{infoItem.value}
+
+ ); + })} + {plugin.updatedAt && ( + + + Last updated: + {' '} + + {formatDate(new Date(plugin.updatedAt), { day: 'numeric', month: 'short', year: 'numeric' })} + + + )} + {plugin?.details?.lastCommitDate && ( + + + Last commit date: + {' '} + + {formatDate(new Date(plugin.details.lastCommitDate), { + day: 'numeric', + month: 'short', + year: 'numeric', + })} + + + )}
- )} - - {!plugin?.isCore && ( - - - - Report a concern + {shouldRenderLinks && ( + <> + + + {plugin.url && ( + + Repository + + )} + {plugin.raiseAnIssueUrl && ( + + Raise an issue + + )} + {plugin.details?.licenseUrl && ( + + License + + )} + {plugin.details?.documentationUrl && ( + + Documentation + + )} + + + + )} + {customLinks && customLinks?.length > 0 && ( + + + + Custom links + + + These links are provided by the plugin developer to offer additional, developer-specific + resources and information +
+ } + placement="right-end" + > + + + + } + > + + {customLinks.map((link, index) => ( + + {link.name} + + ))} + + + + )} + {!plugin?.isCore && ( + + + + Report a concern + + + Report issues related to malicious or harmful plugins directly to Grafana Labs. +
+ } + placement="right-end" + > + + + + } + > + + + + + + )} + + {reportAbuseModalOpen && ( + Report a plugin concern
} + isOpen + onDismiss={() => setReportAbuseModalOpen(false)} + > + + + + This feature is for reporting malicious or harmful behaviour within plugins. For plugin concerns, email + us at:{' '} + + integrations@grafana.com + + + + Note: For general plugin issues like bugs or feature requests, please contact the plugin author using + the provided links.{' '} + - - Contact Grafana Labs - - + + + + + )} - + ); } diff --git a/public/app/features/plugins/admin/helpers.test.ts b/public/app/features/plugins/admin/helpers.test.ts index b47c5bf774b..b1b109f2ca5 100644 --- a/public/app/features/plugins/admin/helpers.test.ts +++ b/public/app/features/plugins/admin/helpers.test.ts @@ -217,6 +217,7 @@ describe('Plugins/Helpers', () => { updatedAt: '2021-05-18T14:53:01.000Z', isFullyInstalled: false, angularDetected: false, + url: 'https://github.com/alexanderzobnin/grafana-zabbix', }); }); @@ -354,6 +355,7 @@ describe('Plugins/Helpers', () => { installedVersion: '4.2.2', isFullyInstalled: true, angularDetected: false, + url: 'https://github.com/alexanderzobnin/grafana-zabbix', }); }); diff --git a/public/app/features/plugins/admin/helpers.ts b/public/app/features/plugins/admin/helpers.ts index 172a32e088f..2f7814cc729 100644 --- a/public/app/features/plugins/admin/helpers.ts +++ b/public/app/features/plugins/admin/helpers.ts @@ -121,6 +121,8 @@ export function mapRemoteToCatalog(plugin: RemotePlugin, error?: PluginError): C signatureType, versionSignatureType, versionSignedByOrgName, + url, + raiseAnIssueUrl, } = plugin; const isDisabled = !!error || isDisabledSecretsPlugin(typeCode); @@ -158,6 +160,8 @@ export function mapRemoteToCatalog(plugin: RemotePlugin, error?: PluginError): C angularDetected, isFullyInstalled: isDisabled, latestVersion: plugin.version, + url, + raiseAnIssueUrl, }; } @@ -174,6 +178,7 @@ export function mapLocalToCatalog(plugin: LocalPlugin, error?: PluginError): Cat hasUpdate, accessControl, angularDetected, + raiseAnIssueUrl, } = plugin; const isDisabled = !!error || isDisabledSecretsPlugin(type); @@ -208,6 +213,7 @@ export function mapLocalToCatalog(plugin: LocalPlugin, error?: PluginError): Cat isFullyInstalled: true, iam: plugin.iam, latestVersion: plugin.latestVersion, + raiseAnIssueUrl, }; } @@ -271,6 +277,8 @@ export function mapToCatalogPlugin(local?: LocalPlugin, remote?: RemotePlugin, e isFullyInstalled: Boolean(local) || isDisabled, iam: local?.iam, latestVersion: local?.latestVersion || remote?.version || '', + url: remote?.url || '', + raiseAnIssueUrl: remote?.raiseAnIssueUrl || local?.raiseAnIssueUrl, }; } diff --git a/public/app/features/plugins/admin/types.ts b/public/app/features/plugins/admin/types.ts index 031f6c1543c..f3d5783ae3c 100644 --- a/public/app/features/plugins/admin/types.ts +++ b/public/app/features/plugins/admin/types.ts @@ -64,6 +64,8 @@ export interface CatalogPlugin extends WithAccessControlMetadata { isUpdatingFromInstance?: boolean; iam?: IdentityAccessManagement; isProvisioned?: boolean; + url?: string; + raiseAnIssueUrl?: string; } export interface CatalogPluginDetails { @@ -79,6 +81,8 @@ export interface CatalogPluginDetails { iam?: IdentityAccessManagement; changelog?: string; lastCommitDate?: string; + licenseUrl?: string; + documentationUrl?: string; signatureType?: PluginSignatureType; signature?: PluginSignatureStatus; } @@ -143,6 +147,9 @@ export type RemotePlugin = { versionStatus: string; angularDetected?: boolean; lastCommitDate?: string; + licenseUrl?: string; + documentationUrl?: string; + raiseAnIssueUrl?: string; }; // The available status codes on GCOM are available here: @@ -190,6 +197,7 @@ export type LocalPlugin = WithAccessControlMetadata & { dependencies: PluginDependencies; angularDetected: boolean; iam?: IdentityAccessManagement; + raiseAnIssueUrl?: string; }; interface IdentityAccessManagement { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 2c90bf29511..b7ea5026077 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -2781,17 +2781,30 @@ }, "labels": { "contactGrafanaLabs": "Contact Grafana Labs", + "customLinks": "Custom links ", + "customLinksTooltip": "These links are provided by the plugin developer to offer additional, developer-specific resources and information", "dependencies": "Dependencies", + "documentation": "Documentation", "downloads": "Downloads", "from": "From", "installedVersion": "Installed Version", "lastCommitDate": "Last commit date:", "latestVersion": "Latest Version", - "links": "Links ", + "license": "License", + "raiseAnIssue": "Raise an issue", "reportAbuse": "Report a concern ", + "reportAbuseTooltip": "Report issues related to malicious or harmful plugins directly to Grafana Labs.", + "repository": "Repository", "signature": "Signature", "status": "Status", "updatedAt": "Last updated:" + }, + "modal": { + "cancel": "Cancel", + "copyEmail": "Copy email address", + "description": "This feature is for reporting malicious or harmful behaviour within plugins. For plugin concerns, email us at: ", + "node": "Note: For general plugin issues like bugs or feature requests, please contact the plugin author using the provided links. ", + "title": "Report a plugin concern" } }, "empty-state": { diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 61b47f9f9a5..9543a62f07e 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -2781,17 +2781,30 @@ }, "labels": { "contactGrafanaLabs": "Cőʼnŧäčŧ Ğřäƒäʼnä Ŀäþş", + "customLinks": "Cūşŧőm ľįʼnĸş ", + "customLinksTooltip": "Ŧĥęşę ľįʼnĸş äřę přővįđęđ þy ŧĥę pľūģįʼn đęvęľőpęř ŧő őƒƒęř äđđįŧįőʼnäľ, đęvęľőpęř-şpęčįƒįč řęşőūřčęş äʼnđ įʼnƒőřmäŧįőʼn", "dependencies": "Đępęʼnđęʼnčįęş", + "documentation": "Đőčūmęʼnŧäŧįőʼn", "downloads": "Đőŵʼnľőäđş", "from": "Fřőm", "installedVersion": "Ĩʼnşŧäľľęđ Vęřşįőʼn", "lastCommitDate": "Ŀäşŧ čőmmįŧ đäŧę:", "latestVersion": "Ŀäŧęşŧ Vęřşįőʼn", - "links": "Ŀįʼnĸş ", + "license": "Ŀįčęʼnşę", + "raiseAnIssue": "Ŗäįşę äʼn įşşūę", "reportAbuse": "Ŗępőřŧ ä čőʼnčęřʼn ", + "reportAbuseTooltip": "Ŗępőřŧ įşşūęş řęľäŧęđ ŧő mäľįčįőūş őř ĥäřmƒūľ pľūģįʼnş đįřęčŧľy ŧő Ğřäƒäʼnä Ŀäþş.", + "repository": "Ŗępőşįŧőřy", "signature": "Ŝįģʼnäŧūřę", "status": "Ŝŧäŧūş", "updatedAt": "Ŀäşŧ ūpđäŧęđ:" + }, + "modal": { + "cancel": "Cäʼnčęľ", + "copyEmail": "Cőpy ęmäįľ äđđřęşş", + "description": "Ŧĥįş ƒęäŧūřę įş ƒőř řępőřŧįʼnģ mäľįčįőūş őř ĥäřmƒūľ þęĥävįőūř ŵįŧĥįʼn pľūģįʼnş. Főř pľūģįʼn čőʼnčęřʼnş, ęmäįľ ūş äŧ: ", + "node": "Ńőŧę: Főř ģęʼnęřäľ pľūģįʼn įşşūęş ľįĸę þūģş őř ƒęäŧūřę řęqūęşŧş, pľęäşę čőʼnŧäčŧ ŧĥę pľūģįʼn äūŧĥőř ūşįʼnģ ŧĥę přővįđęđ ľįʼnĸş. ", + "title": "Ŗępőřŧ ä pľūģįʼn čőʼnčęřʼn" } }, "empty-state": {