Files
grafana/public/app/features/plugins/admin/components/PluginDetailsPanel.tsx
Yulia Shanyrova 4e8802afff Plugins: Add repositoryUrl parameter to plugin details panel. (#107048)
add repositoryUrl parameter to plugin details
2025-06-26 08:59:46 +02:00

280 lines
11 KiB
TypeScript

import { css } from '@emotion/css';
import { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
import { PageInfoItem } from '@grafana/runtime/internal';
import {
Stack,
Text,
LinkButton,
Box,
TextLink,
CollapsableSection,
Tooltip,
Icon,
Modal,
Button,
useStyles2,
} from '@grafana/ui';
import { formatDate } from 'app/core/internationalization/dates';
import { CatalogPlugin } from '../types';
type Props = {
pluginExtentionsInfo: PageInfoItem[];
plugin: CatalogPlugin;
width?: string;
};
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.details?.repositoryUrl,
plugin.details?.licenseUrl,
plugin.details?.documentationUrl,
plugin.details?.raiseAnIssueUrl,
plugin.details?.sponsorshipUrl,
]
.map(normalizeURL)
.includes(normalizeURL(link.url));
return customLinksFiltered;
});
const shouldRenderLinks =
plugin.details?.repositoryUrl ||
plugin.details?.licenseUrl ||
plugin.details?.documentationUrl ||
plugin.details?.raiseAnIssueUrl ||
plugin.details?.sponsorshipUrl;
const styles = useStyles2(getStyles);
const onClickReportConcern = (pluginId: string) => {
setReportAbuseModalOpen(true);
reportInteraction('plugin_detail_report_concern', {
plugin_id: pluginId,
});
};
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}>
{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.latestReleaseDate">Latest release date:</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>
{shouldRenderLinks && (
<>
<Box padding={2} borderColor="medium" borderStyle="solid" data-testid="plugin-details-regular-links">
<Stack direction="column" gap={2}>
{plugin.details?.repositoryUrl && (
<LinkButton
href={plugin.details?.repositoryUrl}
variant="secondary"
fill="solid"
icon="code-branch"
target="_blank"
data-testid="plugin-details-repository-link"
>
<Trans i18nKey="plugins.details.labels.repository">Repository</Trans>
</LinkButton>
)}
{plugin.details?.raiseAnIssueUrl && (
<LinkButton
href={plugin.details?.raiseAnIssueUrl}
variant="secondary"
fill="solid"
icon="bug"
target="_blank"
data-testid="plugin-details-raise-issue-link"
>
<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"
data-testid="plugin-details-license-link"
>
<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"
data-testid="plugin-details-documentation-link"
>
<Trans i18nKey="plugins.details.labels.documentation">Documentation</Trans>
</LinkButton>
)}
{plugin.details?.sponsorshipUrl && (
<LinkButton
href={plugin.details?.sponsorshipUrl}
variant="secondary"
fill="solid"
icon={'heart'}
target="_blank"
data-testid="plugin-details-sponsorship-link"
>
<Trans i18nKey="plugins.details.labels.sponsorDeveloper">Sponsor this developer</Trans>
</LinkButton>
)}
</Stack>
</Box>
</>
)}
{customLinks && customLinks?.length > 0 && (
<Box padding={2} borderColor="medium" borderStyle="solid" data-testid="plugin-details-custom-links">
<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>
{/* eslint-disable-next-line @grafana/i18n/no-untranslated-strings */}
<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>
</Stack>
<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>
)}
</>
);
}
export const getStyles = (theme: GrafanaTheme2) => {
return {
pluginVersionDetails: css({
wordBreak: 'break-word',
}),
};
};