diff --git a/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.module.css b/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.module.css new file mode 100644 index 00000000000..ec8f3d7ff26 --- /dev/null +++ b/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.module.css @@ -0,0 +1,14 @@ +@layer components { + .header { + padding: var(--bui-space-4) var(--bui-space-4) var(--bui-space-4) var(--bui-space-5); + } + + .action { + margin: 0; + } + + .lastAnalyzed { + color: var(--bui-fg-secondary); + padding: var(--bui-space-2) 0; + } +} diff --git a/workspaces/sonarqube/.changeset/eighty-baboons-eat.md b/workspaces/sonarqube/.changeset/eighty-baboons-eat.md new file mode 100644 index 00000000000..63b8c2a2ca6 --- /dev/null +++ b/workspaces/sonarqube/.changeset/eighty-baboons-eat.md @@ -0,0 +1,5 @@ +--- +'@backstage-community/plugin-sonarqube': major +--- + +migrated the @backstage-community/plugin-sonarqube plugin from Material-UI (MUI) v4 to Backstage UI (BUI) v1.7.0, eliminating deprecated Material-UI dependencies while improving theming consistency and reducing bundle size. diff --git a/workspaces/sonarqube/plugins/sonarqube/package.json b/workspaces/sonarqube/plugins/sonarqube/package.json index 31eb9fb05fa..298dd93b070 100644 --- a/workspaces/sonarqube/plugins/sonarqube/package.json +++ b/workspaces/sonarqube/plugins/sonarqube/package.json @@ -68,9 +68,9 @@ "@backstage/frontend-app-api": "backstage:^", "@backstage/frontend-plugin-api": "backstage:^", "@backstage/plugin-catalog-react": "backstage:^", - "@material-ui/core": "^4.12.2", - "@material-ui/icons": "^4.9.1", - "@material-ui/styles": "^4.10.0", + "@backstage/ui": "^1.7.0", + "@remixicon/react": "^4.3.0", + "react-aria-components": "^1.4.0", "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0", "cross-fetch": "^4.0.0", "luxon": "^3.0.0", diff --git a/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/MetricInsights.module.css b/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/MetricInsights.module.css new file mode 100644 index 00000000000..a4f450a7794 --- /dev/null +++ b/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/MetricInsights.module.css @@ -0,0 +1,22 @@ +@layer components { + .badgeLabel { + color: var(--bui-fg-primary); + } + + .badgeCompact { + margin: 0; + height: 28px; + } + + .badgeError { + background-color: var(--bui-status-error); + } + + .badgeSuccess { + background-color: var(--bui-status-ok); + } + + .badgeUnknown { + background-color: var(--bui-bg-surface-3); + } +} diff --git a/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/MetricInsights.tsx b/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/MetricInsights.tsx index 3a963021140..35083549102 100644 --- a/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/MetricInsights.tsx +++ b/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/MetricInsights.tsx @@ -14,16 +14,18 @@ * limitations under the License. */ import { useMemo } from 'react'; -import Chip from '@material-ui/core/Chip'; -import { makeStyles } from '@material-ui/core/styles'; +import { Tag, Text } from '@backstage/ui'; +import { TooltipTrigger, Tooltip } from 'react-aria-components'; +import { + RiBugLine, + RiLockLine, + RiLockUnlockLine, + RiCheckLine, + RiAlertLine, + RiShieldLine, + RiExternalLinkLine, +} from '@remixicon/react'; import { defaultDuplicationRatings } from '../SonarQubeTable/types'; -import BugReport from '@material-ui/icons/BugReport'; -import Lock from '@material-ui/icons/Lock'; -import Typography from '@material-ui/core/Typography'; -import LockOpen from '@material-ui/icons/LockOpen'; -import SentimentVeryDissatisfied from '@material-ui/icons/SentimentVeryDissatisfied'; -import SentimentVerySatisfied from '@material-ui/icons/SentimentVerySatisfied'; -import Security from '@material-ui/icons/Security'; import { DateTime } from 'luxon'; import { Percentage } from './Percentage'; import { Rating } from './Rating'; @@ -32,8 +34,7 @@ import { Value } from './Value'; import { FindingSummary } from '@backstage-community/plugin-sonarqube-react'; import { useTranslationRef } from '@backstage/frontend-plugin-api'; import { sonarqubeTranslationRef } from '../../translation'; -import Tooltip from '@material-ui/core/Tooltip'; -import LinkIcon from '@material-ui/icons/Link'; +import styles from './MetricInsights.module.css'; type MetricInsightsProps = { value: FindingSummary | any; @@ -42,63 +43,47 @@ type MetricInsightsProps = { sonarQubeComponentKey?: string; }; -const useStyles = makeStyles(theme => ({ - badgeLabel: { - color: theme.palette.common.white, - }, - badgeCompact: { - margin: 0, - height: 28, - }, - badgeError: { - backgroundColor: theme.palette.error.main, - }, - badgeSuccess: { - backgroundColor: theme.palette.success.main, - }, - badgeUnknown: { - backgroundColor: theme.palette.grey[500], - }, -})); - export const QualityBadge = (props: MetricInsightsProps) => { - const classes = useStyles(); const { t } = useTranslationRef(sonarqubeTranslationRef); const { value } = props; let gateLabel: string = t('sonarQubeCard.qualityBadgeLabel.notComputed'); - let gateColor = classes.badgeUnknown; + let badgeClass = styles.badgeUnknown; let gateLinkToolTip = ''; + if (value?.metrics.alert_status) { const gatePassed = value?.metrics.alert_status === 'OK'; gateLabel = gatePassed ? t('sonarQubeCard.qualityBadgeLabel.gatePassed') : t('sonarQubeCard.qualityBadgeLabel.gateFailed'); - gateColor = gatePassed ? classes.badgeSuccess : classes.badgeError; + badgeClass = gatePassed ? styles.badgeSuccess : styles.badgeError; } - let clickableAttrs = {}; + if (value.projectUrl) { gateLinkToolTip = t('sonarQubeCard.qualityBadgeTooltip'); - clickableAttrs = { - component: 'a', - href: value.projectUrl, - target: '_blank', - rel: 'noopener noreferrer', - clickable: true, - }; } + const qualityBadge = ( - - : undefined} - /> - + + {value.projectUrl ? : null} + {gateLabel} + + ); + + if (!value.projectUrl) { + return qualityBadge; + } + + return ( + + + {qualityBadge} + + {gateLinkToolTip} + ); - return qualityBadge; }; export const BugReportRatingCard = (props: MetricInsightsProps) => { @@ -106,7 +91,7 @@ export const BugReportRatingCard = (props: MetricInsightsProps) => { return ( } + titleIcon={} title={title} link={value.getIssuesUrl('BUG')} leftSlot={} @@ -121,7 +106,11 @@ export const VulnerabilitiesRatingCard = (props: MetricInsightsProps) => { : + value.metrics.vulnerabilities === '0' ? ( + + ) : ( + + ) } title={title} link={value.getIssuesUrl('VULNERABILITY')} @@ -140,9 +129,9 @@ export const CodeSmellsRatingCard = (props: MetricInsightsProps) => { compact={props.compact} titleIcon={ value.metrics.code_smells === '0' ? ( - + ) : ( - + ) } title={title} @@ -161,7 +150,7 @@ export const HotspotsReviewed = (props: MetricInsightsProps) => { value.metrics.security_review_rating && ( } + titleIcon={} title={title} link={value.getSecurityHotspotsUrl()} leftSlot={ @@ -249,13 +238,13 @@ export const NoSonarQubeCard = (props: MetricInsightsProps) => { const { value, sonarQubeComponentKey } = props; const { t } = useTranslationRef(sonarqubeTranslationRef); return ( - + {value?.isSonarQubeAnnotationEnabled && t('sonarQubeCard.noSonarQubeError.hasAnnotation', { project: sonarQubeComponentKey || '', })} {!value?.isSonarQubeAnnotationEnabled && t('sonarQubeCard.noSonarQubeError.noAnnotation')} - + ); }; diff --git a/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/Percentage.module.css b/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/Percentage.module.css new file mode 100644 index 00000000000..da5bf192151 --- /dev/null +++ b/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/Percentage.module.css @@ -0,0 +1,11 @@ +@layer components { + :root { + --sonarqube-percentage-ok: var(--bui-status-ok, #1db679); + --sonarqube-percentage-error: var(--bui-status-error, #e82c3c); + } + + .root { + height: var(--bui-space-6); + width: var(--bui-space-6); + } +} diff --git a/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/Percentage.tsx b/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/Percentage.tsx index b3d48563605..17358f03669 100644 --- a/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/Percentage.tsx +++ b/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/Percentage.tsx @@ -14,30 +14,32 @@ * limitations under the License. */ -import { makeStyles } from '@material-ui/core/styles'; -import { useTheme } from '@material-ui/core/styles'; import { Circle } from 'rc-progress'; - -const useStyles = makeStyles(theme => ({ - root: { - height: theme.spacing(3), - width: theme.spacing(3), - }, -})); +import styles from './Percentage.module.css'; export const Percentage = ({ value }: { value?: string }) => { - const classes = useStyles(); - const theme = useTheme(); + // Use theme tokens from CSS variables, fallback to SonarQube default colors + const getStyleValue = (propertyName: string, fallback: string): string => { + if (typeof window === 'undefined') return fallback; + const colorValue = window + .getComputedStyle(document.documentElement) + .getPropertyValue(propertyName) + .trim(); + return colorValue || fallback; + }; + + const okColor = getStyleValue('--sonarqube-percentage-ok', '#1db679'); + const errorColor = getStyleValue('--sonarqube-percentage-error', '#e82c3c'); return ( ); }; diff --git a/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/Rating.module.css b/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/Rating.module.css new file mode 100644 index 00000000000..79a6d130e44 --- /dev/null +++ b/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/Rating.module.css @@ -0,0 +1,81 @@ +@layer components { + :root { + --sonarqube-rating-a: var(--bui-status-ok, #1db679); + --sonarqube-rating-b: #a3d566; + --sonarqube-rating-c: var(--bui-status-pending, #f2cc0f); + --sonarqube-rating-d: var(--bui-status-warning, #f88d3d); + --sonarqube-rating-e: var(--bui-status-error, #e82c3c); + } + + .ratingDefault { + height: var(--bui-space-6); + width: var(--bui-space-6); + color: var(--bui-fg-primary); + background-color: var(--bui-bg-surface-3); + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--bui-radius-2); + font-weight: 600; + } + + .ratingA { + height: var(--bui-space-6); + width: var(--bui-space-6); + color: var(--bui-white); + background-color: var(--sonarqube-rating-a); + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--bui-radius-2); + font-weight: 600; + } + + .ratingB { + height: var(--bui-space-6); + width: var(--bui-space-6); + color: var(--bui-white); + background-color: var(--sonarqube-rating-b); + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--bui-radius-2); + font-weight: 600; + } + + .ratingC { + height: var(--bui-space-6); + width: var(--bui-space-6); + color: var(--bui-white); + background-color: var(--sonarqube-rating-c); + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--bui-radius-2); + font-weight: 600; + } + + .ratingD { + height: var(--bui-space-6); + width: var(--bui-space-6); + color: var(--bui-white); + background-color: var(--sonarqube-rating-d); + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--bui-radius-2); + font-weight: 600; + } + + .ratingE { + height: var(--bui-space-6); + width: var(--bui-space-6); + color: var(--bui-white); + background-color: var(--sonarqube-rating-e); + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--bui-radius-2); + font-weight: 600; + } +} diff --git a/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/Rating.tsx b/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/Rating.tsx index bd0c049a38f..379d981ad04 100644 --- a/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/Rating.tsx +++ b/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/Rating.tsx @@ -14,45 +14,8 @@ * limitations under the License. */ -import Avatar from '@material-ui/core/Avatar'; -import { lighten, makeStyles } from '@material-ui/core/styles'; -import { CSSProperties } from '@material-ui/styles/withStyles'; import { useMemo } from 'react'; - -const useStyles = makeStyles(theme => { - const commonCardRating: CSSProperties = { - height: theme.spacing(3), - width: theme.spacing(3), - color: theme.palette.common.white, - }; - - return { - ratingDefault: { - ...commonCardRating, - background: theme.palette.status.aborted, - }, - ratingA: { - ...commonCardRating, - background: theme.palette.status.ok, - }, - ratingB: { - ...commonCardRating, - background: lighten(theme.palette.status.ok, 0.5), - }, - ratingC: { - ...commonCardRating, - background: theme.palette.status.pending, - }, - ratingD: { - ...commonCardRating, - background: theme.palette.status.warning, - }, - ratingE: { - ...commonCardRating, - background: theme.palette.error.main, - }, - }; -}); +import styles from './Rating.module.css'; export const Rating = ({ rating, @@ -61,51 +24,47 @@ export const Rating = ({ rating?: string; hideValue?: boolean; }) => { - const classes = useStyles(); - const ratingProp = useMemo(() => { switch (rating) { case '1.0': return { name: 'A', - className: classes.ratingA, + className: styles.ratingA, }; case '2.0': return { name: 'B', - className: classes.ratingB, + className: styles.ratingB, }; case '3.0': return { name: 'C', - className: classes.ratingC, + className: styles.ratingC, }; case '4.0': return { name: 'D', - className: classes.ratingD, + className: styles.ratingD, }; case '5.0': return { name: 'E', - className: classes.ratingE, + className: styles.ratingE, }; default: return { name: '', - className: classes.ratingDefault, + className: styles.ratingDefault, }; } - }, [classes, rating]); + }, [rating]); return ( - - {!hideValue && ratingProp.name} - +
{!hideValue && ratingProp.name}
); }; diff --git a/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/RatingCard.module.css b/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/RatingCard.module.css new file mode 100644 index 00000000000..eb10174da3c --- /dev/null +++ b/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/RatingCard.module.css @@ -0,0 +1,33 @@ +@layer components { + .root { + margin: var(--bui-space-2) 0; + min-width: 140px; + } + + .upper { + display: flex; + justify-content: center; + align-items: center; + gap: var(--bui-space-2); + } + + .cardTitle { + text-align: center; + margin-top: var(--bui-space-2); + } + + .wrapIcon { + display: inline-flex; + align-items: center; + gap: var(--bui-space-1); + vertical-align: baseline; + } + + .left { + display: flex; + } + + .right { + display: flex; + } +} diff --git a/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/RatingCard.tsx b/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/RatingCard.tsx index d9c89b3c75f..c88d4487f06 100644 --- a/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/RatingCard.tsx +++ b/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/RatingCard.tsx @@ -15,38 +15,9 @@ */ import { Link } from '@backstage/core-components'; -import Grid from '@material-ui/core/Grid'; -import Typography from '@material-ui/core/Typography'; -import { makeStyles } from '@material-ui/core/styles'; +import { Text, Flex, Box } from '@backstage/ui'; import { ReactNode } from 'react'; - -const useStyles = makeStyles(theme => { - return { - root: { - margin: theme.spacing(1, 0), - minWidth: '140px', - }, - upper: { - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - }, - cardTitle: { - textAlign: 'center', - }, - wrapIcon: { - display: 'inline-flex', - verticalAlign: 'baseline', - }, - left: { - display: 'flex', - }, - right: { - display: 'flex', - marginLeft: theme.spacing(0.5), - }, - }; -}); +import styles from './RatingCard.module.css'; export const RatingCard = ({ leftSlot, @@ -63,27 +34,26 @@ export const RatingCard = ({ link: string; compact?: boolean; }) => { - const classes = useStyles(); - return ( - - - - {leftSlot} - - - {rightSlot} - - - {compact || ( - - + + +
{leftSlot}
+
{rightSlot}
+
+ {!compact && ( + + {titleIcon} {title} -
-
+ + )} -
+ ); }; diff --git a/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.module.css b/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.module.css new file mode 100644 index 00000000000..01c3e918086 --- /dev/null +++ b/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.module.css @@ -0,0 +1,29 @@ +@layer components { + .header { + padding: var(--bui-space-4) var(--bui-space-4) var(--bui-space-4) var(--bui-space-5); + } + + .action { + margin: 0; + } + + .metricsContainer { + height: 100%; + } + + .metricsGrid { + display: flex; + flex-wrap: wrap; + justify-content: space-around; + width: 100%; + } + + .clearfix { + width: 100%; + } + + .lastAnalyzed { + color: var(--bui-fg-secondary); + padding: var(--bui-space-2) 0; + } +} diff --git a/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.tsx b/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.tsx index 79ad00670f4..b20683558ff 100644 --- a/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.tsx +++ b/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.tsx @@ -23,8 +23,7 @@ import { SONARQUBE_PROJECT_KEY_ANNOTATION, isSonarQubeAvailable, } from '@backstage-community/plugin-sonarqube-react'; -import Grid from '@material-ui/core/Grid'; -import { makeStyles } from '@material-ui/core/styles'; +import { Box, Flex } from '@backstage/ui'; import useAsync from 'react-use/esm/useAsync'; import { EmptyState, @@ -46,18 +45,7 @@ import { import { DuplicationRating } from '../SonarQubeTable/types'; import { useTranslationRef } from '@backstage/frontend-plugin-api'; import { sonarqubeTranslationRef } from '../../translation'; - -const useStyles = makeStyles(theme => ({ - header: { - padding: theme.spacing(2, 2, 2, 2.5), - }, - action: { - margin: 0, - }, - lastAnalyzed: { - color: theme.palette.text.secondary, - }, -})); +import styles from './SonarQubeCard.module.css'; /** @public */ export const SonarQubeCard = (props: { @@ -87,7 +75,6 @@ export const SonarQubeCard = (props: { } : undefined; - const classes = useStyles(); return ( ), classes: { - root: classes.header, - action: classes.action, + root: styles.header, + action: styles.action, }, }} > @@ -116,16 +103,12 @@ export const SonarQubeCard = (props: { {!loading && summaryFinding?.metrics && ( <> - - +
-
+
- - +
+ - - + + )} diff --git a/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/Value.module.css b/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/Value.module.css new file mode 100644 index 00000000000..49798401289 --- /dev/null +++ b/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/Value.module.css @@ -0,0 +1,10 @@ +@layer components { + .value { + font-size: 1.5rem; + font-weight: 500; + } + + .compact { + line-height: 1; + } +} diff --git a/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/Value.tsx b/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/Value.tsx index 5ac1d05530b..9575050a3a8 100644 --- a/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/Value.tsx +++ b/workspaces/sonarqube/plugins/sonarqube/src/components/SonarQubeCard/Value.tsx @@ -14,30 +14,16 @@ * limitations under the License. */ -import { makeStyles } from '@material-ui/core/styles'; -import Typography from '@material-ui/core/Typography'; - -const useStyles = makeStyles(theme => { - return { - value: { - fontSize: '1.5rem', - fontWeight: theme.typography.fontWeightMedium, - }, - compact: { - lineHeight: '1.0', - }, - }; -}); +import { Text } from '@backstage/ui'; +import styles from './Value.module.css'; export const Value = (props: { value?: string; compact?: boolean }) => { - const classes = useStyles(); return ( - {props.value} - + ); };