Plugins: Custom links for plugin details page (#97186)

* Custom links with repository link, licence link, docs link and raise an issue link

* run translation command

* delete console log

* delete console log

* fix frontend tests

* change UI with a new design

* remove license, documentation, repository url calculation logic from grafana

* remove unsused function from helpers

* change repo icons and raise an issue icon

* fix the build

* remove logic for raiseAnIssueUrl

* fix the build

* fix lint

* Delete Links title in the box of links

---------

Co-authored-by: Timur Olzhabayev <timur.olzhabayev@grafana.com>
pull/100587/head
Yulia Shanyrova 4 months ago committed by GitHub
parent 0b4c622df8
commit 6db155649c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      .betterer.results
  2. 2
      public/app/features/plugins/admin/api.ts
  3. 3
      public/app/features/plugins/admin/components/PluginDetailsPanel.test.tsx
  4. 260
      public/app/features/plugins/admin/components/PluginDetailsPanel.tsx
  5. 2
      public/app/features/plugins/admin/helpers.test.ts
  6. 8
      public/app/features/plugins/admin/helpers.ts
  7. 8
      public/app/features/plugins/admin/types.ts
  8. 15
      public/locales/en-US/grafana.json
  9. 15
      public/locales/pseudo-LOCALE/grafana.json

@ -5501,7 +5501,8 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "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 <Trans />", "1"]
],
"public/app/features/plugins/admin/components/PluginDetailsSignature.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],

@ -37,6 +37,8 @@ export async function getPluginDetails(id: string): Promise<CatalogPluginDetails
iam: remote?.json?.iam,
lastCommitDate: remote?.lastCommitDate,
changelog: remote?.changelog || localChangelog,
licenseUrl: remote?.licenseUrl,
documentationUrl: remote?.documentationUrl,
signatureType: local?.signatureType || (remote?.signatureType !== '' ? remote?.signatureType : undefined),
signature: local?.signature,
};

@ -105,7 +105,6 @@ describe('PluginDetailsPanel', () => {
it('should render report abuse section for non-core plugins', () => {
render(<PluginDetailsPanel plugin={mockPlugin} pluginExtentionsInfo={mockInfo} />);
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(<PluginDetailsPanel plugin={mockPlugin} pluginExtentionsInfo={mockInfo} width="300px" />);
const panel = screen.getByTestId('plugin-details-panel');
expect(panel).toHaveStyle({ maxWidth: '300px' });
expect(panel).toHaveStyle({ width: '300px' });
});
});

