mirror of
https://github.com/grafana/grafana.git
synced 2025-12-18 22:16:21 -05:00
This reverts commit 1f4f2b4d7c.
This commit is contained in:
parent
1862e5dac5
commit
051cdaad0d
19 changed files with 16 additions and 566 deletions
|
|
@ -2868,6 +2868,11 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/admin/components/PluginDetailsPage.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/plugins/admin/helpers.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 2
|
||||
|
|
|
|||
|
|
@ -1189,11 +1189,6 @@ export interface FeatureToggles {
|
|||
*/
|
||||
onlyStoreActionSets?: boolean;
|
||||
/**
|
||||
* Show insights for plugins in the plugin details page
|
||||
* @default false
|
||||
*/
|
||||
pluginInsights?: boolean;
|
||||
/**
|
||||
* Enables a new panel time settings drawer
|
||||
*/
|
||||
panelTimeSettings?: boolean;
|
||||
|
|
|
|||
|
|
@ -1960,14 +1960,6 @@ var (
|
|||
Owner: identityAccessTeam,
|
||||
Expression: "true",
|
||||
},
|
||||
{
|
||||
Name: "pluginInsights",
|
||||
Description: "Show insights for plugins in the plugin details page",
|
||||
Stage: FeatureStageExperimental,
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaPluginsPlatformSquad,
|
||||
Expression: "false",
|
||||
},
|
||||
{
|
||||
Name: "panelTimeSettings",
|
||||
Description: "Enables a new panel time settings drawer",
|
||||
|
|
|
|||
1
pkg/services/featuremgmt/toggles_gen.csv
generated
1
pkg/services/featuremgmt/toggles_gen.csv
generated
|
|
@ -266,7 +266,6 @@ jaegerEnableGrpcEndpoint,experimental,@grafana/oss-big-tent,false,false,false
|
|||
pluginStoreServiceLoading,experimental,@grafana/plugins-platform-backend,false,false,false
|
||||
newPanelPadding,preview,@grafana/dashboards-squad,false,false,true
|
||||
onlyStoreActionSets,GA,@grafana/identity-access-team,false,false,false
|
||||
pluginInsights,experimental,@grafana/plugins-platform-backend,false,false,true
|
||||
panelTimeSettings,experimental,@grafana/dashboards-squad,false,false,false
|
||||
elasticsearchRawDSLQuery,experimental,@grafana/partner-datasources,false,false,false
|
||||
kubernetesAnnotations,experimental,@grafana/grafana-backend-services-squad,false,false,false
|
||||
|
|
|
|||
|
14
pkg/services/featuremgmt/toggles_gen.json
generated
14
pkg/services/featuremgmt/toggles_gen.json
generated
|
|
@ -2693,20 +2693,6 @@
|
|||
"expression": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "pluginInsights",
|
||||
"resourceVersion": "1761300628147",
|
||||
"creationTimestamp": "2025-10-24T10:10:28Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Show insights for plugins in the plugin details page",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/plugins-platform-backend",
|
||||
"frontend": true,
|
||||
"expression": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "pluginInstallAPISync",
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import {
|
|||
LocalPlugin,
|
||||
RemotePlugin,
|
||||
CatalogPluginDetails,
|
||||
CatalogPluginInsights,
|
||||
Version,
|
||||
PluginVersion,
|
||||
InstancePlugin,
|
||||
|
|
@ -48,21 +47,6 @@ export async function getPluginDetails(id: string): Promise<CatalogPluginDetails
|
|||
};
|
||||
}
|
||||
|
||||
export async function getPluginInsights(id: string, version: string | undefined): Promise<CatalogPluginInsights> {
|
||||
if (!version) {
|
||||
throw new Error('Version is required');
|
||||
}
|
||||
try {
|
||||
const insights = await getBackendSrv().get(`${GCOM_API_ROOT}/plugins/${id}/versions/${version}/insights`);
|
||||
return insights;
|
||||
} catch (error) {
|
||||
if (isFetchError(error)) {
|
||||
error.isHandled = true;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRemotePlugins(): Promise<RemotePlugin[]> {
|
||||
try {
|
||||
const { items: remotePlugins }: { items: RemotePlugin[] } = await getBackendSrv().get(`${GCOM_API_ROOT}/plugins`, {
|
||||
|
|
|
|||
|
|
@ -62,12 +62,10 @@ const plugin: CatalogPlugin = {
|
|||
angularDetected: false,
|
||||
isFullyInstalled: true,
|
||||
accessControl: {},
|
||||
insights: { id: 1, name: 'test-plugin', version: '1.0.0', insights: [] },
|
||||
};
|
||||
|
||||
jest.mock('../state/hooks', () => ({
|
||||
useGetSingle: jest.fn(),
|
||||
useGetPluginInsights: jest.fn(),
|
||||
useFetchStatus: jest.fn().mockReturnValue({ isLoading: false }),
|
||||
useFetchDetailsStatus: () => ({ isLoading: false }),
|
||||
useIsRemotePluginsAvailable: () => false,
|
||||
|
|
|
|||
|
|
@ -16,19 +16,11 @@ import { PluginDetailsPanel } from '../components/PluginDetailsPanel';
|
|||
import { PluginDetailsSignature } from '../components/PluginDetailsSignature';
|
||||
import { usePluginDetailsTabs } from '../hooks/usePluginDetailsTabs';
|
||||
import { usePluginPageExtensions } from '../hooks/usePluginPageExtensions';
|
||||
import { useGetSingle, useFetchStatus, useFetchDetailsStatus, useGetPluginInsights } from '../state/hooks';
|
||||
import { useGetSingle, useFetchStatus, useFetchDetailsStatus } from '../state/hooks';
|
||||
import { PluginTabIds } from '../types';
|
||||
|
||||
import { PluginDetailsDeprecatedWarning } from './PluginDetailsDeprecatedWarning';
|
||||
|
||||
function isPluginTabId(value: string | null): value is PluginTabIds {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
const validIds: string[] = Object.values(PluginTabIds);
|
||||
return validIds.includes(value);
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
// The ID of the plugin
|
||||
pluginId: string;
|
||||
|
|
@ -57,13 +49,12 @@ export function PluginDetailsPage({
|
|||
};
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
const plugin = useGetSingle(pluginId); // fetches the plugin settings for this Grafana instance
|
||||
useGetPluginInsights(pluginId, plugin?.isInstalled ? plugin?.installedVersion : plugin?.latestVersion);
|
||||
|
||||
const isNarrowScreen = useMedia('(max-width: 600px)');
|
||||
const pageParam = queryParams.get('page');
|
||||
const pageId = pageParam && isPluginTabId(pageParam) ? pageParam : undefined;
|
||||
const { navModel, activePageId } = usePluginDetailsTabs(plugin, pageId, isNarrowScreen);
|
||||
|
||||
const { navModel, activePageId } = usePluginDetailsTabs(
|
||||
plugin,
|
||||
queryParams.get('page') as PluginTabIds,
|
||||
isNarrowScreen
|
||||
);
|
||||
const { actions, info, subtitle } = usePluginPageExtensions(plugin);
|
||||
const { isLoading: isFetchLoading } = useFetchStatus();
|
||||
const { isLoading: isFetchDetailsLoading } = useFetchDetailsStatus();
|
||||
|
|
|
|||
|
|
@ -1,23 +1,11 @@
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import { render, screen } from 'test/test-utils';
|
||||
|
||||
import { PluginSignatureStatus, PluginSignatureType, PluginType } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { CatalogPlugin, SCORE_LEVELS } from '../types';
|
||||
import { CatalogPlugin } from '../types';
|
||||
|
||||
import { PluginDetailsPanel } from './PluginDetailsPanel';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
config: {
|
||||
...jest.requireActual('@grafana/runtime').config,
|
||||
featureToggles: {
|
||||
pluginInsights: false,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const mockPlugin: CatalogPlugin = {
|
||||
description: 'Test plugin description',
|
||||
downloads: 1000,
|
||||
|
|
@ -197,61 +185,4 @@ describe('PluginDetailsPanel', () => {
|
|||
expect(regularLinks).toContainElement(raiseIssueLink);
|
||||
expect(regularLinks).not.toContainElement(websiteLink);
|
||||
});
|
||||
|
||||
it('should render plugin insights when plugin has insights', async () => {
|
||||
config.featureToggles.pluginInsights = true;
|
||||
const pluginWithInsights = {
|
||||
...mockPlugin,
|
||||
insights: {
|
||||
id: 1,
|
||||
name: 'test-plugin',
|
||||
version: '1.0.0',
|
||||
insights: [
|
||||
{
|
||||
name: 'security',
|
||||
scoreValue: 90,
|
||||
scoreLevel: SCORE_LEVELS.EXCELLENT,
|
||||
items: [
|
||||
{
|
||||
id: 'signature',
|
||||
name: 'Signature verified',
|
||||
level: 'ok' as const,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
render(<PluginDetailsPanel plugin={pluginWithInsights} pluginExtentionsInfo={mockInfo} />);
|
||||
expect(screen.getByTestId('plugin-insights-container')).toBeInTheDocument();
|
||||
expect(screen.getByText('Plugin insights')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Security')).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByText('Security'));
|
||||
expect(screen.getByTestId('plugin-insight-item-signature')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render plugin insights when plugin has no insights', () => {
|
||||
const pluginWithoutInsights = {
|
||||
...mockPlugin,
|
||||
insights: undefined,
|
||||
};
|
||||
render(<PluginDetailsPanel plugin={pluginWithoutInsights} pluginExtentionsInfo={mockInfo} />);
|
||||
expect(screen.queryByTestId('plugin-insights-container')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Plugin insights')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render plugin insights when insights array is empty', () => {
|
||||
const pluginWithEmptyInsights = {
|
||||
...mockPlugin,
|
||||
insights: {
|
||||
id: 1,
|
||||
name: 'test-plugin',
|
||||
version: '1.0.0',
|
||||
insights: [],
|
||||
},
|
||||
};
|
||||
render(<PluginDetailsPanel plugin={pluginWithEmptyInsights} pluginExtentionsInfo={mockInfo} />);
|
||||
expect(screen.queryByTestId('plugin-insights-container')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Plugin insights')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { useState } from 'react';
|
|||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { PageInfoItem } from '@grafana/runtime/internal';
|
||||
import {
|
||||
Stack,
|
||||
|
|
@ -22,8 +22,6 @@ import { formatDate } from 'app/core/internationalization/dates';
|
|||
|
||||
import { CatalogPlugin } from '../types';
|
||||
|
||||
import { PluginInsights } from './PluginInsights';
|
||||
|
||||
type Props = { pluginExtentionsInfo: PageInfoItem[]; plugin: CatalogPlugin; width?: string };
|
||||
|
||||
export function PluginDetailsPanel(props: Props): React.ReactElement | null {
|
||||
|
|
@ -71,11 +69,6 @@ export function PluginDetailsPanel(props: Props): React.ReactElement | null {
|
|||
return (
|
||||
<>
|
||||
<Stack direction="column" gap={3} shrink={0} grow={0} width={width} data-testid="plugin-details-panel">
|
||||
{config.featureToggles.pluginInsights && plugin.insights && plugin.insights?.insights?.length > 0 && (
|
||||
<Box borderRadius="lg" padding={2} borderColor="medium" borderStyle="solid">
|
||||
<PluginInsights pluginInsights={plugin.insights} />
|
||||
</Box>
|
||||
)}
|
||||
<Box borderRadius="lg" padding={2} borderColor="medium" borderStyle="solid">
|
||||
<Stack direction="column" gap={2}>
|
||||
{pluginExtentionsInfo.map((infoItem, index) => {
|
||||
|
|
|
|||
|
|
@ -1,171 +0,0 @@
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import { render, screen } from 'test/test-utils';
|
||||
|
||||
import { CatalogPluginInsights, InsightLevel, SCORE_LEVELS } from '../types';
|
||||
|
||||
import { PluginInsights } from './PluginInsights';
|
||||
|
||||
const mockPluginInsights: CatalogPluginInsights = {
|
||||
id: 1,
|
||||
name: 'test-plugin',
|
||||
version: '1.0.0',
|
||||
insights: [
|
||||
{
|
||||
name: 'security',
|
||||
scoreValue: 90,
|
||||
scoreLevel: SCORE_LEVELS.EXCELLENT,
|
||||
items: [
|
||||
{
|
||||
id: 'signature',
|
||||
name: 'Signature verified',
|
||||
description: 'Plugin signature is valid',
|
||||
level: 'ok' as InsightLevel,
|
||||
},
|
||||
{
|
||||
id: 'trackingscripts',
|
||||
name: 'No unsafe JavaScript detected',
|
||||
level: 'good' as InsightLevel,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
scoreValue: 60,
|
||||
scoreLevel: SCORE_LEVELS.FAIR,
|
||||
items: [
|
||||
{
|
||||
id: 'metadatavalid',
|
||||
name: 'Metadata is valid',
|
||||
level: 'ok' as InsightLevel,
|
||||
},
|
||||
{
|
||||
id: 'code-rules',
|
||||
name: 'Missing code rules',
|
||||
description: 'Plugin lacks comprehensive code rules',
|
||||
level: 'warning' as InsightLevel,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mockPluginInsightsWithPoorLevel: CatalogPluginInsights = {
|
||||
id: 3,
|
||||
name: 'test-plugin-poor',
|
||||
version: '0.8.0',
|
||||
insights: [
|
||||
{
|
||||
name: 'quality',
|
||||
scoreValue: 35,
|
||||
scoreLevel: SCORE_LEVELS.POOR,
|
||||
items: [
|
||||
{
|
||||
id: 'legacy-platform',
|
||||
name: 'Quality issues detected',
|
||||
level: 'warning' as InsightLevel,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('PluginInsights', () => {
|
||||
it('should render plugin insights section', () => {
|
||||
render(<PluginInsights pluginInsights={mockPluginInsights} />);
|
||||
const insightsSection = screen.getByTestId('plugin-insights-container');
|
||||
expect(insightsSection).toBeInTheDocument();
|
||||
expect(screen.getByText('Plugin insights')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render all insight categories with test ids', () => {
|
||||
render(<PluginInsights pluginInsights={mockPluginInsights} />);
|
||||
expect(screen.getByTestId('plugin-insight-security')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('plugin-insight-quality')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render category names with test ids', () => {
|
||||
render(<PluginInsights pluginInsights={mockPluginInsights} />);
|
||||
const securityCategory = screen.getByTestId('plugin-insight-security');
|
||||
const qualityCategory = screen.getByTestId('plugin-insight-quality');
|
||||
|
||||
expect(securityCategory).toBeInTheDocument();
|
||||
expect(securityCategory).toHaveTextContent('Security');
|
||||
expect(qualityCategory).toBeInTheDocument();
|
||||
expect(qualityCategory).toHaveTextContent('Quality');
|
||||
});
|
||||
|
||||
it('should render individual insight items with test ids', async () => {
|
||||
render(<PluginInsights pluginInsights={mockPluginInsights} />);
|
||||
await userEvent.click(screen.getByText('Security'));
|
||||
expect(screen.getByTestId('plugin-insight-item-signature')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('plugin-insight-item-trackingscripts')).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByText('Quality'));
|
||||
expect(screen.getByTestId('plugin-insight-item-metadatavalid')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('plugin-insight-item-code-rules')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display correct icons for Excellent score level', () => {
|
||||
render(<PluginInsights pluginInsights={mockPluginInsights} />);
|
||||
|
||||
const securityCategory = screen.getByTestId('plugin-insight-security');
|
||||
const securityIcon = securityCategory.querySelector('[data-testid="excellent-icon"]');
|
||||
expect(securityIcon).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display correct icons for Poor score levels', () => {
|
||||
// Test Poor level - should show exclamation-triangle
|
||||
render(<PluginInsights pluginInsights={mockPluginInsightsWithPoorLevel} />);
|
||||
const poorCategory = screen.getByTestId('plugin-insight-quality');
|
||||
const poorIcon = poorCategory.querySelector('[data-testid="poor-icon"]');
|
||||
expect(poorIcon).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle multiple items with different insight levels', async () => {
|
||||
const multiLevelInsights: CatalogPluginInsights = {
|
||||
id: 5,
|
||||
name: 'multi-level-plugin',
|
||||
version: '2.0.0',
|
||||
insights: [
|
||||
{
|
||||
name: 'quality',
|
||||
scoreValue: 75,
|
||||
scoreLevel: SCORE_LEVELS.GOOD,
|
||||
items: [
|
||||
{
|
||||
id: 'code-rules',
|
||||
name: 'Info level item',
|
||||
level: 'info' as InsightLevel,
|
||||
},
|
||||
{
|
||||
id: 'sdk-usage',
|
||||
name: 'OK level item',
|
||||
level: 'ok' as InsightLevel,
|
||||
},
|
||||
{
|
||||
id: 'jsMap',
|
||||
name: 'Good level item',
|
||||
level: 'good' as InsightLevel,
|
||||
},
|
||||
{
|
||||
id: 'gosec',
|
||||
name: 'Warning level item',
|
||||
level: 'warning' as InsightLevel,
|
||||
},
|
||||
{
|
||||
id: 'legacy-builder',
|
||||
name: 'Danger level item',
|
||||
level: 'danger' as InsightLevel,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
render(<PluginInsights pluginInsights={multiLevelInsights} />);
|
||||
await userEvent.click(screen.getByText('Quality'));
|
||||
expect(screen.getByText('Info level item')).toBeInTheDocument();
|
||||
expect(screen.getByText('OK level item')).toBeInTheDocument();
|
||||
expect(screen.getByText('Good level item')).toBeInTheDocument();
|
||||
expect(screen.getByText('Warning level item')).toBeInTheDocument();
|
||||
expect(screen.getByText('Danger level item')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
import { css } from '@emotion/css';
|
||||
import { capitalize } from 'lodash';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { Stack, Text, TextLink, CollapsableSection, Tooltip, Icon, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { CatalogPluginInsights } from '../types';
|
||||
|
||||
type Props = { pluginInsights: CatalogPluginInsights | undefined };
|
||||
|
||||
const PLUGINS_INSIGHTS_OPENED_EVENT_NAME = 'plugins_insights_opened';
|
||||
|
||||
export function PluginInsights(props: Props): React.ReactElement | null {
|
||||
const { pluginInsights } = props;
|
||||
const styles = useStyles2(getStyles);
|
||||
const theme = useTheme2();
|
||||
const [openInsights, setOpenInsights] = useState<Record<string, boolean>>({});
|
||||
|
||||
const handleInsightToggle = (insightName: string, isOpen: boolean) => {
|
||||
if (isOpen) {
|
||||
reportInteraction(PLUGINS_INSIGHTS_OPENED_EVENT_NAME, { insight: insightName });
|
||||
}
|
||||
setOpenInsights((prev) => ({ ...prev, [insightName]: isOpen }));
|
||||
};
|
||||
|
||||
const tooltipInfo = (
|
||||
<Stack direction="column" gap={0.5}>
|
||||
<Stack direction="row" alignItems="center">
|
||||
<Icon name="check-circle" size="md" color={theme.colors.success.main} />
|
||||
<Text color="primary" variant="body">
|
||||
<Trans i18nKey="plugins.details.labels.pluginInsightsSuccessTooltip">
|
||||
All relevant signals are present and verified
|
||||
</Trans>
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack direction="row" alignItems="center">
|
||||
<Icon name="exclamation-triangle" size="md" />
|
||||
<Text color="primary" variant="body">
|
||||
<Trans i18nKey="plugins.details.labels.pluginInsightsWarningTooltip">
|
||||
One or more signals are missing or need attention
|
||||
</Trans>
|
||||
</Text>
|
||||
</Stack>
|
||||
<hr className={styles.pluginInsightsTooltipSeparator} />
|
||||
<Text color="secondary" variant="body">
|
||||
<Trans i18nKey="plugins.details.labels.moreDetails">
|
||||
Do you find Plugin Insights usefull? Please share your feedback{' '}
|
||||
<TextLink href="https://forms.gle/1ZVLbecyQ8aY9mDYA" external>
|
||||
here
|
||||
</TextLink>
|
||||
.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="column" gap={0.5} shrink={0} grow={0} data-testid="plugin-insights-container">
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Text color="secondary" variant="h6" data-testid="plugin-insights-header">
|
||||
<Trans i18nKey="plugins.details.labels.pluginInsights.header">Plugin insights</Trans>
|
||||
</Text>
|
||||
<Tooltip content={tooltipInfo} placement="right-end" interactive>
|
||||
<Icon name="info-circle" size="md" />
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
{pluginInsights?.insights.map((insightItem, index) => {
|
||||
return (
|
||||
<Stack key={index} wrap direction="column" gap={1}>
|
||||
<CollapsableSection
|
||||
isOpen={openInsights[insightItem.name] ?? false}
|
||||
onToggle={(isOpen) => handleInsightToggle(insightItem.name, isOpen)}
|
||||
label={
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={1}
|
||||
alignItems="center"
|
||||
data-testid={`plugin-insight-${insightItem.name.toLowerCase()}`}
|
||||
>
|
||||
{insightItem.scoreLevel === 'Excellent' ? (
|
||||
<Icon
|
||||
name="check-circle"
|
||||
size="lg"
|
||||
color={theme.colors.success.main}
|
||||
data-testid="excellent-icon"
|
||||
/>
|
||||
) : (
|
||||
<Icon name="exclamation-triangle" size="lg" data-testid="poor-icon" />
|
||||
)}
|
||||
<Text
|
||||
color="primary"
|
||||
variant="body"
|
||||
data-testid={`plugin-insight-color-${insightItem.name.toLowerCase()}`}
|
||||
>
|
||||
{capitalize(insightItem.name)}
|
||||
</Text>
|
||||
</Stack>
|
||||
}
|
||||
contentClassName={styles.pluginInsightsItems}
|
||||
>
|
||||
<Stack direction="column" gap={1}>
|
||||
{insightItem.items.map((item, idx) => (
|
||||
<Stack key={idx} direction="row" gap={1} alignItems="flex-start">
|
||||
<span>
|
||||
{item.level === 'good' ? (
|
||||
<Icon name="check-circle" size="sm" color={theme.colors.success.main} />
|
||||
) : (
|
||||
<Icon name="exclamation-triangle" size="sm" />
|
||||
)}
|
||||
</span>
|
||||
<Text color="secondary" variant="body" data-testid={`plugin-insight-item-${item.id}`}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
</CollapsableSection>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
pluginVersionDetails: css({ wordBreak: 'break-word' }),
|
||||
pluginInsightsItems: css({ marginLeft: '26px', paddingTop: '0 !important' }),
|
||||
pluginInsightsTooltipSeparator: css({
|
||||
border: 'none',
|
||||
borderTop: `1px solid ${theme.colors.border.medium}`,
|
||||
margin: `${theme.spacing(1)} 0`,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
|
@ -34,7 +34,6 @@ export default {
|
|||
updatedAt: '2021-08-25T15:03:49.000Z',
|
||||
version: '4.2.2',
|
||||
error: undefined,
|
||||
insights: { id: 1, name: 'alexanderzobnin-zabbix-app', version: '4.2.2', insights: [] },
|
||||
details: {
|
||||
grafanaDependency: '>=8.0.0',
|
||||
pluginDependencies: [],
|
||||
|
|
@ -382,7 +381,6 @@ export const datasourcePlugin = {
|
|||
angularDetected: false,
|
||||
isFullyInstalled: true,
|
||||
latestVersion: '1.20.0',
|
||||
insights: { id: 2, name: 'grafana-redshift-datasource', version: '1.20.0', insights: [] },
|
||||
details: {
|
||||
grafanaDependency: '>=8.0.0',
|
||||
pluginDependencies: [],
|
||||
|
|
|
|||
|
|
@ -31,9 +31,6 @@ export const getPluginsStateMock = (plugins: CatalogPlugin[] = []): ReducerState
|
|||
'plugins/fetchDetails': {
|
||||
status: RequestStatus.Fulfilled,
|
||||
},
|
||||
'plugins/fetchPluginInsights': {
|
||||
status: RequestStatus.Fulfilled,
|
||||
},
|
||||
},
|
||||
// Backward compatibility
|
||||
plugins: [],
|
||||
|
|
@ -78,11 +75,6 @@ export const mockPluginApis = ({
|
|||
return Promise.resolve({ items: versions });
|
||||
}
|
||||
|
||||
// Mock plugin insights - return empty insights to avoid API call errors
|
||||
if (path.includes('/insights')) {
|
||||
return Promise.resolve({ id: 1, name: '', version: '', insights: [] });
|
||||
}
|
||||
|
||||
// Mock local plugin settings (installed) if necessary
|
||||
if (local && path === `${API_ROOT}/${local.id}/settings`) {
|
||||
return Promise.resolve(local);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import {
|
|||
getPluginErrors,
|
||||
getLocalPlugins,
|
||||
getPluginDetails,
|
||||
getPluginInsights,
|
||||
installPlugin,
|
||||
uninstallPlugin,
|
||||
getInstancePlugins,
|
||||
|
|
@ -166,22 +165,6 @@ export const fetchDetails = createAsyncThunk<Update<CatalogPlugin, string>, stri
|
|||
}
|
||||
);
|
||||
|
||||
export const fetchPluginInsights = createAsyncThunk<Update<CatalogPlugin, string>, { id: string; version?: string }>(
|
||||
`${STATE_PREFIX}/fetchPluginInsights`,
|
||||
async ({ id, version }, thunkApi) => {
|
||||
try {
|
||||
const insights = await getPluginInsights(id, version);
|
||||
|
||||
return {
|
||||
id,
|
||||
changes: { insights },
|
||||
};
|
||||
} catch (e) {
|
||||
return thunkApi.rejectWithValue('Unknown error.');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const addPlugins = createAction<CatalogPlugin[]>(`${STATE_PREFIX}/addPlugins`);
|
||||
|
||||
// 1. gets remote equivalents from the store (if there are any)
|
||||
|
|
@ -282,8 +265,7 @@ export const panelPluginLoaded = createAction<PanelPlugin>(`${STATE_PREFIX}/pane
|
|||
// TODO<remove once the "plugin_admin_enabled" feature flag is removed>
|
||||
export const loadPanelPlugin = (id: string): ThunkResult<Promise<PanelPlugin>> => {
|
||||
return async (dispatch, getStore) => {
|
||||
const state = getStore();
|
||||
let plugin = state.plugins.panels[id];
|
||||
let plugin = getStore().plugins.panels[id];
|
||||
|
||||
if (!plugin) {
|
||||
plugin = await importPanelPlugin(id);
|
||||
|
|
|
|||
|
|
@ -6,16 +6,7 @@ import { useDispatch, useSelector } from 'app/types/store';
|
|||
import { sortPlugins, Sorters, isPluginUpdatable } from '../helpers';
|
||||
import { CatalogPlugin, PluginStatus } from '../types';
|
||||
|
||||
import {
|
||||
fetchAll,
|
||||
fetchDetails,
|
||||
fetchRemotePlugins,
|
||||
install,
|
||||
uninstall,
|
||||
fetchAllLocal,
|
||||
unsetInstall,
|
||||
fetchPluginInsights,
|
||||
} from './actions';
|
||||
import { fetchAll, fetchDetails, fetchRemotePlugins, install, uninstall, fetchAllLocal, unsetInstall } from './actions';
|
||||
import {
|
||||
selectPlugins,
|
||||
selectById,
|
||||
|
|
@ -53,18 +44,13 @@ export const useGetUpdatable = () => {
|
|||
};
|
||||
};
|
||||
|
||||
export const useGetSingle = (id: string, version?: string): CatalogPlugin | undefined => {
|
||||
export const useGetSingle = (id: string): CatalogPlugin | undefined => {
|
||||
useFetchAll();
|
||||
useFetchDetails(id);
|
||||
|
||||
return useSelector((state) => selectById(state, id));
|
||||
};
|
||||
|
||||
export const useGetPluginInsights = (id: string, version: string | undefined): CatalogPlugin | undefined => {
|
||||
useFetchPluginInsights(id, version);
|
||||
return useSelector((state) => selectById(state, id));
|
||||
};
|
||||
|
||||
export const useGetSingleLocalWithoutDetails = (id: string): CatalogPlugin | undefined => {
|
||||
useFetchAllLocal();
|
||||
return useSelector((state) => selectById(state, id));
|
||||
|
|
@ -167,17 +153,6 @@ export const useFetchDetails = (id: string) => {
|
|||
}, [plugin]); // eslint-disable-line
|
||||
};
|
||||
|
||||
export const useFetchPluginInsights = (id: string, version: string | undefined) => {
|
||||
const dispatch = useDispatch();
|
||||
const plugin = useSelector((state) => selectById(state, id));
|
||||
const isNotFetching = !useSelector(selectIsRequestPending(fetchPluginInsights.typePrefix));
|
||||
const shouldFetch = isNotFetching && plugin && !plugin.insights && version;
|
||||
|
||||
useEffect(() => {
|
||||
shouldFetch && dispatch(fetchPluginInsights({ id, version }));
|
||||
}, [plugin, version]); // eslint-disable-line
|
||||
};
|
||||
|
||||
export const useFetchDetailsLazy = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { CatalogPlugin, ReducerState, RequestStatus } from '../types';
|
|||
|
||||
import {
|
||||
fetchDetails,
|
||||
fetchPluginInsights,
|
||||
install,
|
||||
uninstall,
|
||||
loadPluginDashboards,
|
||||
|
|
@ -64,10 +63,6 @@ const slice = createSlice({
|
|||
.addCase(fetchDetails.fulfilled, (state, action) => {
|
||||
pluginsAdapter.updateOne(state.items, action.payload);
|
||||
})
|
||||
// Fetch Plugin Insights
|
||||
.addCase(fetchPluginInsights.fulfilled, (state, action) => {
|
||||
pluginsAdapter.updateOne(state.items, action.payload);
|
||||
})
|
||||
// Install
|
||||
.addCase(install.fulfilled, (state, action) => {
|
||||
pluginsAdapter.updateOne(state.items, action.payload);
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ export interface CatalogPlugin extends WithAccessControlMetadata {
|
|||
updatedAt: string;
|
||||
installedVersion?: string;
|
||||
details?: CatalogPluginDetails;
|
||||
insights?: CatalogPluginInsights;
|
||||
error?: PluginErrorCode;
|
||||
angularDetected?: boolean;
|
||||
// instance plugins may not be fully installed, which means a new instance
|
||||
|
|
@ -91,54 +90,6 @@ export interface CatalogPluginDetails {
|
|||
screenshots?: Screenshots[] | null;
|
||||
}
|
||||
|
||||
export type InsightLevel = 'ok' | 'warning' | 'danger' | 'good' | 'info';
|
||||
|
||||
export const SCORE_LEVELS = {
|
||||
EXCELLENT: 'Excellent',
|
||||
GOOD: 'Good',
|
||||
FAIR: 'Fair',
|
||||
POOR: 'Poor',
|
||||
CRITICAL: 'Critical',
|
||||
} as const;
|
||||
|
||||
export type ScoreLevel = (typeof SCORE_LEVELS)[keyof typeof SCORE_LEVELS];
|
||||
|
||||
export const INSIGHT_CATEGORIES = {
|
||||
SECURITY: 'security',
|
||||
QUALITY: 'quality',
|
||||
PERFORMANCE: 'performance',
|
||||
} as const;
|
||||
|
||||
export const INSIGHT_LEVELS = {
|
||||
GOOD: 'good',
|
||||
OK: 'ok',
|
||||
WARNING: 'warning',
|
||||
DANGER: 'danger',
|
||||
INFO: 'info',
|
||||
} as const;
|
||||
|
||||
export interface InsightItem {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
level: InsightLevel;
|
||||
link?: string;
|
||||
}
|
||||
|
||||
export interface InsightCategory {
|
||||
name: string;
|
||||
items: InsightItem[];
|
||||
scoreValue: number;
|
||||
scoreLevel: ScoreLevel;
|
||||
}
|
||||
|
||||
export interface CatalogPluginInsights {
|
||||
id: number;
|
||||
name: string;
|
||||
version: string;
|
||||
insights: InsightCategory[];
|
||||
}
|
||||
|
||||
export interface CatalogPluginInfo {
|
||||
logos: { large: string; small: string };
|
||||
keywords: string[];
|
||||
|
|
|
|||
|
|
@ -11390,12 +11390,6 @@
|
|||
"latestReleaseDate": "Latest release date:",
|
||||
"latestVersion": "Latest Version",
|
||||
"license": "License",
|
||||
"moreDetails": "Do you find Plugin Insights usefull? Please share your feedback <2>here</2>.",
|
||||
"pluginInsights": {
|
||||
"header": "Plugin insights"
|
||||
},
|
||||
"pluginInsightsSuccessTooltip": "All relevant signals are present and verified",
|
||||
"pluginInsightsWarningTooltip": "One or more signals are missing or need attention",
|
||||
"raiseAnIssue": "Raise an issue",
|
||||
"reportAbuse": "Report a concern",
|
||||
"reportAbuseTooltip": "Report issues related to malicious or harmful plugins directly to Grafana Labs.",
|
||||
|
|
|
|||
Loading…
Reference in a new issue