diff --git a/public/app/features/plugins/admin/components/PluginList.tsx b/public/app/features/plugins/admin/components/PluginList.tsx
index 38a67e5e8a6..62e862aa7f2 100644
--- a/public/app/features/plugins/admin/components/PluginList.tsx
+++ b/public/app/features/plugins/admin/components/PluginList.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { css } from '@emotion/css';
+import { css, cx } from '@emotion/css';
import { useStyles2 } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';
import { useLocation } from 'react-router-dom';
@@ -12,26 +12,28 @@ interface Props {
}
export const PluginList = ({ plugins, displayMode }: Props) => {
- const styles = useStyles2((theme) => getStyles(theme, displayMode));
+ const isList = displayMode === PluginListDisplayMode.List;
+ const styles = useStyles2(getStyles);
const location = useLocation();
return (
-
+
{plugins.map((plugin) => (
-
+
))}
);
};
-const getStyles = (theme: GrafanaTheme2, display: PluginListDisplayMode) => {
- const isList = display === PluginListDisplayMode.List;
-
+const getStyles = (theme: GrafanaTheme2) => {
return {
container: css`
display: grid;
- grid-template-columns: ${isList ? '1fr' : 'repeat(auto-fill, minmax(288px, 1fr))'};
- grid-gap: ${theme.spacing(3)};
+ grid-template-columns: repeat(auto-fill, minmax(288px, 1fr));
+ gap: ${theme.spacing(3)};
+ `,
+ list: css`
+ grid-template-columns: 1fr;
`,
};
};
diff --git a/public/app/features/plugins/admin/components/PluginListItem.test.tsx b/public/app/features/plugins/admin/components/PluginListItem.test.tsx
new file mode 100644
index 00000000000..30ee6565c7e
--- /dev/null
+++ b/public/app/features/plugins/admin/components/PluginListItem.test.tsx
@@ -0,0 +1,118 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { PluginErrorCode, PluginSignatureStatus, PluginType } from '@grafana/data';
+import { PluginListItem } from './PluginListItem';
+import { CatalogPlugin, PluginListDisplayMode } from '../types';
+
+describe('PluginListItem', () => {
+ const plugin: CatalogPlugin = {
+ description: 'The test plugin',
+ downloads: 5,
+ id: 'test-plugin',
+ info: {
+ logos: {
+ small: 'https://grafana.com/api/plugins/test-plugin/versions/0.0.10/logos/small',
+ large: 'https://grafana.com/api/plugins/test-plugin/versions/0.0.10/logos/large',
+ },
+ },
+ name: 'Testing Plugin',
+ orgName: 'Test',
+ popularity: 0,
+ signature: PluginSignatureStatus.valid,
+ publishedAt: '2020-09-01',
+ updatedAt: '2021-06-28',
+ version: '1.0.0',
+ hasUpdate: false,
+ isInstalled: false,
+ isCore: false,
+ isDev: false,
+ isEnterprise: false,
+ isDisabled: false,
+ };
+
+ /** As Grid */
+ it('renders a card with link, image, name, orgName and badges', () => {
+ render(
);
+
+ expect(screen.getByRole('link')).toHaveAttribute('href', '/plugins/test-plugin?page=overview');
+
+ const logo = screen.getByRole('img');
+ expect(logo).toHaveAttribute('src', plugin.info.logos.small);
+
+ expect(screen.getByRole('heading', { name: /testing plugin/i })).toBeVisible();
+ expect(screen.getByText(`By ${plugin.orgName}`)).toBeVisible();
+ expect(screen.getByText(/signed/i)).toBeVisible();
+ expect(screen.queryByLabelText(/icon/i)).not.toBeInTheDocument();
+ });
+
+ it('renders a datasource plugin with correct icon', () => {
+ const datasourcePlugin = { ...plugin, type: PluginType.datasource };
+ render(
);
+
+ expect(screen.getByLabelText(/datasource plugin icon/i)).toBeVisible();
+ });
+
+ it('renders a panel plugin with correct icon', () => {
+ const panelPlugin = { ...plugin, type: PluginType.panel };
+ render(
);
+
+ expect(screen.getByLabelText(/panel plugin icon/i)).toBeVisible();
+ });
+
+ it('renders an app plugin with correct icon', () => {
+ const appPlugin = { ...plugin, type: PluginType.app };
+ render(
);
+
+ expect(screen.getByLabelText(/app plugin icon/i)).toBeVisible();
+ });
+
+ it('renders a disabled plugin with a badge to indicate its error', () => {
+ const pluginWithError = { ...plugin, isDisabled: true, error: PluginErrorCode.modifiedSignature };
+ render(
);
+
+ expect(screen.getByText(/disabled/i)).toBeVisible();
+ });
+
+ /** As List */
+ it('renders a row with link, image, name, orgName and badges', () => {
+ render(
);
+
+ expect(screen.getByRole('link')).toHaveAttribute('href', '/plugins/test-plugin?page=overview');
+
+ const logo = screen.getByRole('img');
+ expect(logo).toHaveAttribute('src', plugin.info.logos.small);
+
+ expect(screen.getByRole('heading', { name: /testing plugin/i })).toBeVisible();
+ expect(screen.getByText(`By ${plugin.orgName}`)).toBeVisible();
+ expect(screen.getByText(/signed/i)).toBeVisible();
+ expect(screen.queryByLabelText(/icon/i)).not.toBeInTheDocument();
+ });
+
+ it('renders a datasource plugin with correct icon', () => {
+ const datasourcePlugin = { ...plugin, type: PluginType.datasource };
+ render(
);
+
+ expect(screen.getByLabelText(/datasource plugin icon/i)).toBeVisible();
+ });
+
+ it('renders a panel plugin with correct icon', () => {
+ const panelPlugin = { ...plugin, type: PluginType.panel };
+ render(
);
+
+ expect(screen.getByLabelText(/panel plugin icon/i)).toBeVisible();
+ });
+
+ it('renders an app plugin with correct icon', () => {
+ const appPlugin = { ...plugin, type: PluginType.app };
+ render(
);
+
+ expect(screen.getByLabelText(/app plugin icon/i)).toBeVisible();
+ });
+
+ it('renders a disabled plugin with a badge to indicate its error', () => {
+ const pluginWithError = { ...plugin, isDisabled: true, error: PluginErrorCode.modifiedSignature };
+ render(
);
+
+ expect(screen.getByText(/disabled/i)).toBeVisible();
+ });
+});
diff --git a/public/app/features/plugins/admin/components/PluginListItem.tsx b/public/app/features/plugins/admin/components/PluginListItem.tsx
index 03eab8892ca..0fd69691be0 100644
--- a/public/app/features/plugins/admin/components/PluginListItem.tsx
+++ b/public/app/features/plugins/admin/components/PluginListItem.tsx
@@ -1,78 +1,102 @@
import React from 'react';
-import { css } from '@emotion/css';
+import { css, cx } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
-import { useDisplayMode } from '../state/hooks';
-import { CatalogPlugin, PluginListDisplayMode } from '../types';
-import { PluginListItemRow } from './PluginListItemRow';
-import { PluginListItemCard } from './PluginListItemCard';
+import { CatalogPlugin, PluginIconName, PluginListDisplayMode, PluginTabIds } from '../types';
+import { PluginListItemBadges } from './PluginListItemBadges';
+import { PluginLogo } from './PluginLogo';
+import { Icon, useStyles2 } from '@grafana/ui';
export const LOGO_SIZE = '48px';
type Props = {
plugin: CatalogPlugin;
pathName: string;
+ displayMode?: PluginListDisplayMode;
};
-export function PluginListItem({ plugin, pathName }: Props) {
- const { displayMode } = useDisplayMode();
+export function PluginListItem({ plugin, pathName, displayMode = PluginListDisplayMode.Grid }: Props) {
+ const styles = useStyles2(getStyles);
const isList = displayMode === PluginListDisplayMode.List;
- if (isList) {
- return
;
- }
-
- return
;
+ return (
+
+
+ {plugin.name}
+
+
By {plugin.orgName}
+
+
+
+ {plugin.type && }
+
+
+ );
}
// Styles shared between the different type of list items
-export const getStyles = (theme: GrafanaTheme2, displayMode: PluginListDisplayMode) => {
- const isRow = displayMode === PluginListDisplayMode.List;
- const isCard = displayMode === PluginListDisplayMode.Grid;
-
+export const getStyles = (theme: GrafanaTheme2) => {
return {
- cardContainer: css`
- margin-bottom: 0;
- padding: ${theme.spacing()};
- `,
- headerWrap: css`
+ container: css`
display: grid;
grid-template-columns: ${LOGO_SIZE} 1fr ${theme.spacing(3)};
- grid-gap: ${theme.spacing(2)};
- width: 100%;
- ${isCard &&
- css`
- align-items: center;
- `}
+ grid-template-rows: auto;
+ gap: ${theme.spacing(2)};
+ grid-auto-flow: row;
+ background: ${theme.colors.background.secondary};
+ border-radius: ${theme.shape.borderRadius()};
+ padding: ${theme.spacing(3)};
+ transition: ${theme.transitions.create(['background-color', 'box-shadow', 'border-color', 'color'], {
+ duration: theme.transitions.duration.short,
+ })};
+
+ &:hover {
+ background: ${theme.colors.emphasize(theme.colors.background.secondary, 0.03)};
+ }
+ `,
+ list: css`
+ row-gap: 0px;
+
+ > img {
+ align-self: start;
+ }
+
+ > .plugin-content {
+ min-height: 0px;
+ grid-area: 2 / 2 / 4 / 3;
+
+ > p {
+ margin: ${theme.spacing(0, 0, 0.5, 0)};
+ }
+ }
+
+ > .plugin-name {
+ align-self: center;
+ grid-area: 1 / 2 / 2 / 3;
+ }
+ `,
+ pluginType: css`
+ grid-area: 1 / 3 / 2 / 4;
+ color: ${theme.colors.text.secondary};
+ `,
+ pluginLogo: css`
+ grid-area: 1 / 1 / 3 / 2;
+ max-width: 100%;
+ align-self: center;
+ object-fit: contain;
+ `,
+ content: css`
+ grid-area: 3 / 1 / 4 / 3;
+ color: ${theme.colors.text.secondary};
`,
name: css`
- color: ${theme.colors.text.primary};
- flex-grow: 1;
+ grid-area: 1 / 2 / 3 / 3;
+ align-self: center;
font-size: ${theme.typography.h4.fontSize};
- margin-bottom: 0;
- `,
- image: css`
- object-fit: contain;
- max-width: 100%;
- `,
- icon: css`
- align-self: flex-start;
- color: ${theme.colors.text.secondary};
- `,
- orgName: css`
- color: ${theme.colors.text.secondary};
- ${isRow &&
- css`
- margin: ${theme.spacing(0, 0, 0.5, 0)};
- `}
- ${isCard &&
- css`
- margin-bottom: 0;
- `};
- `,
- hasUpdate: css`
- color: ${theme.colors.text.secondary};
- font-size: ${theme.typography.bodySmall.fontSize};
- margin-bottom: 0;
+ color: ${theme.colors.text.primary};
+ margin: 0;
`,
};
};
diff --git a/public/app/features/plugins/admin/components/PluginListItemCard.test.tsx b/public/app/features/plugins/admin/components/PluginListItemCard.test.tsx
deleted file mode 100644
index 7ca53c9afff..00000000000
--- a/public/app/features/plugins/admin/components/PluginListItemCard.test.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
-import { PluginErrorCode, PluginSignatureStatus, PluginType } from '@grafana/data';
-import { PluginListItemCard } from './PluginListItemCard';
-import { CatalogPlugin } from '../types';
-
-describe('PluginListItemCard', () => {
- const plugin: CatalogPlugin = {
- description: 'The test plugin',
- downloads: 5,
- id: 'test-plugin',
- info: {
- logos: {
- small: 'https://grafana.com/api/plugins/test-plugin/versions/0.0.10/logos/small',
- large: 'https://grafana.com/api/plugins/test-plugin/versions/0.0.10/logos/large',
- },
- },
- name: 'Testing Plugin',
- orgName: 'Test',
- popularity: 0,
- signature: PluginSignatureStatus.valid,
- publishedAt: '2020-09-01',
- updatedAt: '2021-06-28',
- version: '1.0.0',
- hasUpdate: false,
- isInstalled: false,
- isCore: false,
- isDev: false,
- isEnterprise: false,
- isDisabled: false,
- };
-
- it('renders a card with link, image, name, orgName and badges', () => {
- render(
);
-
- expect(screen.getByRole('link')).toHaveAttribute('href', '/plugins/test-plugin?page=overview');
-
- const logo = screen.getByRole('img');
- expect(logo).toHaveAttribute('src', plugin.info.logos.small);
- expect(logo).toHaveAttribute('alt', `${plugin.name} logo`);
-
- expect(screen.getByRole('heading', { name: /testing plugin/i })).toBeVisible();
- expect(screen.getByText(`By ${plugin.orgName}`)).toBeVisible();
- expect(screen.getByText(/signed/i)).toBeVisible();
- expect(screen.queryByLabelText(/icon/i)).not.toBeInTheDocument();
- });
-
- it('renders a datasource plugin with correct icon', () => {
- const datasourcePlugin = { ...plugin, type: PluginType.datasource };
- render(
);
-
- expect(screen.getByTestId(/datasource plugin icon/i)).toBeVisible();
- });
-
- it('renders a panel plugin with correct icon', () => {
- const panelPlugin = { ...plugin, type: PluginType.panel };
- render(
);
-
- expect(screen.getByTestId(/panel plugin icon/i)).toBeVisible();
- });
-
- it('renders an app plugin with correct icon', () => {
- const appPlugin = { ...plugin, type: PluginType.app };
- render(
);
-
- expect(screen.getByTestId(/app plugin icon/i)).toBeVisible();
- });
-
- it('renders a disabled plugin with a badge to indicate its error', () => {
- const pluginWithError = { ...plugin, isDisabled: true, error: PluginErrorCode.modifiedSignature };
- render(
);
-
- expect(screen.getByText(/disabled/i)).toBeVisible();
- });
-});
diff --git a/public/app/features/plugins/admin/components/PluginListItemCard.tsx b/public/app/features/plugins/admin/components/PluginListItemCard.tsx
deleted file mode 100644
index 5ba03839988..00000000000
--- a/public/app/features/plugins/admin/components/PluginListItemCard.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import React from 'react';
-import { Icon, useStyles2, CardContainer, VerticalGroup } from '@grafana/ui';
-import { CatalogPlugin, PluginIconName, PluginListDisplayMode, PluginTabIds } from '../types';
-import { PluginLogo } from './PluginLogo';
-import { PluginListItemBadges } from './PluginListItemBadges';
-import { getStyles, LOGO_SIZE } from './PluginListItem';
-
-type Props = {
- plugin: CatalogPlugin;
- pathName: string;
-};
-
-export function PluginListItemCard({ plugin, pathName }: Props) {
- const styles = useStyles2((theme) => getStyles(theme, PluginListDisplayMode.Grid));
-
- return (
-
-
-
- {/* Logo */}
-
-
- {/* Name */}
-
{plugin.name}
-
- {/* Type Icon */}
- {plugin.type && (
-
-
-
- )}
-
-
- {/* Org */}
- By {plugin.orgName}
-
- {/* Badges */}
-
-
-
- );
-}
diff --git a/public/app/features/plugins/admin/components/PluginListItemRow.test.tsx b/public/app/features/plugins/admin/components/PluginListItemRow.test.tsx
deleted file mode 100644
index 5ce61f1c236..00000000000
--- a/public/app/features/plugins/admin/components/PluginListItemRow.test.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
-import { PluginErrorCode, PluginSignatureStatus, PluginType } from '@grafana/data';
-import { PluginListItemRow } from './PluginListItemRow';
-import { CatalogPlugin } from '../types';
-
-describe('PluginListItemRow', () => {
- const plugin: CatalogPlugin = {
- description: 'The test plugin',
- downloads: 5,
- id: 'test-plugin',
- info: {
- logos: {
- small: 'https://grafana.com/api/plugins/test-plugin/versions/0.0.10/logos/small',
- large: 'https://grafana.com/api/plugins/test-plugin/versions/0.0.10/logos/large',
- },
- },
- name: 'Testing Plugin',
- orgName: 'Test',
- popularity: 0,
- signature: PluginSignatureStatus.valid,
- publishedAt: '2020-09-01',
- updatedAt: '2021-06-28',
- version: '1.0.0',
- hasUpdate: false,
- isInstalled: false,
- isCore: false,
- isDev: false,
- isEnterprise: false,
- isDisabled: false,
- };
-
- it('renders a row with link, image, name, orgName and badges', () => {
- render(
);
-
- expect(screen.getByRole('link')).toHaveAttribute('href', '/plugins/test-plugin?page=overview');
-
- const logo = screen.getByRole('img');
- expect(logo).toHaveAttribute('src', plugin.info.logos.small);
- expect(logo).toHaveAttribute('alt', `${plugin.name} logo`);
-
- expect(screen.getByRole('heading', { name: /testing plugin/i })).toBeVisible();
- expect(screen.getByText(`By ${plugin.orgName}`)).toBeVisible();
- expect(screen.getByText(/signed/i)).toBeVisible();
- expect(screen.queryByLabelText(/icon/i)).not.toBeInTheDocument();
- });
-
- it('renders a datasource plugin with correct icon', () => {
- const datasourcePlugin = { ...plugin, type: PluginType.datasource };
- render(
);
-
- expect(screen.getByLabelText(/datasource plugin icon/i)).toBeVisible();
- });
-
- it('renders a panel plugin with correct icon', () => {
- const panelPlugin = { ...plugin, type: PluginType.panel };
- render(
);
-
- expect(screen.getByLabelText(/panel plugin icon/i)).toBeVisible();
- });
-
- it('renders an app plugin with correct icon', () => {
- const appPlugin = { ...plugin, type: PluginType.app };
- render(
);
-
- expect(screen.getByLabelText(/app plugin icon/i)).toBeVisible();
- });
-
- it('renders a disabled plugin with a badge to indicate its error', () => {
- const pluginWithError = { ...plugin, isDisabled: true, error: PluginErrorCode.modifiedSignature };
- render(
);
-
- expect(screen.getByText(/disabled/i)).toBeVisible();
- });
-});
diff --git a/public/app/features/plugins/admin/components/PluginListItemRow.tsx b/public/app/features/plugins/admin/components/PluginListItemRow.tsx
deleted file mode 100644
index bc599daf661..00000000000
--- a/public/app/features/plugins/admin/components/PluginListItemRow.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import React from 'react';
-import { Icon, useStyles2, CardContainer, VerticalGroup } from '@grafana/ui';
-import { CatalogPlugin, PluginIconName, PluginListDisplayMode, PluginTabIds } from '../types';
-import { PluginLogo } from './PluginLogo';
-import { PluginListItemBadges } from './PluginListItemBadges';
-import { getStyles, LOGO_SIZE } from './PluginListItem';
-
-type Props = {
- plugin: CatalogPlugin;
- pathName: string;
-};
-
-export function PluginListItemRow({ plugin, pathName }: Props) {
- const styles = useStyles2((theme) => getStyles(theme, PluginListDisplayMode.List));
-
- return (
-
-
-
- {/* Logo */}
-
-
-
- {/* Name */}
-
{plugin.name}
-
- {/* Org */}
-
By {plugin.orgName}
-
- {/* Badges */}
-
-
-
- {/* Type Icon */}
- {plugin.type && (
-
-
-
- )}
-
-
-
- );
-}