@ -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 (
<Stack direction="column" gap={3} shrink={0} grow={0} maxWidth={width} data-testid="plugin-details-panel">
<Box padding={2} borderColor="medium" borderStyle="solid">
<Stack direction="column" gap={2}>
{pluginExtentionsInfo.map((infoItem, index) => {
return (
<Stack key={index} wrap direction="column" gap={0.5}>
<Text color="secondary">{infoItem.label + ':'}</Text>
<div className={styles.pluginVersionDetails}>{infoItem.value}</div>
</Stack>
);
})}
{plugin.updatedAt && (
<Stack direction="column" gap={0.5}>
<Text color="secondary">
<Trans i18nKey="plugins.details.labels.updatedAt">Last updated:</Trans>
</Text>{' '}
<Text>{formatDate(new Date(plugin.updatedAt), { day: 'numeric', month: 'short', year: 'numeric' })}</Text>
</Stack>
)}
{plugin?.details?.lastCommitDate && (
<Stack direction="column" gap={0.5}>
<Text color="secondary">
<Trans i18nKey="plugins.details.labels.lastCommitDate">Last commit date:</Trans>
</Text>{' '}
<Text>
{formatDate(new Date(plugin.details.lastCommitDate), {
day: 'numeric',
month: 'short',
year: 'numeric',
})}
</Text>
</Stack>
)}
</Stack>
</Box>
const onClickReportConcern = (pluginId: string) => {
setReportAbuseModalOpen(true);
reportInteraction('plugin_detail_report_concern', {
plugin_id: pluginId,
});
};
{plugin?.details?.links && plugin.details?.links?.length > 0 && (
return (
<>
<Stack direction="column" gap={3} shrink={0} grow={0} width={width} data-testid="plugin-details-panel">
<Box padding={2} borderColor="medium" borderStyle="solid">
<Stack direction="column" gap={2}>
<Text color="secondary">
<Trans i18nKey="plugins.details.labels.links">Links </Trans>
</Text>
{plugin.details.links.map((link, index) => (
<TextLink key={index} href={link.url} external>
{link.name}
</TextLink>
))}
{pluginExtentionsInfo.map((infoItem, index) => {
return (
<Stack key={index} wrap direction="column" gap={0.5}>
<Text color="secondary">{infoItem.label + ':'}</Text>
<div className={styles.pluginVersionDetails}>{infoItem.value}</div>
</Stack>
);
})}
{plugin.updatedAt && (
<Stack direction="column" gap={0.5}>
<Text color="secondary">
<Trans i18nKey="plugins.details.labels.updatedAt">Last updated:</Trans>
</Text>{' '}
<Text>
{formatDate(new Date(plugin.updatedAt), { day: 'numeric', month: 'short', year: 'numeric' })}
</Text>
</Stack>
)}
{plugin?.details?.lastCommitDate && (
<Stack direction="column" gap={0.5}>
<Text color="secondary">
<Trans i18nKey="plugins.details.labels.lastCommitDate">Last commit date:</Trans>
</Text>{' '}
<Text>
{formatDate(new Date(plugin.details.lastCommitDate), {
day: 'numeric',
month: 'short',
year: 'numeric',
})}
</Text>
</Stack>
)}
</Stack>
</Box>
)}
{!plugin?.isCore && (
<Box padding={2} borderColor="medium" borderStyle="solid">
<Stack direction="column">
<Text color="secondary">
<Trans i18nKey="plugins.details.labels.reportAbuse">Report a concern </Trans>
{shouldRenderLinks && (
<>
<Box padding={2} borderColor="medium" borderStyle="solid">
<Stack direction="column" gap={2}>
{plugin.url && (
<LinkButton href={plugin.url} variant="secondary" fill="solid" icon="code-branch" target="_blank">
<Trans i18nKey="plugins.details.labels.repository">Repository</Trans>
</LinkButton>
)}
{plugin.raiseAnIssueUrl && (
<LinkButton href={plugin.raiseAnIssueUrl} variant="secondary" fill="solid" icon="bug" target="_blank">
<Trans i18nKey="plugins.details.labels.raiseAnIssue">Raise an issue</Trans>
</LinkButton>
)}
{plugin.details?.licenseUrl && (
<LinkButton
href={plugin.details?.licenseUrl}
variant="secondary"
fill="solid"
icon={'document-info'}
target="_blank"
>
<Trans i18nKey="plugins.details.labels.license">License</Trans>
</LinkButton>
)}
{plugin.details?.documentationUrl && (
<LinkButton
href={plugin.details?.documentationUrl}
variant="secondary"
fill="solid"
icon={'list-ui-alt'}
target="_blank"
>
<Trans i18nKey="plugins.details.labels.documentation">Documentation</Trans>
</LinkButton>
)}
</Stack>
</Box>
</>
)}
{customLinks && customLinks?.length > 0 && (
<Box padding={2} borderColor="medium" borderStyle="solid">
<CollapsableSection
isOpen={true}
label={
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Text color="secondary" variant="body">
<Trans i18nKey="plugins.details.labels.customLinks">Custom links </Trans>
</Text>
<Tooltip
content={
<Trans i18nKey="plugins.details.labels.customLinksTooltip">
These links are provided by the plugin developer to offer additional, developer-specific
resources and information
</Trans>
}
placement="right-end"
>
<Icon name="info-circle" size="xs" />
</Tooltip>
</Stack>
}
>
<Stack direction="column" gap={2}>
{customLinks.map((link, index) => (
<TextLink key={index} href={link.url} external>
{link.name}
</TextLink>
))}
</Stack>
</CollapsableSection>
</Box>
)}
{!plugin?.isCore && (
<Box padding={2} borderColor="medium" borderStyle="solid">
<CollapsableSection
headerDataTestId="reportConcern"
isOpen={false}
label={
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Text color="secondary" variant="body">
<Trans i18nKey="plugins.details.labels.reportAbuse">Report a concern </Trans>
</Text>
<Tooltip
content={
<Trans i18nKey="plugins.details.labels.reportAbuseTooltip">
Report issues related to malicious or harmful plugins directly to Grafana Labs.
</Trans>
}
placement="right-end"
>
<Icon name="info-circle" size="xs" />
</Tooltip>
</Stack>
}
>
<Stack direction="column">
<Button variant="secondary" fill="solid" icon="bell" onClick={() => onClickReportConcern(plugin.id)}>
<Trans i18nKey="plugins.details.labels.contactGrafanaLabs">Contact Grafana Labs</Trans>
</Button>
</Stack>
</CollapsableSection>
</Box>
)}
</Stack>
{reportAbuseModalOpen && (
<Modal
title={<Trans i18nKey="plugins.details.modal.title">Report a plugin concern</Trans>}
isOpen
onDismiss={() => setReportAbuseModalOpen(false)}
>
<Stack direction="column" gap={2}>
<Text>
<Trans i18nKey="plugins.details.modal.description">
This feature is for reporting malicious or harmful behaviour within plugins. For plugin concerns, email
us at:{' '}
</Trans>
<TextLink href="mailto:integrations+report-plugin@grafana.com">integrations@grafana.com</TextLink>
</Text>
<Text>
<Trans i18nKey="plugins.details.modal.node">
Note: For general plugin issues like bugs or feature requests, please contact the plugin author using
the provided links.{' '}
</Trans>
</Text>
<LinkButton href="mailto:integrations@grafana.com" variant="secondary" fill="solid">
<Trans i18nKey="plugins.details.labels.contactGrafanaLabs">Contact Grafana Labs</Trans>
</LinkButton>
</Stack>
</Box>
<Modal.ButtonRow>
<Button variant="secondary" fill="outline" onClick={() => setReportAbuseModalOpen(false)}>
<Trans i18nKey="plugins.details.modal.cancel">Cancel</Trans>
</Button>
<Button icon="copy" onClick={() => navigator.clipboard.writeText('integrations@grafana.com')}>
<Trans i18nKey="plugins.details.modal.copyEmail">Copy email address</Trans>
</Button>
</Modal.ButtonRow>
</Modal>
)}
</Stack>
</>
);
}

@ -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',
});
});

@ -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,
};
}

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

@ -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": {

@ -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": {

Loading…
Cancel
Save