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 = () => { ({ color: theme.palette.primary.main, })} diff --git a/workspaces/scorecard/plugins/scorecard/src/components/Common/__tests__/CardWrapper.test.tsx b/workspaces/scorecard/plugins/scorecard/src/components/Common/__tests__/CardWrapper.test.tsx index ceacf9ff7bb..ea60b800310 100644 --- a/workspaces/scorecard/plugins/scorecard/src/components/Common/__tests__/CardWrapper.test.tsx +++ b/workspaces/scorecard/plugins/scorecard/src/components/Common/__tests__/CardWrapper.test.tsx @@ -21,18 +21,18 @@ import { CardWrapper } from '../CardWrapper'; describe('CardWrapper Component', () => { it('should render with title, subtitle and children', () => { render( - + Test Content , ); expect(screen.getByText('Test Title')).toBeInTheDocument(); - expect(screen.getByText('Test Subtitle')).toBeInTheDocument(); + expect(screen.getByText('Test Subheader')).toBeInTheDocument(); expect(screen.getByText('Test Content')).toBeInTheDocument(); }); it('should render with a divider', () => { render( - + Test Content , ); diff --git a/workspaces/scorecard/plugins/scorecard/src/components/Common/__tests__/PermissionRequiredState.test.tsx b/workspaces/scorecard/plugins/scorecard/src/components/Common/__tests__/PermissionRequiredState.test.tsx index cc5b750d55f..128510473c7 100644 --- a/workspaces/scorecard/plugins/scorecard/src/components/Common/__tests__/PermissionRequiredState.test.tsx +++ b/workspaces/scorecard/plugins/scorecard/src/components/Common/__tests__/PermissionRequiredState.test.tsx @@ -121,7 +121,7 @@ describe('PermissionRequiredState Component', () => { const linkButton = screen.getByRole('link', { name: /read more/i }); expect(linkButton).toHaveAttribute( 'href', - 'https://github.com/redhat-developer/rhdh-plugins/blob/main/workspaces/scorecard/plugins/scorecard/README.md#permission-framework-support', + 'https://docs.redhat.com/en/documentation/red_hat_developer_hub/latest/html-single/understand_and_visualize_red_hat_developer_hub_project_health_using_scorecards/index#proc-authenticating-and-managing-scorecard-plugins_assembly-setting-up-scorecards-to-monitor-your-rhdh-health', ); expect(linkButton).toHaveAttribute('target', '_blank'); }); diff --git a/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/CustomLegend.tsx b/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/CustomLegend.tsx new file mode 100644 index 00000000000..103a39b38ac --- /dev/null +++ b/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/CustomLegend.tsx @@ -0,0 +1,109 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + ThresholdResult, + ThresholdRule, +} from '@red-hat-developer-hub/backstage-plugin-scorecard-common'; + +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import { styled, useTheme } from '@mui/material/styles'; + +import { useTranslation } from '../../hooks/useTranslation'; + +const StyledLegend = styled(Box)(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(1), + paddingLeft: theme.spacing(3.2), +})); + +const StyledLegendItem = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), +})); + +const StyledLegendColorBox = styled(Box)<{ color: string }>(({ color }) => ({ + width: '10px', + height: '10px', + backgroundColor: color, + flexShrink: 0, +})); + +type CustomLegendProps = { + thresholds?: ThresholdResult; +}; + +const CustomLegend = (props: CustomLegendProps) => { + const { thresholds } = props; + const theme = useTheme(); + const { t } = useTranslation(); + + if ( + !thresholds || + thresholds?.definition?.rules?.length === 0 || + thresholds?.definition?.rules === undefined + ) { + return ( + + + -- + + + ); + } + + return ( + + {thresholds?.definition?.rules?.map( + ({ key: ruleKey, expression: ruleExpression }: ThresholdRule) => { + return ( + + + )[ruleKey] ?? theme.palette.success.main + } + /> + + {(() => { + const translated = t(`thresholds.${ruleKey}` as any, {}); + // If translation returns the ruleKey itself, fallback to capitalized ruleKey + return translated === `thresholds.${ruleKey}` + ? ruleKey.charAt(0).toUpperCase() + ruleKey.slice(1) + : translated; + })()}{' '} + {ruleExpression && `${ruleExpression}`} + + + ); + }, + )} + + ); +}; + +export default CustomLegend; diff --git a/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/EntityScorecardContent.tsx b/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/EntityScorecardContent.tsx index 6bfa0592202..d8eafe76a57 100644 --- a/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/EntityScorecardContent.tsx +++ b/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/EntityScorecardContent.tsx @@ -96,7 +96,6 @@ export const EntityScorecardContent = () => { key={metric.id} cardTitle={finalTitle} description={finalDescription} - loading={false} statusColor={statusConfig.color} StatusIcon={statusConfig.icon ?? (() => null)} value={metric.result?.value} diff --git a/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/Scorecard.tsx b/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/Scorecard.tsx index 1ff515589a5..6b793b6b877 100644 --- a/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/Scorecard.tsx +++ b/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/Scorecard.tsx @@ -19,22 +19,26 @@ import { ThresholdResult, } from '@red-hat-developer-hub/backstage-plugin-scorecard-common'; -import Card from '@mui/material/Card'; -import CardHeader from '@mui/material/CardHeader'; -import CardContent from '@mui/material/CardContent'; -import Typography from '@mui/material/Typography'; +import { + PieChart, + Pie, + ResponsiveContainer, + Cell, + Legend, + Tooltip, +} from 'recharts'; + import Box from '@mui/material/Box'; -import CircularProgress from '@mui/material/CircularProgress'; -import Divider from '@mui/material/Divider'; -import Grid from '@mui/material/Grid'; -import { styled, useTheme } from '@mui/material/styles'; -import Tooltip from '@mui/material/Tooltip'; +import { useTheme } from '@mui/material/styles'; +import MuiTooltip from '@mui/material/Tooltip'; import { useTranslation } from '../../hooks/useTranslation'; +import { CardWrapper } from '../Common/CardWrapper'; +import CustomLegend from './CustomLegend'; +import { getRingColor } from '../../utils/utils'; interface ScorecardProps { cardTitle: string; description: string; - loading: boolean; statusColor: string; StatusIcon: React.ElementType; value: MetricValue | null; @@ -45,29 +49,9 @@ interface ScorecardProps { thresholdError?: string; } -const StyledCircle = styled('circle')( - ({ - theme, - statusColor, - isError, - }: { - theme: any; - statusColor: string; - isError: boolean; - }) => { - const [paletteKey, shade] = statusColor.split('.'); - return { - stroke: isError - ? theme.palette.rhdh.general.cardBorderColor - : theme.palette?.[paletteKey]?.[shade] ?? statusColor, - }; - }, -); - const Scorecard = ({ cardTitle, description, - loading, statusColor, StatusIcon, value, @@ -80,225 +64,188 @@ const Scorecard = ({ const theme = useTheme(); const { t } = useTranslation(); + const isErrorState = isMetricDataError || isThresholdError; + + const ringColor = getRingColor(theme, statusColor, isErrorState); + + const pieData = [{ name: 'full', value: 100, color: ringColor }]; + return ( - - - - - - - {description} - - + + svg': { + outline: 'none', + }, + }} + > + + + { + if (cx === null || cy === null) return null; - - - - - - {loading ? ( - - - - ) : ( - - - - - - - {!isMetricDataError && !isThresholdError && ( - - muiTheme.palette[statusColor.split('.')[0]][ - statusColor.split('.')[1] - ], - fontSize: 20, - }} - /> - )} - { - if (isMetricDataError || isThresholdError) { - return muiTheme.palette[ - statusColor.split('.')[0] - ]?.[statusColor.split('.')[1]]?.[ - statusColor.split('.')[2] - ]; - } - return muiTheme.palette[ - statusColor.split('.')[0] - ]?.[statusColor.split('.')[1]]; - }, - fontWeight: - isMetricDataError || isThresholdError - ? 400 - : 500, - textAlign: 'center', - fontSize: - isMetricDataError || isThresholdError ? 14 : 24, - }} - > - {isMetricDataError && - t('errors.metricDataUnavailable')} - {!isMetricDataError && - isThresholdError && - t('errors.invalidThresholds')} - {!isThresholdError && !isMetricDataError && value} - - - - - )} - - - + const palettePath = statusColor.split('.'); + let color: string | undefined; + const paletteRoot = + theme.palette[palettePath[0] as keyof typeof theme.palette]; + if (palettePath.length === 1) { + color = paletteRoot as string | undefined; + } else if (palettePath.length === 2) { + color = (paletteRoot as Record)?.[ + palettePath[1] + ] as string | undefined; + } else if (palettePath.length === 3) { + color = (paletteRoot as Record)?.[ + palettePath[1] + ]?.[palettePath[2]] as string | undefined; + } - - - {thresholds?.definition?.rules.length === 0 || - thresholds?.definition?.rules === undefined ? ( - - {' '} - -- - - ) : ( - thresholds?.definition?.rules.map(({ key, expression }) => ( - - + + + muiTheme.palette[statusColor.split('.')[0]][ + statusColor.split('.')[1] + ], }} /> - + {!isErrorState && ( + - {(() => { - const translated = t(`thresholds.${key}` as any, {}); - // If translation returns the key itself, fallback to capitalized key - return translated === `thresholds.${key}` - ? key.charAt(0).toUpperCase() + key.slice(1) - : translated; - })()}{' '} - {expression && `${expression}`} - - - )) - )} - - - - - - + {value} + + )} + + {isErrorState && ( + + + {isMetricDataError && + t('errors.metricDataUnavailable')} + {!isMetricDataError && + isThresholdError && + t('errors.invalidThresholds')} + + + )} + + ); + }} + > + {pieData.map(entry => ( + + ))} + + + ( + + )} + /> + + { + if (!active) return null; + + return ( + + {/* Need to hide the tooltip content because we are using the position prop to position the tooltip */} + Tooltip content + + ); + }} + /> + + + + ); }; diff --git a/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/__tests__/Scorecard.test.tsx b/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/__tests__/Scorecard.test.tsx index b8c48ec9a71..45b2425fe34 100644 --- a/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/__tests__/Scorecard.test.tsx +++ b/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/__tests__/Scorecard.test.tsx @@ -22,6 +22,22 @@ import DangerousOutlinedIcon from '@mui/icons-material/DangerousOutlined'; import Scorecard from '../Scorecard'; +jest.mock('recharts', () => ({ + ResponsiveContainer: ({ children }: any) => ( + {children} + ), + + PieChart: ({ children }: any) => {children}, + + Pie: ({ label }: any) => {label?.({ cx: 0, cy: 0 })}, + + Cell: () => null, + + Legend: ({ content }: any) => {content?.({})}, + + Tooltip: () => null, +})); + // Create a test theme to provide proper palette colors const testTheme = createTheme({ palette: { @@ -40,7 +56,6 @@ describe('Scorecard Component', () => { cardTitle: 'GitHub open PRs', description: 'Current count of open Pull Requests for a given GitHub repository.', - loading: false, statusColor: 'success', StatusIcon: CheckCircleOutlineIcon, value: 8, @@ -107,17 +122,6 @@ describe('Scorecard Component', () => { expect(iconElement).toBeInTheDocument(); }); - it('should show loading spinner when loading is true', () => { - render( - - - , - ); - - expect(screen.getByRole('progressbar')).toBeInTheDocument(); - expect(screen.queryByTestId('8')).not.toBeInTheDocument(); - }); - it('should handle zero value correctly', () => { render( @@ -205,8 +209,7 @@ describe('Scorecard Component', () => { render(); expect(screen.getByText('GitHub open PRs')).toBeInTheDocument(); - expect(screen.getByText('8')).toBeInTheDocument(); - expect(screen.queryByText('Error')).not.toBeInTheDocument(); + expect(screen.getByText('--')).toBeInTheDocument(); }); it('should render correctly with different card titles', () => { diff --git a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/CustomLegend.tsx b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/CustomLegend.tsx index d998132ee05..7a2a66ffd7c 100644 --- a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/CustomLegend.tsx +++ b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/CustomLegend.tsx @@ -17,7 +17,9 @@ import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import { styled } from '@mui/material/styles'; + import type { PieData as PieDataProps } from '../../utils/utils'; +import { useTranslation } from '../../hooks/useTranslation'; const StyledLegend = styled(Box)(({ theme }) => ({ display: 'flex', @@ -33,12 +35,14 @@ const StyledLegendItem = styled(Box)(({ theme }) => ({ cursor: 'pointer', })); -const StyledLegendColorBox = styled(Box)<{ color: string }>(({ color }) => ({ - width: '10px', - height: '10px', - backgroundColor: color, - flexShrink: 0, -})); +const StyledLegendColorBox = styled(Box)<{ color?: string }>( + ({ color = 'success.main' }) => ({ + width: '10px', + height: '10px', + backgroundColor: color, + flexShrink: 0, + }), +); type CustomLegendProps = { pieData: PieDataProps[]; @@ -49,6 +53,7 @@ type CustomLegendProps = { const CustomLegend = (props: CustomLegendProps) => { const { pieData, activeIndex, setActiveIndex, setTooltipPosition } = props; + const { t } = useTranslation(); if (!pieData || pieData.length === 0) return null; return ( @@ -93,7 +98,11 @@ const CustomLegend = (props: CustomLegendProps) => { const relatedTarget = e.relatedTarget as Node | null; const currentTarget = e.currentTarget; - if (!relatedTarget || !currentTarget.contains(relatedTarget)) { + if ( + !relatedTarget || + !(relatedTarget instanceof Node) || + !currentTarget.contains(relatedTarget) + ) { setActiveIndex(null); setTooltipPosition(null); } @@ -104,7 +113,13 @@ const CustomLegend = (props: CustomLegendProps) => { variant="body2" sx={{ fontSize: '0.875rem', fontWeight: 400 }} > - {category.name.charAt(0).toUpperCase() + category.name.slice(1)} + {(() => { + const translated = t(`thresholds.${category.name}` as any, {}); + return translated === `thresholds.${category.name}` + ? category.name.charAt(0).toUpperCase() + + category.name.slice(1) + : translated; + })()} ); diff --git a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/CustomTooltip.tsx b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/CustomTooltip.tsx index 417bd3a0e9a..5b0b58f1b76 100644 --- a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/CustomTooltip.tsx +++ b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/CustomTooltip.tsx @@ -14,10 +14,13 @@ * limitations under the License. */ +import type { TooltipProps } from 'recharts'; + import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; -import type { TooltipProps } from 'recharts'; + import type { PieData } from '../../utils/utils'; +import { useTranslation } from '../../hooks/useTranslation'; type CustomTooltipPayload = { name?: string; @@ -28,9 +31,18 @@ type CustomTooltipPayload = { type CustomTooltipProps = TooltipProps & { payload?: readonly CustomTooltipPayload[]; pieData: PieData[]; + isMissingPermission?: boolean; + isUserNotFoundInCatalog?: boolean; }; -export const CustomTooltip = ({ payload, pieData }: CustomTooltipProps) => { +export const CustomTooltip = ({ + payload, + pieData, + isMissingPermission = false, + isUserNotFoundInCatalog = false, +}: CustomTooltipProps) => { + const { t } = useTranslation(); + const getPercentage = () => { if (!Array.isArray(pieData) || pieData.length === 0) return 0; if (!payload || payload.length === 0) return 0; @@ -50,19 +62,26 @@ export const CustomTooltip = ({ payload, pieData }: CustomTooltipProps) => { let content = null; - if (payload?.[0]?.value === 0 || payload?.[0]?.value === undefined) { + if (isMissingPermission || isUserNotFoundInCatalog) { + content = ( + + {isMissingPermission + ? t('errors.missingPermissionMessage') + : t('errors.userNotFoundInCatalogMessage')} + + ); + } else if (payload?.[0]?.value === 0 || payload?.[0]?.value === undefined) { + const translatedState = t(`thresholds.${payload?.[0]?.name}` as any, {}); content = ( - No entities in {payload?.[0]?.name} state + {t('thresholds.noEntities', { category: translatedState } as any)} ); } else { content = ( <> - {payload?.[0]?.value === 1 - ? '1 entity' - : `${payload?.[0]?.value} entities`} + {t('thresholds.entities', { count: payload?.[0]?.value })} { + const theme = useTheme(); + const { t } = useTranslation(); + + const titleKey = `metric.${metricId}.title`; + const descriptionKey = `metric.${metricId}.description`; + + const cardTitle = t(titleKey as any, {}); + const cardDescription = t(descriptionKey as any, {}); + + const statusConfig = getStatusConfig({ + evaluation: 'error', + thresholdStatus: 'error', + }); + + const ringColor = getRingColor(theme, statusConfig.color, true); + + const pieData = [{ name: 'full', value: 100, color: ringColor }]; + + const isMissingPermission = error.message?.includes('NotAllowedError'); + + const isUserNotFoundInCatalog = + error.message?.includes('NotFoundError') && + error.message?.includes('User entity not found in catalog'); + + if (isMissingPermission || isUserNotFoundInCatalog) { + return ( + + svg': { + outline: 'none', + }, + }} + > + { + if (cx === null || cy === null) return null; + + const palettePath = statusConfig.color.split('.'); + let color: string | undefined; + const paletteRoot = + theme.palette[palettePath[0] as keyof typeof theme.palette]; + if (palettePath.length === 1) { + color = paletteRoot as string | undefined; + } else if (palettePath.length === 2) { + color = (paletteRoot as Record)?.[ + palettePath[1] + ] as string | undefined; + } else if (palettePath.length === 3) { + color = (paletteRoot as Record)?.[ + palettePath[1] + ]?.[palettePath[2]]; + } + + return ( + + + + {isMissingPermission + ? t('errors.missingPermission') + : null} + + + + ); + }} + legendContent={props => ( + + )} + tooltipContent={props => ( + + )} + /> + + + ); + } + + return ; +}; diff --git a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/ResponsivePieChart.tsx b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/ResponsivePieChart.tsx new file mode 100644 index 00000000000..82c4ad74f40 --- /dev/null +++ b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/ResponsivePieChart.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + PieChart, + Pie, + ResponsiveContainer, + Cell, + Legend, + Tooltip, + PieLabelRenderProps, +} from 'recharts'; +import { PieData } from '../../utils/utils'; + +interface PieTooltipPayload { + name?: string; + value?: number; + payload?: { + name: string; + value: number; + color?: string; + }; +} + +export interface PieLegendItem { + value?: string; + color?: string; +} + +export interface PieLegendContentProps { + payload?: readonly PieLegendItem[]; +} + +interface PieTooltipContentProps { + active?: boolean; + payload?: readonly PieTooltipPayload[]; + label?: string | number; +} +interface ResponsivePieChartProps { + pieData: PieData[]; + isMissingPermission?: boolean; + LabelContent?: (props: PieLabelRenderProps) => React.ReactNode; + legendContent: (props: PieLegendContentProps) => React.ReactNode; + tooltipContent: (props: PieTooltipContentProps) => React.ReactNode; +} + +export const ResponsivePieChart = ({ + pieData, + isMissingPermission = false, + LabelContent, + legendContent, + tooltipContent, +}: ResponsivePieChartProps) => { + return ( + + + + {pieData.map(category => ( + + ))} + + + + + + + + ); +}; diff --git a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/ScorecardHomepageCard.tsx b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/ScorecardHomepageCard.tsx index 2301ab87be6..4dc72ee2952 100644 --- a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/ScorecardHomepageCard.tsx +++ b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/ScorecardHomepageCard.tsx @@ -14,162 +14,68 @@ * limitations under the License. */ -import { useState } from 'react'; +import { Fragment } from 'react'; -import { - PieChart, - Pie, - ResponsiveContainer, - Cell, - Tooltip, - Legend, -} from 'recharts'; -import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; -import { useTheme } from '@mui/material/styles'; - -import { CardWrapper } from '../Common/CardWrapper'; -import { CustomTooltip } from './CustomTooltip'; -import CustomLegend from './CustomLegend'; -import type { PieData } from '../../utils/utils'; import type { AggregatedMetricResult } from '@red-hat-developer-hub/backstage-plugin-scorecard-common'; -export const ScorecardHomepageCard = ({ - scorecard, -}: { - scorecard: AggregatedMetricResult; -}) => { - const theme = useTheme(); - const [activeIndex, setActiveIndex] = useState(null); - const [tooltipPosition, setTooltipPosition] = useState<{ - x: number; - y: number; - } | null>(null); +import Box from '@mui/material/Box'; +import CircularProgress from '@mui/material/CircularProgress'; - const pieData: PieData[] = - scorecard.result.values?.map(value => ({ - name: value.name, - value: value.count, - color: - { - success: theme.palette.success.main, - warning: theme.palette.warning.main, - error: theme.palette.error.main, - }[value.name] || theme.palette.success.main, - })) ?? []; +import { ScorecardHomepageCardComponent } from './ScorecardHomepageCardComponent'; +import { EmptyStatePanel } from './EmptyStatePanel'; +import { useAggregatedScorecard } from '../../hooks/useAggregatedScorecard'; +import { useTranslation } from '../../hooks/useTranslation'; - return ( - - - - {scorecard.metadata.description} - - +export const ScorecardHomepageCard = ({ metricId }: { metricId: string }) => { + const { t } = useTranslation(); + + const { aggregatedScorecard, loadingData, error } = useAggregatedScorecard({ + metricId, + }); + if (loadingData) { + return ( svg': { - outline: 'none', - }, - }} + display="flex" + justifyContent="center" + alignItems="center" + minHeight="200px" > - - - - {pieData.map(category => ( - - ))} - + + + ); + } - ( - - )} - /> + if (error) { + return ; + } - ( - - )} - /> - - + return ( + + {aggregatedScorecard + ?.slice(0, 1) + .map((metric: AggregatedMetricResult) => { + const titleKey = `metric.${metric.id}.title`; + const descriptionKey = `metric.${metric.id}.description`; - {activeIndex !== null && tooltipPosition && ( - - - - )} - - + ); + })} + ); }; diff --git a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/ScorecardHomepageCardComponent.tsx b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/ScorecardHomepageCardComponent.tsx new file mode 100644 index 00000000000..cef8f7b3cdf --- /dev/null +++ b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/ScorecardHomepageCardComponent.tsx @@ -0,0 +1,128 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useState } from 'react'; + +import type { AggregatedMetricResult } from '@red-hat-developer-hub/backstage-plugin-scorecard-common'; + +import Box from '@mui/material/Box'; +import { useTheme } from '@mui/material/styles'; + +import { CardWrapper } from '../Common/CardWrapper'; +import { CustomTooltip } from './CustomTooltip'; +import CustomLegend from './CustomLegend'; +import type { PieData } from '../../utils/utils'; +import { useTranslation } from '../../hooks/useTranslation'; +import { ResponsivePieChart } from './ResponsivePieChart'; + +export const ScorecardHomepageCardComponent = ({ + scorecard, + cardTitle, + description, +}: { + scorecard: AggregatedMetricResult; + cardTitle: string; + description: string; +}) => { + const theme = useTheme(); + const { t } = useTranslation(); + + const [activeIndex, setActiveIndex] = useState(null); + const [tooltipPosition, setTooltipPosition] = useState<{ + x: number; + y: number; + } | null>(null); + + const pieData: PieData[] = + scorecard.result.values?.map(value => ({ + name: value.name, + value: value.count, + color: + { + success: theme.palette.success.main, + warning: theme.palette.warning.main, + error: theme.palette.error.main, + }[value.name] || theme.palette.success.main, + })) ?? []; + + return ( + + svg': { + outline: 'none', + }, + }} + > + ( + + )} + tooltipContent={({ active, payload }) => + active && payload ? ( + + ) : null + } + /> + + {activeIndex !== null && tooltipPosition && ( + + + + )} + + + ); +}; diff --git a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/ScorecardHomepageSection.tsx b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/ScorecardHomepageSection.tsx deleted file mode 100644 index 2b4de7f121a..00000000000 --- a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/ScorecardHomepageSection.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ResponseErrorPanel } from '@backstage/core-components'; - -import Grid from '@mui/material/Grid'; -import Box from '@mui/material/Box'; -import CircularProgress from '@mui/material/CircularProgress'; - -import { ScorecardHomepageCard } from './ScorecardHomepageCard'; -import PermissionRequiredState from '../Common/PermissionRequiredState'; -import { useAggregatedScorecards } from '../../hooks/useAggregatedScorecards'; -import type { AggregatedMetricResult } from '@red-hat-developer-hub/backstage-plugin-scorecard-common'; - -export const ScorecardHomepageSection = () => { - const { aggregatedScorecards, loadingData, error } = - useAggregatedScorecards(); - - if (loadingData) { - return ( - - - - ); - } - - if (error) { - if (error.message?.includes('NotAllowedError')) { - return ; - } - return ; - } - - return ( - - - {aggregatedScorecards - ?.slice(0, 2) - .map((scorecard: AggregatedMetricResult) => ( - - - - ))} - - - ); -}; diff --git a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/__tests__/CustomLegend.test.tsx b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/__tests__/CustomLegend.test.tsx index 24b64ce92f4..41b00197652 100644 --- a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/__tests__/CustomLegend.test.tsx +++ b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/__tests__/CustomLegend.test.tsx @@ -17,35 +17,42 @@ import { render, screen, fireEvent } from '@testing-library/react'; import CustomLegend from '../CustomLegend'; -import { CustomTooltip } from '../CustomTooltip'; -describe('CustomLegend Component', () => { - it('should render with the correct number of legend items', () => { - const pieData = [ - { name: 'test1', value: 10, color: 'red' }, - { name: 'test2', value: 20, color: 'blue' }, - { name: 'test3', value: 30, color: 'green' }, - ]; +jest.mock('../../../hooks/useTranslation', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); +describe('CustomLegend', () => { + const pieData = [ + { name: 'success', value: 10, color: 'red' }, + { name: 'warning', value: 20, color: 'blue' }, + { name: 'error', value: 30, color: 'green' }, + ]; + + it('should render correct number of legend items', () => { render( - , + + + , ); - const legendItems = screen.getAllByText(/Test[1-3]/i); - expect(legendItems).toHaveLength(3); + expect(screen.getByText('Success')).toBeInTheDocument(); + expect(screen.getByText('Warning')).toBeInTheDocument(); + expect(screen.getByText('Error')).toBeInTheDocument(); }); - it('should show correct value in tooltip on hover', () => { + it('should call setActiveIndex and setTooltipPosition on mouse enter', () => { const setActiveIndex = jest.fn(); const setTooltipPosition = jest.fn(); - const pieData = [{ name: 'Test', value: 10, color: 'red' }]; - const { rerender } = render( + render( { , ); - const legendItem = screen.getByText('Test'); - fireEvent.mouseEnter(legendItem); + const successItem = screen.getByText('Success'); + + fireEvent.mouseEnter(successItem); expect(setActiveIndex).toHaveBeenCalledWith(0); + expect(setTooltipPosition).toHaveBeenCalledWith( + expect.objectContaining({ + x: expect.any(Number), + y: expect.any(Number), + }), + ); + }); + + it('should clear active index and tooltip on mouse leave of legend container', () => { + const setActiveIndex = jest.fn(); + const setTooltipPosition = jest.fn(); - rerender( - + + , + ); + + const legendItem = getByText('Success'); + + const legendContainer = legendItem.parentElement?.parentElement; + + expect(legendContainer).toBeTruthy(); + + fireEvent.mouseLeave(legendContainer as Element, { + relatedTarget: null, + }); + + expect(setActiveIndex).toHaveBeenCalledWith(null); + expect(setTooltipPosition).toHaveBeenCalledWith(null); + }); + + it('should return null when pieData is empty', () => { + const { container } = render( + , ); - expect(screen.getByText('10 entities')).toBeInTheDocument(); + expect(container.firstChild).toBeNull(); }); }); diff --git a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/__tests__/CustomTooltip.test.tsx b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/__tests__/CustomTooltip.test.tsx index fa0cf22fc83..90a0f19aeef 100644 --- a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/__tests__/CustomTooltip.test.tsx +++ b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/__tests__/CustomTooltip.test.tsx @@ -18,25 +18,58 @@ import { render, screen } from '@testing-library/react'; import { CustomTooltip } from '../CustomTooltip'; +jest.mock('../../../hooks/useTranslation', () => ({ + useTranslation: () => ({ + t: (key: string, options?: any) => { + switch (key) { + case 'thresholds.entities': + return `${options?.count} entities`; + case 'thresholds.noEntities': + return `No entities in ${options?.category} state`; + case 'thresholds.Test': + return 'Test'; + case 'errors.missingPermissionMessage': + return 'Missing permission'; + default: + return key; + } + }, + }), +})); + describe('CustomTooltip Component', () => { - it('should render with the correct content when there are entities', () => { + it('should render entity count and percentage when entities exist', () => { render( , ); + expect(screen.getByText('10 entities')).toBeInTheDocument(); expect(screen.getByText('100%')).toBeInTheDocument(); }); - it('should render with the correct content when there are no entities', () => { + it('should render no-entities message when value is zero', () => { render( , ); + expect(screen.getByText('No entities in Test state')).toBeInTheDocument(); }); + + it('should render missing permission message when isMissingPermission is true', () => { + render( + , + ); + + expect(screen.getByText('Missing permission')).toBeInTheDocument(); + }); }); diff --git a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/__tests__/ScorecardHomepageCard.test.tsx b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/__tests__/ScorecardHomepageCard.test.tsx index 8076e1b3e26..801f4d92c57 100644 --- a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/__tests__/ScorecardHomepageCard.test.tsx +++ b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/__tests__/ScorecardHomepageCard.test.tsx @@ -17,86 +17,134 @@ import { render, screen } from '@testing-library/react'; import { ThemeProvider, createTheme } from '@mui/material/styles'; -import { ScorecardHomepageCard } from '../ScorecardHomepageCard'; -import { mockAggregatedScorecardSuccessData } from '../../../../__fixtures__/aggregatedScorecardData'; +import { ScorecardHomepageCardComponent } from '../ScorecardHomepageCardComponent'; import type { AggregatedMetricResult } from '@red-hat-developer-hub/backstage-plugin-scorecard-common'; -// Mock the child components +// -------------------- +// Mocks +// -------------------- + jest.mock('../../Common/CardWrapper', () => ({ - CardWrapper: function MockCardWrapper({ + CardWrapper: ({ title, - subtitle, + subheader, + description, children, }: { title: string; - subtitle: string; + subheader: string; + description: string; children: React.ReactNode; - }) { - return ( - - {title} - {subtitle} - {children} - - ); - }, + }) => ( + + {title} + {subheader} + {description} + {children} + + ), })); -jest.mock('../CustomTooltip', () => ({ - CustomTooltip: function MockCustomTooltip() { - return Custom Tooltip; - }, +jest.mock('../ResponsivePieChart', () => ({ + ResponsivePieChart: ({ + legendContent, + tooltipContent, + pieData, + }: { + legendContent: (props: unknown) => React.ReactNode; + tooltipContent: (props: { + active?: boolean; + payload?: unknown[]; + }) => React.ReactNode; + pieData: Array<{ name: string; value: number; color: string }>; + }) => ( + + {pieData.length} + {legendContent({})} + + {tooltipContent({ active: true, payload: [] })} + + + ), })); jest.mock('../CustomLegend', () => ({ __esModule: true, - default: function MockCustomLegend() { - return Custom Legend; - }, + default: () => Custom Legend, })); -// Mock recharts components -jest.mock('recharts', () => ({ - PieChart: function MockPieChart({ children }: { children: React.ReactNode }) { - return {children}; - }, - Pie: function MockPie({ children }: { children: React.ReactNode }) { - return {children}; - }, - ResponsiveContainer: function MockResponsiveContainer({ - children, - }: { - children: React.ReactNode; - }) { - return {children}; - }, - Cell: function MockCell() { - return ; - }, - Tooltip: function MockTooltip() { - return ; - }, - Legend: function MockLegend() { - return ; - }, +jest.mock('../CustomTooltip', () => ({ + CustomTooltip: () => Custom Tooltip, })); -const TestWrapper = ({ children }: { children: React.ReactNode }) => { - const theme = createTheme({ - palette: { - success: { main: '#52c41a' }, - warning: { main: '#F0AB00' }, - error: { main: '#C9190B' }, +jest.mock('../../../hooks/useTranslation', () => ({ + useTranslation: () => ({ + t: (key: string, options?: any) => { + switch (key) { + case 'thresholds.entities': + return `${options?.count} entities`; + case 'thresholds.noEntities': + return `No entities in ${options?.category} state`; + case 'thresholds.Test': + return 'Test'; + case 'errors.missingPermissionMessage': + return 'Missing permission'; + default: + return key; + } }, - }); - return {children}; + }), +})); + +// -------------------- +// Test data +// -------------------- + +const mockScorecard: AggregatedMetricResult = { + id: 'github.open_prs', + status: 'success', + metadata: { + title: 'GitHub open PRs', + description: 'Open PRs', + type: 'number', + history: true, + }, + result: { + total: 37, + values: [ + { name: 'success', count: 11 }, + { name: 'warning', count: 14 }, + { name: 'error', count: 12 }, + ], + timestamp: '2024-01-01T00:00:00Z', + }, }; -describe('ScorecardHomepageCard Component', () => { - it('should render card with title and subtitle', () => { +const TestWrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + +); + +// -------------------- +// Tests +// -------------------- + +describe('ScorecardHomepageCardComponent', () => { + it('should render title, subheader, and description', () => { render( - , { wrapper: TestWrapper }, ); @@ -104,136 +152,112 @@ describe('ScorecardHomepageCard Component', () => { expect(screen.getByTestId('card-title')).toHaveTextContent( 'GitHub open PRs', ); - expect(screen.getByTestId('card-subtitle')).toHaveTextContent( + expect(screen.getByTestId('card-subheader')).toHaveTextContent( '37 entities', ); + expect(screen.getByTestId('card-description')).toHaveTextContent( + 'Current count of open Pull Requests', + ); }); - it('should render description', () => { + it('should render ResponsivePieChart', () => { render( - , { wrapper: TestWrapper }, ); - expect( - screen.getByText( - 'Current count of open Pull Requests for a given GitHub repository.', - ), - ).toBeInTheDocument(); + expect(screen.getByTestId('responsive-pie-chart')).toBeInTheDocument(); }); - it('should render pie chart components', () => { + it('should pass correct pie data length', () => { render( - , { wrapper: TestWrapper }, ); - expect(screen.getByTestId('responsive-container')).toBeInTheDocument(); - expect(screen.getByTestId('pie-chart')).toBeInTheDocument(); - expect(screen.getByTestId('pie')).toBeInTheDocument(); + expect(screen.getByTestId('pie-data-length')).toHaveTextContent('3'); }); - it('should calculate total entities correctly', () => { + it('should render CustomLegend and CustomTooltip', () => { render( - , { wrapper: TestWrapper }, ); - // 11 + 14 + 12 = 37 - expect(screen.getByTestId('card-subtitle')).toHaveTextContent( - '37 entities', - ); - }); - - it('should handle scorecard with zero total entities', () => { - const scorecardWithZero: AggregatedMetricResult = { - ...mockAggregatedScorecardSuccessData[0], - result: { - ...mockAggregatedScorecardSuccessData[0].result, - values: [ - { count: 0, name: 'success' }, - { count: 0, name: 'warning' }, - { count: 0, name: 'error' }, - ], - total: 0, - }, - }; - - render(, { - wrapper: TestWrapper, - }); - - expect(screen.getByTestId('card-subtitle')).toHaveTextContent('0 entities'); - }); - - it('should handle scorecard with empty result values', () => { - const scorecardWithEmpty: AggregatedMetricResult = { - ...mockAggregatedScorecardSuccessData[0], - result: { - ...mockAggregatedScorecardSuccessData[0].result, - values: [], - total: 0, - }, - }; - - render(, { - wrapper: TestWrapper, - }); - - expect(screen.getByTestId('card-subtitle')).toHaveTextContent('0 entities'); + expect(screen.getByTestId('custom-legend')).toBeInTheDocument(); + expect(screen.getByTestId('custom-tooltip')).toBeInTheDocument(); }); - it('should handle scorecard with missing result values', () => { - const scorecardWithNull: AggregatedMetricResult = { - ...mockAggregatedScorecardSuccessData[0], + it('should handle empty values gracefully', () => { + const emptyScorecard: AggregatedMetricResult = { + ...mockScorecard, result: { - ...mockAggregatedScorecardSuccessData[0].result, + ...mockScorecard.result, values: [], total: 0, }, }; - render(, { - wrapper: TestWrapper, - }); + render( + , + { wrapper: TestWrapper }, + ); - expect(screen.getByTestId('card-subtitle')).toHaveTextContent('0 entities'); + expect(screen.getByTestId('card-subheader')).toHaveTextContent( + '0 entities', + ); + expect(screen.getByTestId('pie-data-length')).toHaveTextContent('0'); }); - it('should render with different scorecard data', () => { - render( - { + const { container } = render( + , { wrapper: TestWrapper }, ); - expect(screen.getByTestId('card-title')).toHaveTextContent( - 'Jira open blocking tickets', - ); - expect(screen.getByTestId('card-subtitle')).toHaveTextContent('4 entities'); expect( - screen.getByText( - 'Highlights the number of critical, blocking issues that are currently open in Jira.', - ), + container.querySelector('[data-chart-container]'), ).toBeInTheDocument(); }); - it('should render chart container with correct attributes', () => { - const { container } = render( - { + const scorecardWithoutValues: AggregatedMetricResult = { + ...mockScorecard, + result: { + ...mockScorecard.result, + values: [], + }, + }; + + render( + , { wrapper: TestWrapper }, ); - const chartContainer = container.querySelector('[data-chart-container]'); - expect(chartContainer).toBeInTheDocument(); + expect(screen.getByTestId('pie-data-length')).toHaveTextContent('0'); }); }); diff --git a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/__tests__/ScorecardHomepageSection.test.tsx b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/__tests__/ScorecardHomepageSection.test.tsx index bc284008821..e3c5f11f835 100644 --- a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/__tests__/ScorecardHomepageSection.test.tsx +++ b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/__tests__/ScorecardHomepageSection.test.tsx @@ -17,160 +17,178 @@ import { render, screen } from '@testing-library/react'; import { ThemeProvider, createTheme } from '@mui/material/styles'; -import { ScorecardHomepageSection } from '../ScorecardHomepageSection'; -import { mockAggregatedScorecardSuccessData } from '../../../../__fixtures__/aggregatedScorecardData'; +import { ScorecardHomepageCard } from '../ScorecardHomepageCard'; + import type { AggregatedMetricResult } from '@red-hat-developer-hub/backstage-plugin-scorecard-common'; // Mock the child components -jest.mock('../../Common/PermissionRequiredState', () => { - return function MockPermissionRequiredState() { - return ( - Permission Required - ); - }; -}); +jest.mock('../../../hooks/useAggregatedScorecard', () => ({ + useAggregatedScorecard: jest.fn(), +})); -jest.mock('../ScorecardHomepageCard', () => ({ - ScorecardHomepageCard: function MockScorecardHomepageCard({ - scorecard, - }: { - scorecard: { id: string; metadata: { title: string } }; - }) { - return ( - - {scorecard.metadata.title} - - ); - }, +jest.mock('../../../hooks/useTranslation', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), })); -jest.mock('@backstage/core-components', () => ({ - ResponseErrorPanel: ({ error }: { error: Error }) => ( - {error?.message ?? 'Error'} +jest.mock('../ScorecardHomepageCardComponent', () => ({ + ScorecardHomepageCardComponent: ({ + scorecard, + }: { + scorecard: AggregatedMetricResult; + }) => ( + {scorecard.metadata.title} ), })); -jest.mock('../../../hooks/useAggregatedScorecards', () => ({ - useAggregatedScorecards: jest.fn(), +jest.mock('../EmptyStatePanel', () => ({ + EmptyStatePanel: ({ + error, + metricId, + }: { + error: Error; + metricId: string; + }) => ( + + {error.message} + {metricId} + + ), })); -// Get the mocked function const { - useAggregatedScorecards, -} = require('../../../hooks/useAggregatedScorecards'); - -const TestWrapper = ({ children }: { children: React.ReactNode }) => { - const theme = createTheme(); - return {children}; + useAggregatedScorecard, +} = require('../../../hooks/useAggregatedScorecard'); + +const TestWrapper = ({ children }: { children: React.ReactNode }) => ( + {children} +); + +const mockScorecard: AggregatedMetricResult = { + id: 'github.open_prs', + status: 'success', + metadata: { + title: 'GitHub open PRs', + description: 'Open PR count', + type: 'number', + history: true, + }, + result: { + total: 8, + values: [{ name: 'success', count: 8 }], + timestamp: '2024-01-01T00:00:00Z', + }, }; -describe('ScorecardHomepageSection Component', () => { +describe('ScorecardHomepageCard', () => { beforeEach(() => { jest.clearAllMocks(); }); it('should render loading spinner when data is loading', () => { - useAggregatedScorecards.mockReturnValue({ - aggregatedScorecards: undefined, + useAggregatedScorecard.mockReturnValue({ + aggregatedScorecard: undefined, loadingData: true, error: undefined, }); - render(); + render(, { + wrapper: TestWrapper, + }); + expect(screen.getByRole('progressbar')).toBeInTheDocument(); }); - it('should render permission required state when error contains NotAllowedError', () => { - useAggregatedScorecards.mockReturnValue({ - aggregatedScorecards: [], + it('should render empty state panel when NotAllowedError occurs', () => { + useAggregatedScorecard.mockReturnValue({ + aggregatedScorecard: undefined, loadingData: false, error: new Error('NotAllowedError: missing permission'), }); - render(); - expect(screen.getByTestId('permission-required-state')).toBeInTheDocument(); - }); + render(, { + wrapper: TestWrapper, + }); - it('should render error panel for non-permission errors', () => { - const error = new Error('Something went wrong'); + expect(screen.getByTestId('empty-state-panel')).toBeInTheDocument(); + expect(screen.getByTestId('metric-id')).toHaveTextContent( + 'jira.open_issues', + ); + expect(screen.getByTestId('error-message')).toHaveTextContent( + 'NotAllowedError: missing permission', + ); + }); - useAggregatedScorecards.mockReturnValue({ - aggregatedScorecards: [], + it('should render empty state panel for non-permission errors', () => { + useAggregatedScorecard.mockReturnValue({ + aggregatedScorecard: undefined, loadingData: false, - error, + error: new Error('Something went wrong'), }); - render(); + render(, { + wrapper: TestWrapper, + }); - expect(screen.getByTestId('response-error-panel')).toBeInTheDocument(); - expect(screen.getByText('Something went wrong')).toBeInTheDocument(); + expect(screen.getByTestId('empty-state-panel')).toBeInTheDocument(); + expect(screen.getByTestId('error-message')).toHaveTextContent( + 'Something went wrong', + ); }); - it('should render scorecard cards when data is loaded successfully', () => { - useAggregatedScorecards.mockReturnValue({ - aggregatedScorecards: mockAggregatedScorecardSuccessData, + it('should render scorecard homepage card when data loads successfully', () => { + useAggregatedScorecard.mockReturnValue({ + aggregatedScorecard: [mockScorecard], loadingData: false, error: undefined, }); - render(, { wrapper: TestWrapper }); + render(, { + wrapper: TestWrapper, + }); - const cards = screen.getAllByTestId('scorecard-homepage-card'); - expect(cards).toHaveLength(2); + expect(screen.getByTestId('scorecard-homepage-card')).toBeInTheDocument(); expect(screen.getByText('GitHub open PRs')).toBeInTheDocument(); - expect(screen.getByText('Jira open blocking tickets')).toBeInTheDocument(); }); - it('should render only first two scorecards when more than two are available', () => { - const threeScorecards: AggregatedMetricResult[] = [ - ...mockAggregatedScorecardSuccessData, - { - id: 'third.scorecard', - status: 'success', - metadata: { - title: 'Third Scorecard', - description: 'Third description', - type: 'number', - history: true, + it('should render only the first scorecard when multiple are returned', () => { + useAggregatedScorecard.mockReturnValue({ + aggregatedScorecard: [ + mockScorecard, + { + ...mockScorecard, + id: 'second.metric', + metadata: { + ...mockScorecard.metadata, + title: 'Second Metric', + }, }, - result: { - values: [{ count: 5, name: 'success' }], - total: 5, - timestamp: '2024-01-15T10:30:00Z', - }, - }, - ]; - - useAggregatedScorecards.mockReturnValue({ - aggregatedScorecards: threeScorecards, + ], loadingData: false, error: undefined, }); - render(, { wrapper: TestWrapper }); + render(, { + wrapper: TestWrapper, + }); - const cards = screen.getAllByTestId('scorecard-homepage-card'); - expect(cards).toHaveLength(2); + expect(screen.getAllByTestId('scorecard-homepage-card')).toHaveLength(1); expect(screen.getByText('GitHub open PRs')).toBeInTheDocument(); - expect(screen.getByText('Jira open blocking tickets')).toBeInTheDocument(); - expect(screen.queryByText('Third Scorecard')).not.toBeInTheDocument(); + expect(screen.queryByText('Second Metric')).not.toBeInTheDocument(); }); - it('should render empty state when there are no scorecards', () => { - useAggregatedScorecards.mockReturnValue({ - aggregatedScorecards: [], + it('should render nothing when no scorecards are returned', () => { + useAggregatedScorecard.mockReturnValue({ + aggregatedScorecard: [], loadingData: false, error: undefined, }); - render(); + render(, { + wrapper: TestWrapper, + }); - expect( - screen.queryByTestId('permission-required-state'), - ).not.toBeInTheDocument(); - expect( - screen.queryByTestId('response-error-panel'), - ).not.toBeInTheDocument(); expect( screen.queryByTestId('scorecard-homepage-card'), ).not.toBeInTheDocument(); diff --git a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/index.ts b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/index.ts index 5439b9a2baf..f00af6c56e0 100644 --- a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/index.ts +++ b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { ScorecardHomepageSection } from './ScorecardHomepageSection'; +export { ScorecardHomepageCard } from './ScorecardHomepageCard'; diff --git a/workspaces/scorecard/plugins/scorecard/src/hooks/useAggregatedScorecards.tsx b/workspaces/scorecard/plugins/scorecard/src/hooks/useAggregatedScorecard.tsx similarity index 64% rename from workspaces/scorecard/plugins/scorecard/src/hooks/useAggregatedScorecards.tsx rename to workspaces/scorecard/plugins/scorecard/src/hooks/useAggregatedScorecard.tsx index 9eba5378809..4b93fff9256 100644 --- a/workspaces/scorecard/plugins/scorecard/src/hooks/useAggregatedScorecards.tsx +++ b/workspaces/scorecard/plugins/scorecard/src/hooks/useAggregatedScorecard.tsx @@ -19,35 +19,47 @@ import { useApi } from '@backstage/core-plugin-api'; import useAsync from 'react-use/lib/useAsync'; import { scorecardApiRef } from '../api'; +import { useTranslation } from './useTranslation'; import { AggregatedMetricResult } from '@red-hat-developer-hub/backstage-plugin-scorecard-common'; -export const useAggregatedScorecards = () => { +interface UseAggregatedScorecardOptions { + metricId: string; +} + +export const useAggregatedScorecard = ( + options: UseAggregatedScorecardOptions, +) => { const scorecardApi = useApi(scorecardApiRef); + const { metricId } = options; + const { t } = useTranslation(); + const { error, loading, value } = useAsync(async () => { try { - const aggregatedScorecards = await scorecardApi.getAggregatedScorecards(); + const aggregatedScorecard = await scorecardApi.getAggregatedScorecard( + metricId, + ); - if (!aggregatedScorecards || !Array.isArray(aggregatedScorecards)) { - throw new Error( - 'Invalid response format from aggregated scorecard API', - ); + if (!aggregatedScorecard || !Array.isArray(aggregatedScorecard)) { + throw new Error(t('errors.invalidApiResponse')); } - return aggregatedScorecards; + return aggregatedScorecard; } catch (err) { if (err instanceof Error) { throw err; } throw new Error( - `Unexpected error fetching aggregated scorecards: ${String(err)}`, + t('errors.fetchError' as any, { + error: String(err), + }), ); } }, [scorecardApi]); return useMemo( () => ({ - aggregatedScorecards: value as AggregatedMetricResult[], + aggregatedScorecard: value as AggregatedMetricResult[], loadingData: loading, error, }), diff --git a/workspaces/scorecard/plugins/scorecard/src/plugin.ts b/workspaces/scorecard/plugins/scorecard/src/plugin.ts index 8d6ad5f2fa3..1a968a44946 100644 --- a/workspaces/scorecard/plugins/scorecard/src/plugin.ts +++ b/workspaces/scorecard/plugins/scorecard/src/plugin.ts @@ -66,16 +66,16 @@ export const EntityScorecardContent = scorecardPlugin.provide( ); /** - * Scorecard homepage section. + * Scorecard homepage card. * @public */ -export const ScorecardHomepageSection = scorecardPlugin.provide( +export const ScorecardHomepageCard = scorecardPlugin.provide( createComponentExtension({ - name: 'ScorecardHomepageSection', + name: 'ScorecardHomepageCard', component: { lazy: () => import('./components/ScorecardHomepageSection').then( - m => m.ScorecardHomepageSection, + m => m.ScorecardHomepageCard, ), }, }), diff --git a/workspaces/scorecard/plugins/scorecard/src/translations/de.ts b/workspaces/scorecard/plugins/scorecard/src/translations/de.ts index 68ca39d734b..b28e2ba3836 100644 --- a/workspaces/scorecard/plugins/scorecard/src/translations/de.ts +++ b/workspaces/scorecard/plugins/scorecard/src/translations/de.ts @@ -43,7 +43,11 @@ const scorecardTranslationDe = createTranslationMessages({ 'errors.fetchError': 'Fehler beim Abrufen der Scorecards: {{error}}', 'errors.metricDataUnavailable': 'Metrikdaten nicht verfügbar', 'errors.invalidThresholds': 'Ungültige Schwellenwerte', - + 'errors.missingPermission': 'Berechtigung fehlt', + 'errors.missingPermissionMessage': + 'Um die Scorecard-Metriken anzuzeigen, muss Ihr Administrator die erforderliche Berechtigung erteilen.', + 'errors.userNotFoundInCatalogMessage': + 'Benutzer-Entität in Katalog nicht gefunden', // Metric translations 'metric.github.open_prs.title': 'GitHub offene PRs', 'metric.github.open_prs.description': @@ -56,6 +60,9 @@ const scorecardTranslationDe = createTranslationMessages({ 'thresholds.success': 'Erfolg', 'thresholds.warning': 'Warnung', 'thresholds.error': 'Fehler', + 'thresholds.noEntities': 'Keine Entitäten in {{category}} Zustand', + 'thresholds.entities_one': '{{count}} Entität', + 'thresholds.entities_other': '{{count}} Entitäten', }, }); diff --git a/workspaces/scorecard/plugins/scorecard/src/translations/es.ts b/workspaces/scorecard/plugins/scorecard/src/translations/es.ts index 8e41a7d4e57..aa0b83aacae 100644 --- a/workspaces/scorecard/plugins/scorecard/src/translations/es.ts +++ b/workspaces/scorecard/plugins/scorecard/src/translations/es.ts @@ -43,7 +43,11 @@ const scorecardTranslationEs = createTranslationMessages({ 'errors.fetchError': 'Error al obtener scorecards: {{error}}', 'errors.metricDataUnavailable': 'Datos de métrica no disponibles', 'errors.invalidThresholds': 'Umbrales inválidos', - + 'errors.missingPermission': 'Permiso faltante', + 'errors.missingPermissionMessage': + 'Para ver las métricas de scorecard, tu administrador debe otorgarle el permiso requerido.', + 'errors.userNotFoundInCatalogMessage': + 'Entidad de usuario no encontrada en el catálogo', // Metric translations 'metric.github.open_prs.title': 'GitHub PRs abiertas', 'metric.github.open_prs.description': @@ -56,6 +60,9 @@ const scorecardTranslationEs = createTranslationMessages({ 'thresholds.success': 'Éxito', 'thresholds.warning': 'Advertencia', 'thresholds.error': 'Error', + 'thresholds.noEntities': 'No hay entidades en el estado {{category}}', + 'thresholds.entities_one': '{{count}} entidad', + 'thresholds.entities_other': '{{count}} entidades', }, }); diff --git a/workspaces/scorecard/plugins/scorecard/src/translations/fr.ts b/workspaces/scorecard/plugins/scorecard/src/translations/fr.ts index fa0532811e2..5efbf21fa9f 100644 --- a/workspaces/scorecard/plugins/scorecard/src/translations/fr.ts +++ b/workspaces/scorecard/plugins/scorecard/src/translations/fr.ts @@ -42,6 +42,12 @@ const scorecardTranslationFr = createTranslationMessages({ 'Erreur lors de la récupération des tableaux de bord : {{error}}', 'errors.metricDataUnavailable': 'Données métriques indisponibles', 'errors.invalidThresholds': 'Seuils invalides', + 'errors.missingPermission': 'Permission manquante', + 'errors.missingPermissionMessage': + 'Pour voir les métriques de scorecard, votre administrateur doit vous donner la permission requise.', + 'errors.userNotFoundInCatalogMessage': + 'Entité utilisateur non trouvée dans le catalogue', + // Metric translations 'metric.github.open_prs.title': 'GitHub ouvre des PR', 'metric.github.open_prs.description': "Nombre actuel de requêtes d'extraction ouvertes pour un référentiel GitHub donné.", @@ -51,6 +57,9 @@ const scorecardTranslationFr = createTranslationMessages({ 'thresholds.success': 'Succès', 'thresholds.warning': 'Attention', 'thresholds.error': 'Erreur', + 'thresholds.noEntities': "Aucune entité dans l'état {{category}}", + 'thresholds.entities_one': '{{count}} entité', + 'thresholds.entities_other': '{{count}} entités', }, }); diff --git a/workspaces/scorecard/plugins/scorecard/src/translations/it.ts b/workspaces/scorecard/plugins/scorecard/src/translations/it.ts index ccb6bcf29d0..d2c4db7d1aa 100644 --- a/workspaces/scorecard/plugins/scorecard/src/translations/it.ts +++ b/workspaces/scorecard/plugins/scorecard/src/translations/it.ts @@ -42,6 +42,11 @@ const scorecardTranslationIt = createTranslationMessages({ 'Errore durante il recupero delle schede punteggio: {{error}}', 'errors.metricDataUnavailable': 'Dati metrici non disponibili', 'errors.invalidThresholds': 'Soglie non valide', + 'errors.missingPermission': 'Autorizzazione mancante', + 'errors.missingPermissionMessage': + "Per visualizzare le metriche della scheda punteggio, il tuo amministratore deve concedere l'autorizzazione richiesta.", + 'errors.userNotFoundInCatalogMessage': + 'Entità utente non trovata nel catalogo', 'metric.github.open_prs.title': 'Richieste pull aperte su GitHub', 'metric.github.open_prs.description': 'Conteggio attuale delle richieste pull aperte per uno specifico repository GitHub.', diff --git a/workspaces/scorecard/plugins/scorecard/src/translations/ja.ts b/workspaces/scorecard/plugins/scorecard/src/translations/ja.ts index e811ff7c58a..f46dc1dfb20 100644 --- a/workspaces/scorecard/plugins/scorecard/src/translations/ja.ts +++ b/workspaces/scorecard/plugins/scorecard/src/translations/ja.ts @@ -41,6 +41,11 @@ const scorecardTranslationJa = createTranslationMessages({ 'スコアカードの取得中にエラーが発生しました: {{error}}', 'errors.metricDataUnavailable': 'メトリクスデータがありません', 'errors.invalidThresholds': '無効なしきい値', + 'errors.missingPermission': '権限がありません', + 'errors.missingPermissionMessage': + 'スコアカードのメトリクスを表示するには、管理者に権限を付与してもらうよう依頼してください。', + 'errors.userNotFoundInCatalogMessage': + 'ユーザーエンティティーがカタログに見つかりません', 'metric.github.open_prs.title': 'GitHub のオープン状態の PR', 'metric.github.open_prs.description': '特定の GitHub リポジトリーにおけるオープン状態のプルリクエストの数。', diff --git a/workspaces/scorecard/plugins/scorecard/src/translations/ref.ts b/workspaces/scorecard/plugins/scorecard/src/translations/ref.ts index f1e19eeb61d..19afd291f59 100644 --- a/workspaces/scorecard/plugins/scorecard/src/translations/ref.ts +++ b/workspaces/scorecard/plugins/scorecard/src/translations/ref.ts @@ -48,6 +48,10 @@ export const scorecardMessages = { fetchError: 'Error fetching scorecards: {{error}}', metricDataUnavailable: 'Metric data unavailable', invalidThresholds: 'Invalid thresholds', + missingPermission: 'Missing permission', + missingPermissionMessage: + 'To view the scorecard metrics, your administrator must grant you the required permission.', + userNotFoundInCatalogMessage: 'User entity not found in catalog', }, // Metric translations @@ -69,6 +73,9 @@ export const scorecardMessages = { success: 'Success', warning: 'Warning', error: 'Error', + noEntities: 'No entities in {{category}} state', + entities_one: '{{count}} entity', + entities_other: '{{count}} entities', }, }; diff --git a/workspaces/scorecard/plugins/scorecard/src/utils/utils.ts b/workspaces/scorecard/plugins/scorecard/src/utils/utils.ts index 78e08d30f99..59bf84ef684 100644 --- a/workspaces/scorecard/plugins/scorecard/src/utils/utils.ts +++ b/workspaces/scorecard/plugins/scorecard/src/utils/utils.ts @@ -26,7 +26,7 @@ export type StatusConfig = { export type PieData = { name: string; value: number; - color: string; + color?: string; }; /** @@ -57,3 +57,16 @@ export const getStatusConfig = ({ return { color: 'success.main', icon: CheckCircleOutlineIcon }; } }; + +export const getRingColor = ( + theme: any, + statusColor: string, + isError: boolean, +) => { + if (isError) { + return theme.palette.rhdh.general.cardBorderColor; + } + + const [paletteKey, shade] = statusColor.split('.'); + return theme.palette?.[paletteKey]?.[shade] ?? statusColor; +}; diff --git a/workspaces/scorecard/yarn.lock b/workspaces/scorecard/yarn.lock index 9899dd51498..3499b781df1 100644 --- a/workspaces/scorecard/yarn.lock +++ b/workspaces/scorecard/yarn.lock @@ -2387,12 +2387,12 @@ __metadata: languageName: node linkType: hard -"@backstage/core-app-api@npm:^1.19.2": - version: 1.19.2 - resolution: "@backstage/core-app-api@npm:1.19.2" +"@backstage/core-app-api@npm:^1.19.2, @backstage/core-app-api@npm:^1.19.3": + version: 1.19.3 + resolution: "@backstage/core-app-api@npm:1.19.3" dependencies: "@backstage/config": ^1.3.6 - "@backstage/core-plugin-api": ^1.12.0 + "@backstage/core-plugin-api": ^1.12.1 "@backstage/types": ^1.2.2 "@backstage/version-bridge": ^1.0.11 "@types/prop-types": ^15.7.3 @@ -2411,17 +2411,17 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: d5ac01731427a6be00a9565f56a2ada51066ec341c580cd268571aa63e4a7c8c4430437452ae028af96f476547c68b205c83328c639fbe79c3902cd75e2d5382 + checksum: 3312e9cbcfcbe43789be63f03aaa88fa02615638d10620ea7de0955b839dc25f357a89deb448ca73cbaa9f46f6006d07bd3e9bd8c734cd1cbe0c8a5f056d3257 languageName: node linkType: hard -"@backstage/core-compat-api@npm:^0.5.1, @backstage/core-compat-api@npm:^0.5.4": - version: 0.5.4 - resolution: "@backstage/core-compat-api@npm:0.5.4" +"@backstage/core-compat-api@npm:^0.5.1, @backstage/core-compat-api@npm:^0.5.4, @backstage/core-compat-api@npm:^0.5.5": + version: 0.5.5 + resolution: "@backstage/core-compat-api@npm:0.5.5" dependencies: - "@backstage/core-plugin-api": ^1.12.0 - "@backstage/frontend-plugin-api": ^0.13.0 - "@backstage/plugin-catalog-react": ^1.21.3 + "@backstage/core-plugin-api": ^1.12.1 + "@backstage/frontend-plugin-api": ^0.13.2 + "@backstage/plugin-catalog-react": ^1.21.4 "@backstage/types": ^1.2.2 "@backstage/version-bridge": ^1.0.11 lodash: ^4.17.21 @@ -2434,7 +2434,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 30fd05cf3ae21fa3ceeee993bcce96307f67285de440509d1fb8d235fe8d2a38cc3a81fba1ea06681e5ad87aee136a0628dec26204f7df7926ec8e428614aa45 + checksum: 2ef780091ae2cf081001dd12f14c802b7580d68970e5eba322745f6a1f613a35b3dddd2bcc40ba91949919ed9541ae2a4c22b3830de43a32aa9bd3dafcfcb1c8 languageName: node linkType: hard @@ -2492,14 +2492,14 @@ __metadata: languageName: node linkType: hard -"@backstage/core-components@npm:^0.18.3": - version: 0.18.3 - resolution: "@backstage/core-components@npm:0.18.3" +"@backstage/core-components@npm:^0.18.3, @backstage/core-components@npm:^0.18.4": + version: 0.18.4 + resolution: "@backstage/core-components@npm:0.18.4" dependencies: "@backstage/config": ^1.3.6 - "@backstage/core-plugin-api": ^1.12.0 + "@backstage/core-plugin-api": ^1.12.1 "@backstage/errors": ^1.2.7 - "@backstage/theme": ^0.7.0 + "@backstage/theme": ^0.7.1 "@backstage/version-bridge": ^1.0.11 "@dagrejs/dagre": ^1.1.4 "@date-io/core": ^1.3.13 @@ -2543,16 +2543,17 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 8199868482e956781a1238c08ed185fe152a31b1bc4cefbcc87d68d1a1eb814794816a5b9579e585385469a86859ee20f9959a5dbac8009b1c57504080d205c5 + checksum: e35456caef98472f19e27f87475a1419ebc8afd371c104822d0bf0b20b5ab57d369510fdbb6c3a6843920d7eec8287bfb2c5cd4151e203c2379a475c5ac4d30d languageName: node linkType: hard -"@backstage/core-plugin-api@npm:^1.10.8, @backstage/core-plugin-api@npm:^1.10.9, @backstage/core-plugin-api@npm:^1.12.0": - version: 1.12.0 - resolution: "@backstage/core-plugin-api@npm:1.12.0" +"@backstage/core-plugin-api@npm:^1.10.8, @backstage/core-plugin-api@npm:^1.10.9, @backstage/core-plugin-api@npm:^1.12.0, @backstage/core-plugin-api@npm:^1.12.1": + version: 1.12.1 + resolution: "@backstage/core-plugin-api@npm:1.12.1" dependencies: "@backstage/config": ^1.3.6 "@backstage/errors": ^1.2.7 + "@backstage/frontend-plugin-api": ^0.13.2 "@backstage/types": ^1.2.2 "@backstage/version-bridge": ^1.0.11 history: ^5.0.0 @@ -2565,7 +2566,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 5992ca8bdb6e1218e0abda7b6ad64bb4c9ac904db694ff23b3ee5097fc76226804a2895bd28f17eb3c2045ea02bbf57dd1e9699775dda0739ac27d99aa00202f + checksum: aa5d5e0c67ef04233b54630abc08eab47d129ba084893a6ab2d1ad023c75e94ef1780c880e8064c484c483455297de8e9cd278c5b8609db06680c9354e5b6609 languageName: node linkType: hard @@ -2632,16 +2633,16 @@ __metadata: languageName: node linkType: hard -"@backstage/frontend-app-api@npm:^0.13.2": - version: 0.13.2 - resolution: "@backstage/frontend-app-api@npm:0.13.2" +"@backstage/frontend-app-api@npm:^0.13.3": + version: 0.13.3 + resolution: "@backstage/frontend-app-api@npm:0.13.3" dependencies: "@backstage/config": ^1.3.6 - "@backstage/core-app-api": ^1.19.2 - "@backstage/core-plugin-api": ^1.12.0 + "@backstage/core-app-api": ^1.19.3 + "@backstage/core-plugin-api": ^1.12.1 "@backstage/errors": ^1.2.7 - "@backstage/frontend-defaults": ^0.3.3 - "@backstage/frontend-plugin-api": ^0.13.0 + "@backstage/frontend-defaults": ^0.3.4 + "@backstage/frontend-plugin-api": ^0.13.2 "@backstage/types": ^1.2.2 "@backstage/version-bridge": ^1.0.11 lodash: ^4.17.21 @@ -2654,20 +2655,20 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: b63be182fec44dc0af61d040dd06dcacdee3ae60a228bad245b0a72bfd52e9554199f7e226aefef88ee807ee24a1f750a4133e59dee1d6c077ffc47ebf1b9023 + checksum: f85b528818b1fd507f3190553ce9e094302530df42cd841aee173a4f75b7e20a567ff87c2a19bdce06bb5952afb1a97873507b7ee6c62b6bd6cd63c370fa867e languageName: node linkType: hard -"@backstage/frontend-defaults@npm:^0.3.3": - version: 0.3.3 - resolution: "@backstage/frontend-defaults@npm:0.3.3" +"@backstage/frontend-defaults@npm:^0.3.4": + version: 0.3.4 + resolution: "@backstage/frontend-defaults@npm:0.3.4" dependencies: "@backstage/config": ^1.3.6 - "@backstage/core-components": ^0.18.3 + "@backstage/core-components": ^0.18.4 "@backstage/errors": ^1.2.7 - "@backstage/frontend-app-api": ^0.13.2 - "@backstage/frontend-plugin-api": ^0.13.0 - "@backstage/plugin-app": ^0.3.2 + "@backstage/frontend-app-api": ^0.13.3 + "@backstage/frontend-plugin-api": ^0.13.2 + "@backstage/plugin-app": ^0.3.3 "@react-hookz/web": ^24.0.0 peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 @@ -2677,7 +2678,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: df9b95734f1ac2004e0cce6fb09803341db616533c10bf187f6d0e058ed0246a5e0e8a0e67ef76c4c5745a74c728335cc23ca4695a2cba5980f9555363d20e6a + checksum: 681c421367035c957bf545816ce7cc2258a6f5c7a07d7bab29899033d3a12d4c277f182f6782c0d665df012ac58aedb9d2665a16ff6f2b9032a86654481b9359 languageName: node linkType: hard @@ -2705,16 +2706,13 @@ __metadata: languageName: node linkType: hard -"@backstage/frontend-plugin-api@npm:^0.13.0": - version: 0.13.1 - resolution: "@backstage/frontend-plugin-api@npm:0.13.1" +"@backstage/frontend-plugin-api@npm:^0.13.0, @backstage/frontend-plugin-api@npm:^0.13.2": + version: 0.13.2 + resolution: "@backstage/frontend-plugin-api@npm:0.13.2" dependencies: - "@backstage/core-components": ^0.18.3 - "@backstage/core-plugin-api": ^1.12.0 + "@backstage/errors": ^1.2.7 "@backstage/types": ^1.2.2 "@backstage/version-bridge": ^1.0.11 - "@material-ui/core": ^4.12.4 - lodash: ^4.17.21 zod: ^3.22.4 zod-to-json-schema: ^3.21.4 peerDependencies: @@ -2725,19 +2723,19 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 13ff5aae1179386927912ec2f7d491576214441604af195995504d22a30bce73d07319eb9de4b504acbde07d61c7b3c445a546cfb39b4a4aa10493565202a8a7 + checksum: e733130bb2ade9ff48b8a18d6d141352e6491ace2eaa6846799a6232510eb5086e820cccacc25b3341c26b2a4a141d388cd996dcdc516d59d61a38b932a62563 languageName: node linkType: hard -"@backstage/frontend-test-utils@npm:^0.4.1": - version: 0.4.1 - resolution: "@backstage/frontend-test-utils@npm:0.4.1" +"@backstage/frontend-test-utils@npm:^0.4.2": + version: 0.4.2 + resolution: "@backstage/frontend-test-utils@npm:0.4.2" dependencies: "@backstage/config": ^1.3.6 - "@backstage/frontend-app-api": ^0.13.2 - "@backstage/frontend-plugin-api": ^0.13.0 - "@backstage/plugin-app": ^0.3.2 - "@backstage/test-utils": ^1.7.13 + "@backstage/frontend-app-api": ^0.13.3 + "@backstage/frontend-plugin-api": ^0.13.2 + "@backstage/plugin-app": ^0.3.3 + "@backstage/test-utils": ^1.7.14 "@backstage/types": ^1.2.2 "@backstage/version-bridge": ^1.0.11 zod: ^3.22.4 @@ -2750,7 +2748,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: f931650ba2fe0a0247396679183d45b2c1fb2260020b47994de43fa141f692bd56eb7cae620b5ecc9afa6c953cbec2f81241b9a3809b5d1170c2ad9bfc40a0d9 + checksum: bd4537700f8447339c1a9f457778ed895a5cb710ce1c34d5a78c8caaa02e350e8bdd2848a5ea20ce782a299318752db765137e2b5de472f97ba3ac0f11b4c151 languageName: node linkType: hard @@ -2769,13 +2767,13 @@ __metadata: languageName: node linkType: hard -"@backstage/integration-react@npm:^1.2.12, @backstage/integration-react@npm:^1.2.8, @backstage/integration-react@npm:^1.2.9": - version: 1.2.12 - resolution: "@backstage/integration-react@npm:1.2.12" +"@backstage/integration-react@npm:^1.2.12, @backstage/integration-react@npm:^1.2.13, @backstage/integration-react@npm:^1.2.8, @backstage/integration-react@npm:^1.2.9": + version: 1.2.13 + resolution: "@backstage/integration-react@npm:1.2.13" dependencies: "@backstage/config": ^1.3.6 - "@backstage/core-plugin-api": ^1.12.0 - "@backstage/integration": ^1.18.2 + "@backstage/core-plugin-api": ^1.12.1 + "@backstage/integration": ^1.19.0 "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 peerDependencies: @@ -2786,13 +2784,13 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: b7be51304119be9cfc4d474111aed25515828a9f4de72ef23bbd3502e5db33d93b320a3a020fc1064e7ea3784c1564c1c1dd1e3692c8d71425107ea7cdf40a18 + checksum: fa2053316d56f2886d32567a64d356af91cdf237aa3a10a2dc91af78e2f799980a32c556e041da106894a95ab17a399905c95aa0a9a968df371ca30faec0d629 languageName: node linkType: hard -"@backstage/integration@npm:^1.17.0, @backstage/integration@npm:^1.17.1, @backstage/integration@npm:^1.18.0, @backstage/integration@npm:^1.18.2": - version: 1.18.2 - resolution: "@backstage/integration@npm:1.18.2" +"@backstage/integration@npm:^1.17.0, @backstage/integration@npm:^1.17.1, @backstage/integration@npm:^1.18.0, @backstage/integration@npm:^1.18.2, @backstage/integration@npm:^1.19.0": + version: 1.19.1 + resolution: "@backstage/integration@npm:1.19.1" dependencies: "@azure/identity": ^4.0.0 "@azure/storage-blob": ^12.5.0 @@ -2804,7 +2802,7 @@ __metadata: git-url-parse: ^15.0.0 lodash: ^4.17.21 luxon: ^3.0.0 - checksum: 5d037450c4673a0eada8df7f000d4294b970b67c7937fa875165e7bac96a04943f1e8438fe2de06bd6ddfd39777e56c553a6b0cbe30cf1442ed49ae5399f3d67 + checksum: c670fe93630bb016b453f5c0601fdf6730633b92aac6de9e7df67093c98d89df6352f2fae13b5c07226f98736eb6af237434d67233107b768557e3bf3c7ea110 languageName: node linkType: hard @@ -2880,16 +2878,16 @@ __metadata: languageName: node linkType: hard -"@backstage/plugin-app@npm:^0.3.2": - version: 0.3.2 - resolution: "@backstage/plugin-app@npm:0.3.2" - dependencies: - "@backstage/core-components": ^0.18.3 - "@backstage/core-plugin-api": ^1.12.0 - "@backstage/frontend-plugin-api": ^0.13.0 - "@backstage/integration-react": ^1.2.12 - "@backstage/plugin-permission-react": ^0.4.38 - "@backstage/theme": ^0.7.0 +"@backstage/plugin-app@npm:^0.3.3": + version: 0.3.3 + resolution: "@backstage/plugin-app@npm:0.3.3" + dependencies: + "@backstage/core-components": ^0.18.4 + "@backstage/core-plugin-api": ^1.12.1 + "@backstage/frontend-plugin-api": ^0.13.2 + "@backstage/integration-react": ^1.2.13 + "@backstage/plugin-permission-react": ^0.4.39 + "@backstage/theme": ^0.7.1 "@backstage/types": ^1.2.2 "@backstage/version-bridge": ^1.0.11 "@material-ui/core": ^4.9.13 @@ -2906,7 +2904,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 3c78aae2172d50905a429e7f9221e77f68449b87903424e9fd1659a5a7dded99098292943efcb0b94a339607234ba84d086895efcfb0a22f0e010689e3cba174 + checksum: 4dc3369581442d2e4d57ad90b4b6521ec210e8db402bcc8450a833cb1d41d173eb1e9a0bb106ff6be40e8ceaf41326018c02df75117e4939525696b930214d83 languageName: node linkType: hard @@ -3181,22 +3179,22 @@ __metadata: languageName: node linkType: hard -"@backstage/plugin-catalog-react@npm:^1.19.0, @backstage/plugin-catalog-react@npm:^1.20.1, @backstage/plugin-catalog-react@npm:^1.21.3": - version: 1.21.3 - resolution: "@backstage/plugin-catalog-react@npm:1.21.3" +"@backstage/plugin-catalog-react@npm:^1.19.0, @backstage/plugin-catalog-react@npm:^1.20.1, @backstage/plugin-catalog-react@npm:^1.21.3, @backstage/plugin-catalog-react@npm:^1.21.4": + version: 1.21.4 + resolution: "@backstage/plugin-catalog-react@npm:1.21.4" dependencies: "@backstage/catalog-client": ^1.12.1 "@backstage/catalog-model": ^1.7.6 - "@backstage/core-compat-api": ^0.5.4 - "@backstage/core-components": ^0.18.3 - "@backstage/core-plugin-api": ^1.12.0 + "@backstage/core-compat-api": ^0.5.5 + "@backstage/core-components": ^0.18.4 + "@backstage/core-plugin-api": ^1.12.1 "@backstage/errors": ^1.2.7 - "@backstage/frontend-plugin-api": ^0.13.0 - "@backstage/frontend-test-utils": ^0.4.1 - "@backstage/integration-react": ^1.2.12 + "@backstage/frontend-plugin-api": ^0.13.2 + "@backstage/frontend-test-utils": ^0.4.2 + "@backstage/integration-react": ^1.2.13 "@backstage/plugin-catalog-common": ^1.1.7 "@backstage/plugin-permission-common": ^0.9.3 - "@backstage/plugin-permission-react": ^0.4.38 + "@backstage/plugin-permission-react": ^0.4.39 "@backstage/types": ^1.2.2 "@backstage/version-bridge": ^1.0.11 "@material-ui/core": ^4.12.2 @@ -3218,7 +3216,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 456dc21004d4b346d102ee5b18a94b827b16f17efb1991570083131acb83478a6fcdddcae1e21678c90f78f797928f1fa6ed998bd2896aaf2cb3a1178d596436 + checksum: cc76f940434f1f0554abb81fae5ad9db61ca075ab10173055cbeef9f9e249181d12e3a3fb7696b7d95996f59746dfc3d0da04d3af9f1fe186c159382a088ca88 languageName: node linkType: hard @@ -3285,16 +3283,16 @@ __metadata: languageName: node linkType: hard -"@backstage/plugin-home-react@npm:^0.1.27, @backstage/plugin-home-react@npm:^0.1.29, @backstage/plugin-home-react@npm:^0.1.32": - version: 0.1.32 - resolution: "@backstage/plugin-home-react@npm:0.1.32" +"@backstage/plugin-home-react@npm:^0.1.27, @backstage/plugin-home-react@npm:^0.1.32, @backstage/plugin-home-react@npm:^0.1.33": + version: 0.1.33 + resolution: "@backstage/plugin-home-react@npm:0.1.33" dependencies: - "@backstage/core-components": ^0.18.3 - "@backstage/core-plugin-api": ^1.12.0 - "@backstage/frontend-plugin-api": ^0.13.0 + "@backstage/core-components": ^0.18.4 + "@backstage/core-plugin-api": ^1.12.1 + "@backstage/frontend-plugin-api": ^0.13.2 "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 - "@rjsf/utils": 5.23.2 + "@rjsf/utils": 5.24.13 peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 react: ^17.0.0 || ^18.0.0 @@ -3303,32 +3301,31 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: c77c5f5631f8192e2a12eb552c1210bb1a4265d045162019c2d8ec7e842eaa4d698b4ba8b9a195202be3cdf29f654b1c491d529452148b6d7c1f42367312ab1a + checksum: 30984f004ff1ffa6a822e7a9e3fa662399f83eb6bc8b7a33cc925de3143ffc83fbf09f042217f7d002ce4346b644cd568d14bec3c3c2af015e918130bbc94cdc languageName: node linkType: hard -"@backstage/plugin-home@npm:^0.8.11": - version: 0.8.14 - resolution: "@backstage/plugin-home@npm:0.8.14" +"@backstage/plugin-home@npm:^0.8.14": + version: 0.8.15 + resolution: "@backstage/plugin-home@npm:0.8.15" dependencies: "@backstage/catalog-client": ^1.12.1 "@backstage/catalog-model": ^1.7.6 "@backstage/config": ^1.3.6 - "@backstage/core-app-api": ^1.19.2 - "@backstage/core-compat-api": ^0.5.4 - "@backstage/core-components": ^0.18.3 - "@backstage/core-plugin-api": ^1.12.0 - "@backstage/frontend-plugin-api": ^0.13.0 - "@backstage/plugin-catalog-react": ^1.21.3 - "@backstage/plugin-home-react": ^0.1.32 - "@backstage/theme": ^0.7.0 + "@backstage/core-app-api": ^1.19.3 + "@backstage/core-components": ^0.18.4 + "@backstage/core-plugin-api": ^1.12.1 + "@backstage/frontend-plugin-api": ^0.13.2 + "@backstage/plugin-catalog-react": ^1.21.4 + "@backstage/plugin-home-react": ^0.1.33 + "@backstage/theme": ^0.7.1 "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.61 - "@rjsf/core": 5.23.2 - "@rjsf/material-ui": 5.23.2 - "@rjsf/utils": 5.23.2 - "@rjsf/validator-ajv8": 5.23.2 + "@rjsf/core": 5.24.13 + "@rjsf/material-ui": 5.24.13 + "@rjsf/utils": 5.24.13 + "@rjsf/validator-ajv8": 5.24.13 lodash: ^4.17.21 luxon: ^3.4.3 react-grid-layout: 1.3.4 @@ -3343,7 +3340,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 1e5ececdcca0aaa6bd9c093f670e509c3e58324163021460bbf859c02f020601a96a2349062682f2801ecc51435c07d780c5878096f9544917cdc6334705dd05 + checksum: 423e052fdb85ecef6347dbde12c6d1bd5097bced18bfefd52bec4249f92e77db3cb0f9b2ee5b36e069c59d795f1a22f9e4f4a6be02fb5e00c657c9ce3463cfa6 languageName: node linkType: hard @@ -3559,12 +3556,12 @@ __metadata: languageName: node linkType: hard -"@backstage/plugin-permission-react@npm:^0.4.36, @backstage/plugin-permission-react@npm:^0.4.38": - version: 0.4.38 - resolution: "@backstage/plugin-permission-react@npm:0.4.38" +"@backstage/plugin-permission-react@npm:^0.4.36, @backstage/plugin-permission-react@npm:^0.4.38, @backstage/plugin-permission-react@npm:^0.4.39": + version: 0.4.39 + resolution: "@backstage/plugin-permission-react@npm:0.4.39" dependencies: "@backstage/config": ^1.3.6 - "@backstage/core-plugin-api": ^1.12.0 + "@backstage/core-plugin-api": ^1.12.1 "@backstage/plugin-permission-common": ^0.9.3 swr: ^2.0.0 peerDependencies: @@ -3575,7 +3572,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: a374dc0dc83ce42e99207c98cc2d198104b3cf6ff2c4205495581d92f15f5a311ae1c0189dd6292691f3d914707faa846109579545583a16076d9672832f23e7 + checksum: 513b468cfcc42e9c10ab7adeabd4f8076f1983172f43aec0d4f7a6f9b5da75d996a47c896c7518f9ef5d84e2211ad1c0f56b6167b5f4c69e3e49add7abe5be75 languageName: node linkType: hard @@ -4060,7 +4057,7 @@ __metadata: languageName: node linkType: hard -"@backstage/plugin-search-react@npm:^1.10.0, @backstage/plugin-search-react@npm:^1.9.3": +"@backstage/plugin-search-react@npm:^1.10.0": version: 1.10.0 resolution: "@backstage/plugin-search-react@npm:1.10.0" dependencies: @@ -4313,7 +4310,7 @@ __metadata: languageName: node linkType: hard -"@backstage/plugin-user-settings@npm:^0.8.25, @backstage/plugin-user-settings@npm:^0.8.29": +"@backstage/plugin-user-settings@npm:^0.8.29": version: 0.8.29 resolution: "@backstage/plugin-user-settings@npm:0.8.29" dependencies: @@ -4419,16 +4416,16 @@ __metadata: languageName: node linkType: hard -"@backstage/test-utils@npm:^1.7.13": - version: 1.7.13 - resolution: "@backstage/test-utils@npm:1.7.13" +"@backstage/test-utils@npm:^1.7.13, @backstage/test-utils@npm:^1.7.14": + version: 1.7.14 + resolution: "@backstage/test-utils@npm:1.7.14" dependencies: "@backstage/config": ^1.3.6 - "@backstage/core-app-api": ^1.19.2 - "@backstage/core-plugin-api": ^1.12.0 + "@backstage/core-app-api": ^1.19.3 + "@backstage/core-plugin-api": ^1.12.1 "@backstage/plugin-permission-common": ^0.9.3 - "@backstage/plugin-permission-react": ^0.4.38 - "@backstage/theme": ^0.7.0 + "@backstage/plugin-permission-react": ^0.4.39 + "@backstage/theme": ^0.7.1 "@backstage/types": ^1.2.2 "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 @@ -4444,7 +4441,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: fdc2e9abffb09ff3dd47e0a00557a591e403e2283ecd2a3e542c7602bc8aff42d5c128de139ad6caf554410f160922d6e7ebeb63b8e802675d4a08891b901844 + checksum: afc74ce13927e1f7a5d72582468b6d2b3cb1fba3f648999dbfabef8a2337867ab687ee514deaed5ad285d556ff8734fd8f054058db0de7eb7d354685e807f11c languageName: node linkType: hard @@ -4468,9 +4465,9 @@ __metadata: languageName: node linkType: hard -"@backstage/theme@npm:^0.7.0": - version: 0.7.0 - resolution: "@backstage/theme@npm:0.7.0" +"@backstage/theme@npm:^0.7.0, @backstage/theme@npm:^0.7.1": + version: 0.7.1 + resolution: "@backstage/theme@npm:0.7.1" dependencies: "@emotion/react": ^11.10.5 "@emotion/styled": ^11.10.5 @@ -4484,7 +4481,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: c6cc3c8bdbeb4fde673e6062242ca1d840edb772b04999e68a42cd731673d0422948aa0b89db4ec4971973175966e528ddfa14f4f009614636c45a2cc3719ba0 + checksum: 4ce9c47d4e89dafb85e5dc8f2445013f0e9431e54b5b06aa116d4e5e68417fd1a89b1f3e52ffa135b4419350e83b941ff09731a2fd8398fdb0fd5e9a4f25a270 languageName: node linkType: hard @@ -11027,31 +11024,31 @@ __metadata: languageName: node linkType: hard -"@red-hat-developer-hub/backstage-plugin-dynamic-home-page@npm:^1.9.2": - version: 1.9.2 - resolution: "@red-hat-developer-hub/backstage-plugin-dynamic-home-page@npm:1.9.2" +"@red-hat-developer-hub/backstage-plugin-dynamic-home-page@npm:^1.10.2": + version: 1.10.2 + resolution: "@red-hat-developer-hub/backstage-plugin-dynamic-home-page@npm:1.10.2" dependencies: - "@backstage/catalog-client": ^1.11.0 - "@backstage/catalog-model": ^1.7.5 - "@backstage/core-components": ^0.17.5 - "@backstage/core-plugin-api": ^1.10.9 - "@backstage/plugin-catalog-react": ^1.20.1 - "@backstage/plugin-home": ^0.8.11 - "@backstage/plugin-home-react": ^0.1.29 - "@backstage/plugin-search-react": ^1.9.3 - "@backstage/plugin-user-settings": ^0.8.25 - "@backstage/theme": ^0.6.8 + "@backstage/catalog-client": ^1.12.1 + "@backstage/catalog-model": ^1.7.6 + "@backstage/core-components": ^0.18.3 + "@backstage/core-plugin-api": ^1.12.0 + "@backstage/plugin-catalog-react": ^1.21.3 + "@backstage/plugin-home": ^0.8.14 + "@backstage/plugin-home-react": ^0.1.32 + "@backstage/plugin-search-react": ^1.10.0 + "@backstage/plugin-user-settings": ^0.8.29 + "@backstage/theme": ^0.7.0 "@mui/icons-material": 5.18.0 "@mui/material": 5.18.0 "@mui/styles": 5.18.0 - "@scalprum/react-core": 0.9.5 - react-grid-layout: 1.5.2 + "@scalprum/react-core": 0.11.1 + react-grid-layout: 1.5.3 react-use: 17.6.0 - tss-react: 4.9.19 + tss-react: 4.9.20 peerDependencies: react: 16.13.1 || ^17.0.0 || ^18.2.0 - react-router-dom: 6.30.1 - checksum: f3ddeb3ed84fc7e2a2c8662d94804a091b278ddccc9317e7d86998fe69b65d63c7149dffc9e0c6d87eae24aec60b31aab414657edd5c984d998514350fef6990 + react-router-dom: 6.30.2 + checksum: 72fd148a0651f73b51c949125e12d53e1026ccef74121f91a239c1d27d73c15877aeff9a5d94b9345b267df1eec2fe8e28ef4b83aa05fcde78b228970624813a languageName: node linkType: hard @@ -11319,7 +11316,7 @@ __metadata: languageName: node linkType: hard -"@rjsf/core@npm:^5.21.2": +"@rjsf/core@npm:5.24.13, @rjsf/core@npm:^5.21.2": version: 5.24.13 resolution: "@rjsf/core@npm:5.24.13" dependencies: @@ -11347,6 +11344,19 @@ __metadata: languageName: node linkType: hard +"@rjsf/material-ui@npm:5.24.13": + version: 5.24.13 + resolution: "@rjsf/material-ui@npm:5.24.13" + peerDependencies: + "@material-ui/core": ^4.12.3 + "@material-ui/icons": ^4.11.2 + "@rjsf/core": ^5.24.x + "@rjsf/utils": ^5.24.x + react: ^16.14.0 || >=17 + checksum: 41ef41817397ad52676c6d721157a6eef9f76d67729207ccea750d17621b6fdeed6eb928835e5249a1b1ae6296ed3d134b498d9e5be2c170acbb0d2447237ddc + languageName: node + linkType: hard + "@rjsf/mui@npm:^5.21.2": version: 5.24.13 resolution: "@rjsf/mui@npm:5.24.13" @@ -11377,7 +11387,7 @@ __metadata: languageName: node linkType: hard -"@rjsf/utils@npm:^5.21.2": +"@rjsf/utils@npm:5.24.13, @rjsf/utils@npm:^5.21.2": version: 5.24.13 resolution: "@rjsf/utils@npm:5.24.13" dependencies: @@ -11406,7 +11416,7 @@ __metadata: languageName: node linkType: hard -"@rjsf/validator-ajv8@npm:^5.21.2": +"@rjsf/validator-ajv8@npm:5.24.13, @rjsf/validator-ajv8@npm:^5.21.2": version: 5.24.13 resolution: "@rjsf/validator-ajv8@npm:5.24.13" dependencies: @@ -11961,16 +11971,6 @@ __metadata: languageName: node linkType: hard -"@scalprum/core@npm:^0.8.3": - version: 0.8.3 - resolution: "@scalprum/core@npm:0.8.3" - dependencies: - "@openshift/dynamic-plugin-sdk": ^5.0.1 - tslib: ^2.6.2 - checksum: 31d1d376c795d47cfc7c4cf84d262989f7a62e3c536aedff03cfa6e23bf969713f70f0e3c9035fe46b280a5133c85c416f1cd2b46c98f9822080ac9d7c752819 - languageName: node - linkType: hard - "@scalprum/core@npm:^0.9.0": version: 0.9.0 resolution: "@scalprum/core@npm:0.9.0" @@ -11995,20 +11995,6 @@ __metadata: languageName: node linkType: hard -"@scalprum/react-core@npm:0.9.5": - version: 0.9.5 - resolution: "@scalprum/react-core@npm:0.9.5" - dependencies: - "@openshift/dynamic-plugin-sdk": ^5.0.1 - "@scalprum/core": ^0.8.3 - lodash: ^4.17.0 - peerDependencies: - react: ">=16.8.0 || >=17.0.0 || ^18.0.0" - react-dom: ">=16.8.0 || >=17.0.0 || ^18.0.0" - checksum: 824cc4c53437a3d0da9a760fefa31a6aba436caef7b2cad69838fb43bd19b6301f90d60e33e823628596ef5e7174a208ba0e67ab129688cf122771fc101b0598 - languageName: node - linkType: hard - "@scarf/scarf@npm:=1.4.0": version: 1.4.0 resolution: "@scarf/scarf@npm:1.4.0" @@ -15801,7 +15787,7 @@ __metadata: "@material-ui/icons": ^4.9.1 "@openshift/dynamic-plugin-sdk": ^5.0.1 "@playwright/test": ^1.56.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 @@ -30392,9 +30378,9 @@ __metadata: languageName: node linkType: hard -"react-grid-layout@npm:1.5.2": - version: 1.5.2 - resolution: "react-grid-layout@npm:1.5.2" +"react-grid-layout@npm:1.5.3": + version: 1.5.3 + resolution: "react-grid-layout@npm:1.5.3" dependencies: clsx: ^2.1.1 fast-equals: ^4.0.3 @@ -30405,7 +30391,7 @@ __metadata: peerDependencies: react: ">= 16.3.0" react-dom: ">= 16.3.0" - checksum: 8beb3a64218168fe25c3f33797f5db03e58a021b4ff863b3ba2186e824a09f0c5409814d248aad3844bc8679bb7492070e82d9d7e899ee5196f3151b9c4177e3 + checksum: 38131e9e5dca5dad8d6ddd75fbc4a6830a41b27b086cef420c16898bafbb58d44a88a48a0e7eb913ce76831531a6d466414d84b464a99fb1705ddc9d5f0675f0 languageName: node linkType: hard @@ -34098,9 +34084,9 @@ __metadata: languageName: node linkType: hard -"tss-react@npm:4.9.19": - version: 4.9.19 - resolution: "tss-react@npm:4.9.19" +"tss-react@npm:4.9.20": + version: 4.9.20 + resolution: "tss-react@npm:4.9.20" dependencies: "@emotion/cache": "*" "@emotion/serialize": "*" @@ -34116,7 +34102,7 @@ __metadata: optional: true "@mui/material": optional: true - checksum: 1e29114220d26aa3a30c05f372f61bd876a0a4eb5482b8cd16a13c9783d1ad5c33bc15cf379d467965dc98ad7c528c9165c816c26958032761dea05296e78466 + checksum: ef2a62b4c3c07239d6cdb3d2ab855efc6c2469423e89381d530ca4fc62cbac4b9af8e8196539f02ed4e06918f615ceab2834689af4b97a47d5275295ab399af0 languageName: node linkType: hard
Test Content