From 9fb6dfe11d33af1700d9639b4647374a2047ec0e Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Tue, 12 Oct 2021 11:06:22 +0100 Subject: [PATCH] Chore/PluginList: Consolidate PluginListItemRow & PluginListItemCard (#40271) * Chore/PluginList: Consolidate PluginListItemRow & PluginListItemCard --- .../plugins/admin/components/PluginList.tsx | 20 +-- .../admin/components/PluginListItem.test.tsx | 118 ++++++++++++++++ .../admin/components/PluginListItem.tsx | 132 +++++++++++------- .../components/PluginListItemCard.test.tsx | 75 ---------- .../admin/components/PluginListItemCard.tsx | 47 ------- .../components/PluginListItemRow.test.tsx | 75 ---------- .../admin/components/PluginListItemRow.tsx | 49 ------- 7 files changed, 207 insertions(+), 309 deletions(-) create mode 100644 public/app/features/plugins/admin/components/PluginListItem.test.tsx delete mode 100644 public/app/features/plugins/admin/components/PluginListItemCard.test.tsx delete mode 100644 public/app/features/plugins/admin/components/PluginListItemCard.tsx delete mode 100644 public/app/features/plugins/admin/components/PluginListItemRow.test.tsx delete mode 100644 public/app/features/plugins/admin/components/PluginListItemRow.tsx 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 && ( -
- -
- )} -
-
-
- ); -}