diff --git a/workspaces/scorecard/.changeset/ninety-hotels-call.md b/workspaces/scorecard/.changeset/ninety-hotels-call.md new file mode 100644 index 00000000000..e5f519d6bc8 --- /dev/null +++ b/workspaces/scorecard/.changeset/ninety-hotels-call.md @@ -0,0 +1,5 @@ +--- +'@red-hat-developer-hub/backstage-plugin-scorecard': patch +--- + +Integrated UI with backend aggregated api diff --git a/workspaces/scorecard/packages/app/e2e-tests/pages/CatalogPage.ts b/workspaces/scorecard/packages/app/e2e-tests/pages/CatalogPage.ts index 735d0abc851..87321217d5e 100644 --- a/workspaces/scorecard/packages/app/e2e-tests/pages/CatalogPage.ts +++ b/workspaces/scorecard/packages/app/e2e-tests/pages/CatalogPage.ts @@ -40,6 +40,6 @@ export class CatalogPage { await page.getByRole('link', { name: 'Settings' }).click(); await page.getByRole('button', { name: 'English' }).click(); await page.getByRole('option', { name: locale }).click(); - await page.locator('a').filter({ hasText: 'Home' }).click(); + await page.locator('a').filter({ hasText: 'Catalog' }).click(); } } diff --git a/workspaces/scorecard/packages/app/e2e-tests/scorecard.test.ts b/workspaces/scorecard/packages/app/e2e-tests/scorecard.test.ts index 6c9add1d2d7..63bb95e1421 100644 --- a/workspaces/scorecard/packages/app/e2e-tests/scorecard.test.ts +++ b/workspaces/scorecard/packages/app/e2e-tests/scorecard.test.ts @@ -159,23 +159,12 @@ test.describe.serial('Scorecard Plugin Tests', () => { ); expect(isGithubVisible).toBe(true); - const errorLocator = page.getByRole('heading', { - name: translations.errors.metricDataUnavailable, - }); + const errorLocator = page.getByText( + translations.errors.metricDataUnavailable, + ); await expect(errorLocator).toBeVisible(); await runAccessibilityTests(page, testInfo); - await errorLocator.hover(); - const errorMetric = unavailableMetricResponse.find( - metric => metric.id === 'github.open_prs', - ); - - if (errorMetric && 'error' in errorMetric) { - const errorTooltip = errorMetric.error; - expect(errorTooltip).toBeTruthy(); - await expect(page.getByText(errorTooltip!)).toBeVisible(); - } - await scorecardPage.validateScorecardAriaFor(jiraMetric); }); @@ -202,22 +191,10 @@ test.describe.serial('Scorecard Plugin Tests', () => { ); expect(isJiraVisible).toBe(true); - const errorLocator = page.getByRole('heading', { - name: translations.errors.invalidThresholds, - }); + const errorLocator = page.getByText(translations.errors.invalidThresholds); await expect(errorLocator).toBeVisible(); await runAccessibilityTests(page, testInfo); - await errorLocator.hover(); - const errorTooltip = invalidThresholdResponse.find( - metric => metric.id === 'github.open_prs', - )?.result?.thresholdResult; - - if (errorTooltip && 'error' in errorTooltip) { - expect(errorTooltip.error).toBeTruthy(); - await expect(page.getByText(errorTooltip.error)).toBeVisible(); - } - await scorecardPage.validateScorecardAriaFor(jiraMetric); }); }); diff --git a/workspaces/scorecard/packages/app/e2e-tests/utils/rbacSetup.ts b/workspaces/scorecard/packages/app/e2e-tests/utils/rbacSetup.ts index 7bc9b5a97aa..8f1810f58cf 100644 --- a/workspaces/scorecard/packages/app/e2e-tests/utils/rbacSetup.ts +++ b/workspaces/scorecard/packages/app/e2e-tests/utils/rbacSetup.ts @@ -57,7 +57,7 @@ export async function setupRBAC(page: Page) { await page.getByTestId('nextButton-2').click(); await page.getByRole('button', { name: 'Create' }).click(); - await page.locator('a').filter({ hasText: 'Home' }).click(); + await page.locator('a').filter({ hasText: 'Catalog' }).click(); await expect(page.getByRole('button', { name: 'Create' })).toBeVisible(); } async function selectCheckbox(page: Page, label: string) { diff --git a/workspaces/scorecard/packages/app/package.json b/workspaces/scorecard/packages/app/package.json index 680b4b33d94..c7e25f532c6 100644 --- a/workspaces/scorecard/packages/app/package.json +++ b/workspaces/scorecard/packages/app/package.json @@ -49,7 +49,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@openshift/dynamic-plugin-sdk": "^5.0.1", - "@red-hat-developer-hub/backstage-plugin-dynamic-home-page": "^1.9.2", + "@red-hat-developer-hub/backstage-plugin-dynamic-home-page": "^1.10.2", "@red-hat-developer-hub/backstage-plugin-scorecard": "workspace:^", "@red-hat-developer-hub/backstage-plugin-theme": "^0.12.0", "@roadiehq/backstage-plugin-github-pull-requests": "^3.5.1", diff --git a/workspaces/scorecard/packages/app/src/App.tsx b/workspaces/scorecard/packages/app/src/App.tsx index 5c2c39c481e..9ce84471e14 100644 --- a/workspaces/scorecard/packages/app/src/App.tsx +++ b/workspaces/scorecard/packages/app/src/App.tsx @@ -54,35 +54,203 @@ import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/ import { scorecardTranslations } from '@red-hat-developer-hub/backstage-plugin-scorecard/alpha'; import { githubAuthApiRef } from '@backstage/core-plugin-api'; import { getThemes } from '@red-hat-developer-hub/backstage-plugin-theme'; -import { ScorecardHomepageSection } from '@red-hat-developer-hub/backstage-plugin-scorecard'; +import { ScorecardHomepageCard } from '@red-hat-developer-hub/backstage-plugin-scorecard'; import { ScalprumContext, ScalprumState } from '@scalprum/react-core'; import { PluginStore } from '@openshift/dynamic-plugin-sdk'; import { DynamicCustomizableHomePage, OnboardingSection, - defaultLayouts, HomePageCardMountPoint, homepageTranslations, } from '@red-hat-developer-hub/backstage-plugin-dynamic-home-page'; +import { ComponentType } from 'react'; const mountPoints: HomePageCardMountPoint[] = [ { Component: OnboardingSection, config: { - layouts: defaultLayouts.onboarding, + id: 'onboarding-section', + title: 'Onboarding section', + layouts: { + xl: { w: 12, h: 5 }, + lg: { w: 12, h: 5 }, + md: { w: 12, h: 5 }, + sm: { w: 12, h: 5 }, + xs: { w: 12, h: 5 }, + xxs: { w: 12, h: 5 }, + }, + }, + }, + { + Component: ScorecardHomepageCard as ComponentType, + config: { + id: 'scorecard-jira.open_issues', + title: 'Scorecard: Jira open blocking tickets', + // Supported card layout + cardLayout: { + width: { + minColumns: 3, + maxColumns: 12, + defaultColumns: 4, + }, + height: { + minRows: 5, + maxRows: 12, + defaultRows: 6, + }, + }, + // Default layout so that it is shown automatically + layouts: { + xl: { w: 4, h: 6 }, + lg: { w: 4, h: 6 }, + md: { w: 4, h: 6 }, + sm: { w: 4, h: 6 }, + xs: { w: 4, h: 6 }, + xxs: { w: 4, h: 6 }, + }, + props: { + metricId: 'jira.open_issues', + }, }, }, { - Component: ScorecardHomepageSection, + Component: ScorecardHomepageCard as ComponentType, config: { + id: 'scorecard-github.open_prs', + title: 'Scorecard: GitHub open PRs', + // Supported card layout + cardLayout: { + width: { + minColumns: 3, + maxColumns: 12, + defaultColumns: 4, + }, + height: { + minRows: 5, + maxRows: 12, + defaultRows: 6, + }, + }, + // Default layout so that it is shown automatically + layouts: { + xl: { w: 4, h: 6, x: 4 }, + lg: { w: 4, h: 6, x: 4 }, + md: { w: 4, h: 6, x: 4 }, + sm: { w: 4, h: 6, x: 4 }, + xs: { w: 4, h: 6, x: 4 }, + xxs: { w: 4, h: 6, x: 4 }, + }, + props: { + metricId: 'github.open_prs', + }, + }, + }, + { + Component: ScorecardHomepageCard as ComponentType, + config: { + id: 'scorecard-customizable', + title: 'Scorecard: Customizable', + // Supported card layout + cardLayout: { + width: { + minColumns: 3, + maxColumns: 12, + defaultColumns: 4, + }, + height: { + minRows: 5, + maxRows: 12, + defaultRows: 6, + }, + }, + // Default layout so that it is shown automatically layouts: { - xl: { w: 12, h: 6 }, - lg: { w: 12, h: 6 }, - md: { w: 12, h: 7 }, - sm: { w: 12, h: 8 }, - xs: { w: 12, h: 9 }, - xxs: { w: 12, h: 10 }, + xl: { w: 4, h: 6, x: 8 }, + lg: { w: 4, h: 6, x: 8 }, + md: { w: 4, h: 6, x: 8 }, + sm: { w: 4, h: 6, x: 8 }, + xs: { w: 4, h: 6, x: 8 }, + xxs: { w: 4, h: 6, x: 8 }, + }, + settings: { + schema: { + properties: { + metricId: { + title: 'Metric (Needs currently a page reload after change!)', + type: 'string', + default: 'jira.open_issues', + enum: ['jira.open_issues', 'github.open_prs'], + }, + }, + }, + uiSchema: { + metricId: { + 'ui:widget': 'RadioWidget', + 'ui:enumNames': ['Jira Open Issues', 'GitHub Open PRs'], + }, + }, + }, + }, + }, + { + Component: ScorecardHomepageCard as ComponentType, + config: { + id: 'scorecard-no-metric-id', + title: 'Scorecard: No metric id (expected error)', + // Supported card layout + cardLayout: { + width: { + minColumns: 3, + maxColumns: 12, + defaultColumns: 4, + }, + height: { + minRows: 5, + maxRows: 12, + defaultRows: 6, + }, + }, + // Default layout so that it is shown automatically + layouts: { + xl: { w: 4, h: 6 }, + lg: { w: 4, h: 6 }, + md: { w: 4, h: 6 }, + sm: { w: 4, h: 6 }, + xs: { w: 4, h: 6 }, + xxs: { w: 4, h: 6 }, + }, + }, + }, + { + Component: ScorecardHomepageCard as ComponentType, + config: { + id: 'scorecard-invalid-metric-id', + title: 'Scorecard: Invalid metric id (expected error)', + // Supported card layout + cardLayout: { + width: { + minColumns: 3, + maxColumns: 12, + defaultColumns: 4, + }, + height: { + minRows: 5, + maxRows: 12, + defaultRows: 6, + }, + }, + // Default layout so that it is shown automatically + layouts: { + xl: { w: 4, h: 6, x: 4 }, + lg: { w: 4, h: 6, x: 4 }, + md: { w: 4, h: 6, x: 4 }, + sm: { w: 4, h: 6, x: 4 }, + xs: { w: 4, h: 6, x: 4 }, + xxs: { w: 4, h: 6, x: 4 }, + }, + props: { + metricId: 'invalid-metric-id', }, }, }, diff --git a/workspaces/scorecard/packages/app/src/components/Root/Root.tsx b/workspaces/scorecard/packages/app/src/components/Root/Root.tsx index 927596f9fc1..d087604ed40 100644 --- a/workspaces/scorecard/packages/app/src/components/Root/Root.tsx +++ b/workspaces/scorecard/packages/app/src/components/Root/Root.tsx @@ -16,6 +16,7 @@ import { PropsWithChildren } from 'react'; import { makeStyles } from '@material-ui/core'; import HomeIcon from '@material-ui/icons/Home'; +import CategoryIcon from '@material-ui/icons/Category'; import ExtensionIcon from '@material-ui/icons/Extension'; import LibraryBooks from '@material-ui/icons/LibraryBooks'; import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; @@ -82,7 +83,8 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( }> {/* Global nav, not org-specific */} - + + { return [...mockScorecardSuccessData, ...mockScorecardErrorData]; } - async getAggregatedScorecards( - _metricIds?: string[], + async getAggregatedScorecard( + _metricId: string, ): Promise { return mockAggregatedScorecardSuccessData; } diff --git a/workspaces/scorecard/plugins/scorecard/report-alpha.api.md b/workspaces/scorecard/plugins/scorecard/report-alpha.api.md index de1bda61d62..fa80088c445 100644 --- a/workspaces/scorecard/plugins/scorecard/report-alpha.api.md +++ b/workspaces/scorecard/plugins/scorecard/report-alpha.api.md @@ -3,8 +3,8 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { TranslationRef } from '@backstage/core-plugin-api/alpha'; -import { TranslationResource } from '@backstage/core-plugin-api/alpha'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; +import { TranslationResource } from '@backstage/frontend-plugin-api'; // @public export const scorecardTranslationRef: TranslationRef< @@ -23,6 +23,9 @@ export const scorecardTranslationRef: TranslationRef< readonly 'errors.fetchError': string; readonly 'errors.metricDataUnavailable': string; readonly 'errors.invalidThresholds': string; + readonly 'errors.missingPermission': string; + readonly 'errors.missingPermissionMessage': string; + readonly 'errors.userNotFoundInCatalogMessage': string; readonly 'metric.github.open_prs.title': string; readonly 'metric.github.open_prs.description': string; readonly 'metric.jira.open_issues.title': string; @@ -30,6 +33,9 @@ export const scorecardTranslationRef: TranslationRef< readonly 'thresholds.success': string; readonly 'thresholds.error': string; readonly 'thresholds.warning': string; + readonly 'thresholds.noEntities': string; + readonly 'thresholds.entities_one': string; + readonly 'thresholds.entities_other': string; } >; diff --git a/workspaces/scorecard/plugins/scorecard/report.api.md b/workspaces/scorecard/plugins/scorecard/report.api.md index 375eac4fa32..ed282936c4b 100644 --- a/workspaces/scorecard/plugins/scorecard/report.api.md +++ b/workspaces/scorecard/plugins/scorecard/report.api.md @@ -10,7 +10,11 @@ import { JSX as JSX_2 } from 'react/jsx-runtime'; export const EntityScorecardContent: () => JSX_2.Element; // @public -export const ScorecardHomepageSection: () => JSX_2.Element; +export const ScorecardHomepageCard: ({ + metricId, +}: { + metricId: string; +}) => JSX_2.Element; // @public export const scorecardPlugin: BackstagePlugin<{}, {}, {}>; diff --git a/workspaces/scorecard/plugins/scorecard/src/api/index.ts b/workspaces/scorecard/plugins/scorecard/src/api/index.ts index abe1eb4d69f..3af0f6b0171 100644 --- a/workspaces/scorecard/plugins/scorecard/src/api/index.ts +++ b/workspaces/scorecard/plugins/scorecard/src/api/index.ts @@ -24,7 +24,6 @@ import type { MetricResult, AggregatedMetricResult, } from '@red-hat-developer-hub/backstage-plugin-scorecard-common'; -import { mockAggregatedScorecardSuccessData } from '../../__fixtures__/aggregatedScorecardData'; export interface ScorecardApi { /** @@ -34,7 +33,7 @@ export interface ScorecardApi { * @returns Promise resolving to an array of metric results */ getScorecards(entity: Entity, metricIds?: string[]): Promise; - getAggregatedScorecards(): Promise; + getAggregatedScorecard(metricId: string): Promise; } export const scorecardApiRef = createApiRef({ @@ -122,8 +121,42 @@ export class ScorecardApiClient implements ScorecardApi { } } - async getAggregatedScorecards(): Promise { - // Return mock data instead of making an API call - return mockAggregatedScorecardSuccessData; + async getAggregatedScorecard( + metricId: string, + ): Promise { + if (!metricId) { + throw new Error('Metric ID is required for aggregated scorecards'); + } + + const baseUrl = await this.getBaseUrl(); + const url = new URL(`${baseUrl}/metrics/${metricId}/catalog/aggregations`); + + try { + const response = await this.fetchApi.fetch(url.toString()); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `Failed to fetch aggregated scorecards: ${response.status} ${response.statusText}. ${errorText}`, + ); + } + + const data = await response.json(); + + if (!Array.isArray(data)) { + throw new TypeError( + 'Invalid response format from aggregated scorecard API', + ); + } + + return data; + } catch (error) { + if (error instanceof Error) { + throw error; + } + throw new Error( + `Unexpected error fetching aggregated scorecards: ${String(error)}`, + ); + } } } diff --git a/workspaces/scorecard/plugins/scorecard/src/components/Common/CardWrapper.tsx b/workspaces/scorecard/plugins/scorecard/src/components/Common/CardWrapper.tsx index 550f6155f02..ac9fc6a2f9b 100644 --- a/workspaces/scorecard/plugins/scorecard/src/components/Common/CardWrapper.tsx +++ b/workspaces/scorecard/plugins/scorecard/src/components/Common/CardWrapper.tsx @@ -20,34 +20,104 @@ import Card from '@mui/material/Card'; import CardHeader from '@mui/material/CardHeader'; import CardContent from '@mui/material/CardContent'; import Divider from '@mui/material/Divider'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; interface CardWrapperProps extends HTMLProps { children: ReactNode; title: string; - subtitle?: ReactNode; + subheader?: ReactNode; + description?: string; width?: string; + childrenWidth?: string | number; + childrenHeight?: string | number; + role?: string; } export const CardWrapper = ({ children, title, - subtitle, - width = '371px', + subheader, + description, + width, + childrenWidth = '100%', + childrenHeight = '100%', + role = 'article', }: CardWrapperProps) => { return ( - + `1px solid ${muiTheme.palette.grey[300]}`, + overflow: 'auto', + }} + role={role} + > - {children} + + {description && ( + + + {description} + + + )} + + + + {children} + + + ); }; diff --git a/workspaces/scorecard/plugins/scorecard/src/components/Common/PermissionRequiredState.tsx b/workspaces/scorecard/plugins/scorecard/src/components/Common/PermissionRequiredState.tsx index 0edc6c138dc..94a6c0ae105 100644 --- a/workspaces/scorecard/plugins/scorecard/src/components/Common/PermissionRequiredState.tsx +++ b/workspaces/scorecard/plugins/scorecard/src/components/Common/PermissionRequiredState.tsx @@ -73,7 +73,7 @@ const PermissionRequiredState = () => {