From: David Cho-Lerat Date: Mon, 24 Apr 2023 14:59:24 +0000 (+0200) Subject: SONAR-19026 Adapt the Graph component to MIUI X-Git-Tag: 10.1.0.73491~410 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=ad0fb36113ab1e16aeaef3404bbbc8b6cbd5a12d;p=sonarqube.git SONAR-19026 Adapt the Graph component to MIUI --- diff --git a/server/sonar-web/design-system/src/components/NewCodeLegend.tsx b/server/sonar-web/design-system/src/components/NewCodeLegend.tsx new file mode 100644 index 00000000000..a6342d22413 --- /dev/null +++ b/server/sonar-web/design-system/src/components/NewCodeLegend.tsx @@ -0,0 +1,52 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import styled from '@emotion/styled'; +import classNames from 'classnames'; +import tw from 'twin.macro'; +import { themeColor } from '../helpers/theme'; + +export const NewCodeLegendIcon = styled.span` + ${tw`sw-align-middle`} + ${tw`sw-box-border`} + ${tw`sw-h-3`} + ${tw`sw-inline-block`} + ${tw`sw-w-3`} + background-color: ${themeColor('newCodeLegend')}; + border: 1px solid ${themeColor('newCodeLegendBorder')}; +`; + +const NewCodeLegendText = styled.span` + ${tw`sw-align-middle`} + ${tw`sw-body-sm`} + ${tw`sw-ml-1`} + color: ${themeColor('graphCursorLineColor')}; +`; + +export function NewCodeLegend(props: { className?: string; text: string }) { + const { className, text } = props; + + return ( + + + {text} + + ); +} diff --git a/server/sonar-web/design-system/src/components/__tests__/NewCodeLegend-test.tsx b/server/sonar-web/design-system/src/components/__tests__/NewCodeLegend-test.tsx new file mode 100644 index 00000000000..a11dcac71d7 --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/NewCodeLegend-test.tsx @@ -0,0 +1,34 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { screen } from '@testing-library/react'; +import tailwindBaseConfig from '../../../../tailwind.base.config'; +import { render } from '../../helpers/testUtils'; +import { NewCodeLegend } from '../NewCodeLegend'; + +it('should render NewCodeLegend', () => { + render(); + + expect(screen.getByText('the text')).toHaveStyle({ + 'font-size': tailwindBaseConfig.theme.fontSize.sm[0], + 'line-height': tailwindBaseConfig.theme.fontSize.sm[1], + 'margin-left': tailwindBaseConfig.theme.spacing[1], + }); +}); diff --git a/server/sonar-web/design-system/src/components/index.ts b/server/sonar-web/design-system/src/components/index.ts index 93058100cf3..41748cd06e9 100644 --- a/server/sonar-web/design-system/src/components/index.ts +++ b/server/sonar-web/design-system/src/components/index.ts @@ -40,6 +40,7 @@ export * from './MainMenu'; export * from './MainMenuItem'; export * from './MetricsRatingBadge'; export * from './NavBarTabs'; +export * from './NewCodeLegend'; export { QualityGateIndicator } from './QualityGateIndicator'; export * from './SizeIndicator'; export * from './SonarQubeLogo'; diff --git a/server/sonar-web/design-system/src/helpers/index.ts b/server/sonar-web/design-system/src/helpers/index.ts index 427c828d24f..5e62e8b766f 100644 --- a/server/sonar-web/design-system/src/helpers/index.ts +++ b/server/sonar-web/design-system/src/helpers/index.ts @@ -21,3 +21,4 @@ export * from './colors'; export * from './constants'; export * from './positioning'; +export * from './theme'; diff --git a/server/sonar-web/design-system/src/index.ts b/server/sonar-web/design-system/src/index.ts index c8e853ca7a1..9c4c8f50bfb 100644 --- a/server/sonar-web/design-system/src/index.ts +++ b/server/sonar-web/design-system/src/index.ts @@ -21,4 +21,5 @@ export * from './components'; export * from './helpers'; export * from './theme'; +export * from './types/measures'; export * from './types/theme'; diff --git a/server/sonar-web/design-system/src/theme/index.ts b/server/sonar-web/design-system/src/theme/index.ts index 6b8c84a5721..6b5303116b8 100644 --- a/server/sonar-web/design-system/src/theme/index.ts +++ b/server/sonar-web/design-system/src/theme/index.ts @@ -17,4 +17,6 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -export { default as lightTheme } from './light'; + +export * from './light'; +export * from './withTheme'; diff --git a/server/sonar-web/design-system/src/theme/light.ts b/server/sonar-web/design-system/src/theme/light.ts index df8bb271834..8ef9b7d1d08 100644 --- a/server/sonar-web/design-system/src/theme/light.ts +++ b/server/sonar-web/design-system/src/theme/light.ts @@ -43,7 +43,7 @@ const danger = { darker: COLORS.red[800], }; -const lightTheme = { +export const lightTheme = { id: 'light-theme', highlightTheme: 'atom-one-light.css', logo: 'sonarcloud-logo-black.svg', @@ -381,10 +381,10 @@ const lightTheme = { // graph - chart graphPointCircleColor: COLORS.white, - 'graphLineColor.0': COLORS.blue[500], - 'graphLineColor.1': COLORS.blue[700], + 'graphLineColor.0': COLORS.blue[700], + 'graphLineColor.1': COLORS.blue[500], 'graphLineColor.2': COLORS.blue[300], - 'graphLineColor.3': COLORS.blue[900], + 'graphLineColor.3': COLORS.blue[800], graphGridColor: COLORS.grey[50], graphCursorLineColor: COLORS.blueGrey[400], newCodeHighlight: COLORS.indigo[300], @@ -441,9 +441,9 @@ const lightTheme = { 'bubble.4': [...COLORS.orange[500], 0.3], 'bubble.5': [...COLORS.red[500], 0.3], - // leak legend - leakLegend: [...COLORS.indigo[300], 0.15], - leakLegendBorder: COLORS.indigo[100], + // new code legend + newCodeLegend: [...COLORS.indigo[300], 0.15], + newCodeLegendBorder: COLORS.indigo[200], // hotspot hotspotStatus: COLORS.blueGrey[25], @@ -744,5 +744,3 @@ const lightTheme = { GitLabPipeline: '/images/alms/gitlab.svg', }, }; - -export default lightTheme; diff --git a/server/sonar-web/design-system/src/theme/withTheme.tsx b/server/sonar-web/design-system/src/theme/withTheme.tsx new file mode 100644 index 00000000000..64e657fc57e --- /dev/null +++ b/server/sonar-web/design-system/src/theme/withTheme.tsx @@ -0,0 +1,36 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { useTheme } from '@emotion/react'; +import { Theme } from '../types/theme'; + +export interface ThemeProp { + theme: Theme; +} + +export function withTheme

( + WrappedComponent: React.ComponentType

+): React.ComponentType

{ + return function WrappedComponentWithTheme(props: P) { + const theme = useTheme(); + + return ; + }; +} diff --git a/server/sonar-web/src/main/js/app/styles/components/page.css b/server/sonar-web/src/main/js/app/styles/components/page.css index 64ecdd667bc..966a7cc80c5 100644 --- a/server/sonar-web/src/main/js/app/styles/components/page.css +++ b/server/sonar-web/src/main/js/app/styles/components/page.css @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + .white-page { background-color: #fff !important; } @@ -29,7 +30,6 @@ } .page { - position: relative; z-index: var(--normalZIndex); padding: 10px 20px; } diff --git a/server/sonar-web/src/main/js/apps/overview/branches/ActivityPanel.tsx b/server/sonar-web/src/main/js/apps/overview/branches/ActivityPanel.tsx index 41edf6d1698..8b1060beeba 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/ActivityPanel.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/ActivityPanel.tsx @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import * as React from 'react'; import GraphsHeader from '../../../components/activity-graph/GraphsHeader'; import GraphsHistory from '../../../components/activity-graph/GraphsHistory'; @@ -72,15 +73,18 @@ export function ActivityPanel(props: ActivityPanelProps) { const series = generateSeries(measuresHistory, graph, metrics, displayedMetrics); const graphs = splitSeriesInGraphs(series, MAX_GRAPH_NB, MAX_SERIES_PER_GRAPH); let shownLeakPeriodDate; + if (leakPeriodDate !== undefined) { const startDate = measuresHistory.reduce((oldest: Date, { history }) => { if (history.length > 0) { const date = parseDate(history[0].date); + return oldest.getTime() > date.getTime() ? date : oldest; - } else { - return oldest; } + + return oldest; }, new Date()); + shownLeakPeriodDate = startDate.getTime() > leakPeriodDate.getTime() ? startDate : leakPeriodDate; } @@ -94,7 +98,7 @@ export function ActivityPanel(props: ActivityPanelProps) {

-
+
{ let type; if (/(coverage|duplication)$/.test(key)) { - type = 'PERCENT'; + type = MetricType.Percent; } else if (/_rating$/.test(key)) { - type = 'RATING'; + type = MetricType.Rating; } else { - type = 'INT'; + type = MetricType.Integer; } metrics.push(mockMetric({ key, id: key, name: key, type })); measures.push( @@ -350,10 +350,20 @@ it.each([ it('should correctly handle graph type storage', async () => { renderBranchOverview(); + expect(getActivityGraph).toHaveBeenCalledWith(BRANCH_OVERVIEW_ACTIVITY_GRAPH, 'foo'); - const select = await screen.findByLabelText('project_activity.graphs.choose_type'); - await selectEvent.select(select, `project_activity.graphs.${GraphType.issues}`); + const dropdownButton = await screen.findByLabelText('project_activity.graphs.choose_type'); + + await userEvent.click(dropdownButton); + + const issuesItem = await screen.findByRole('menuitem', { + name: `project_activity.graphs.${GraphType.issues}`, + }); + + expect(issuesItem).toBeInTheDocument(); + + await userEvent.click(issuesItem); expect(saveActivityGraph).toHaveBeenCalledWith( BRANCH_OVERVIEW_ACTIVITY_GRAPH, diff --git a/server/sonar-web/src/main/js/apps/overview/styles.css b/server/sonar-web/src/main/js/apps/overview/styles.css index 19dfba11686..11cefef0cf9 100644 --- a/server/sonar-web/src/main/js/apps/overview/styles.css +++ b/server/sonar-web/src/main/js/apps/overview/styles.css @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + .overview { animation: fadeIn 0.5s forwards; } @@ -267,21 +268,7 @@ .overview-panel .activity-graph-legends { justify-content: right; - margin-top: -30px; -} - -.overview-panel .activity-graph-new-code-legend { - position: relative; - z-index: var(--aboveNormalZIndex); - width: 12px; - overflow: hidden; - margin-top: 1px; - margin-left: calc(2 * var(--gridSize)); - text-indent: -9999px; -} - -.overview-panel .activity-graph-new-code-legend::after { - margin: 0; + margin-top: -38px; } .overview-analysis { diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx index d18daecdff6..87132b7d8bf 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { keyBy, times } from 'lodash'; @@ -66,6 +67,7 @@ beforeEach(() => { jest.clearAllMocks(); projectActivityHandler.reset(); timeMachineHandler.reset(); + timeMachineHandler.setMeasureHistory( [ MetricKey.bugs, @@ -97,6 +99,7 @@ describe('rendering', () => { it('should correctly show the baseline marker', async () => { const { ui } = getPageObject(); + renderProjectActivityAppContainer( mockComponent({ leakPeriodDate: parseDate('2017-03-01T22:00:00.000Z').toDateString(), @@ -105,6 +108,7 @@ describe('rendering', () => { ], }) ); + await ui.appLoaded(); expect(ui.baseline.get()).toBeInTheDocument(); @@ -112,6 +116,7 @@ describe('rendering', () => { it('should only show certain security hotspot-related metrics for a project', async () => { const { ui } = getPageObject(); + renderProjectActivityAppContainer( mockComponent({ breadcrumbs: [ @@ -130,6 +135,7 @@ describe('rendering', () => { 'should only show certain security hotspot-related metrics for a %s', async (qualifier) => { const { ui } = getPageObject(); + renderProjectActivityAppContainer( mockComponent({ qualifier, @@ -140,6 +146,7 @@ describe('rendering', () => { await ui.changeGraphType(GraphType.custom); await ui.openMetricsDropdown(); expect(ui.metricCheckbox(MetricKey.security_review_rating).get()).toBeInTheDocument(); + expect( ui.metricCheckbox(MetricKey.security_hotspots_reviewed).query() ).not.toBeInTheDocument(); @@ -152,6 +159,7 @@ describe('CRUD', () => { const { ui } = getPageObject(); const initialValue = '1.1-SNAPSHOT'; const updatedValue = '1.1--SNAPSHOT'; + renderProjectActivityAppContainer( mockComponent({ breadcrumbs: [ @@ -160,6 +168,7 @@ describe('CRUD', () => { configuration: { showHistory: true }, }) ); + await ui.appLoaded(); await ui.addVersionEvent('1.1.0.1', initialValue); @@ -178,6 +187,7 @@ describe('CRUD', () => { const { ui } = getPageObject(); const initialValue = 'Custom event name'; const updatedValue = 'Custom event updated name'; + renderProjectActivityAppContainer( mockComponent({ breadcrumbs: [ @@ -186,6 +196,7 @@ describe('CRUD', () => { configuration: { showHistory: true }, }) ); + await ui.appLoaded(); await act(async () => { @@ -204,6 +215,7 @@ describe('CRUD', () => { it('should correctly allow deletion of specific analyses', async () => { const { ui } = getPageObject(); + renderProjectActivityAppContainer( mockComponent({ breadcrumbs: [ @@ -212,6 +224,7 @@ describe('CRUD', () => { configuration: { showHistory: true }, }) ); + await ui.appLoaded(); // Most recent analysis is not deletable. @@ -231,6 +244,7 @@ describe('data loading', () => { it('should load all analyses', async () => { const count = 1000; + projectActivityHandler.setAnalysesList( times(count, (i) => { return mockAnalysis({ @@ -239,6 +253,7 @@ describe('data loading', () => { }); }) ); + const { ui } = getPageObject(); renderProjectActivityAppContainer(); await ui.appLoaded(); @@ -257,6 +272,7 @@ describe('data loading', () => { it('should correctly fetch the top level component when dealing with sub portfolios', async () => { const { ui } = getPageObject(); + renderProjectActivityAppContainer( mockComponent({ key: 'unknown', @@ -267,6 +283,7 @@ describe('data loading', () => { ], }) ); + await ui.appLoaded(); // If it didn't fail, it means we correctly queried for project "foo". @@ -319,6 +336,7 @@ describe('filtering', () => { }); }) ); + const { ui } = getPageObject(); renderProjectActivityAppContainer(); await ui.appLoaded(); @@ -347,9 +365,11 @@ describe('graph interactions', () => { await ui.appLoaded(); expect(ui.bugsPopupCell.query()).not.toBeInTheDocument(); + await act(async () => { await ui.showDetails('1.1.0.1'); }); + expect(ui.bugsPopupCell.get()).toBeInTheDocument(); }); @@ -386,6 +406,7 @@ describe('graph interactions', () => { function getPageObject() { const user = userEvent.setup(); + const ui = { // Graph types. graphTypeSelect: byLabelText('project_activity.graphs.choose_type'), @@ -426,7 +447,7 @@ function getPageObject() { // Misc. loading: byLabelText('loading'), baseline: byText('project_activity.new_code_period_start'), - bugsPopupCell: byRole('cell', { name: 'bugs' }), + bugsPopupCell: byRole('cell', { name: MetricKey.bugs }), }; return { @@ -438,65 +459,80 @@ function getPageObject() { expect(ui.loading.query()).not.toBeInTheDocument(); }); }, + async changeGraphType(type: GraphType) { await selectEvent.select(ui.graphTypeSelect.get(), [`project_activity.graphs.${type}`]); }, + async openMetricsDropdown() { await user.click(ui.addMetricBtn.get()); }, + async toggleMetric(metric: MetricKey) { await user.click(ui.metricCheckbox(metric).get()); }, + async closeMetricsDropdown() { await user.keyboard('{Escape}'); }, + async openCogMenu(id: string) { await user.click(ui.cogBtn(id).get()); }, + async deleteAnalysis(id: string) { await user.click(ui.cogBtn(id).get()); await user.click(ui.deleteAnalysisBtn.get()); await user.click(ui.deleteBtn.get()); }, + async addVersionEvent(id: string, value: string) { await user.click(ui.cogBtn(id).get()); await user.click(ui.addVersionEvenBtn.get()); await user.type(ui.nameInput.get(), value); await user.click(ui.saveBtn.get()); }, + async addCustomEvent(id: string, value: string) { await user.click(ui.cogBtn(id).get()); await user.click(ui.addCustomEventBtn.get()); await user.type(ui.nameInput.get(), value); await user.click(ui.saveBtn.get()); }, + async updateEvent(index: number, value: string) { await user.click(ui.editEventBtn.getAll()[index]); await user.clear(ui.nameInput.get()); await user.type(ui.nameInput.get(), value); await user.click(ui.changeBtn.get()); }, + async deleteEvent(index: number) { await user.click(ui.deleteEventBtn.getAll()[index]); await user.click(ui.deleteBtn.get()); }, + async showDetails(id: string) { await user.click(ui.seeDetailsBtn(id).get()); }, + async filterByCategory( category: ProjectAnalysisEventCategory | ApplicationAnalysisEventCategory ) { await selectEvent.select(ui.categorySelect.get(), [`event.category.${category}`]); }, + async setDateRange(from?: string, to?: string) { const dateInput = dateInputEvent(user); if (from) { await dateInput.pickDate(ui.fromDateInput.get(), parseDate(from)); } + if (to) { await dateInput.pickDate(ui.toDateInput.get(), parseDate(to)); } }, + async resetDateFilters() { await user.click(ui.resetDatesBtn.get()); }, diff --git a/server/sonar-web/src/main/js/components/activity-graph/GraphHistory.tsx b/server/sonar-web/src/main/js/components/activity-graph/GraphHistory.tsx index f724d84c71b..eee04c2270d 100644 --- a/server/sonar-web/src/main/js/components/activity-graph/GraphHistory.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/GraphHistory.tsx @@ -17,18 +17,19 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import * as React from 'react'; import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer'; -import AdvancedTimeline from '../../components/charts/AdvancedTimeline'; +import { AdvancedTimeline } from '../../components/charts/AdvancedTimeline'; import { translate } from '../../helpers/l10n'; import { formatMeasure, getShortType } from '../../helpers/measures'; import { MeasureHistory, ParsedAnalysis, Serie } from '../../types/project-activity'; -import { Button } from '../controls/buttons'; import ModalButton from '../controls/ModalButton'; +import { Button } from '../controls/buttons'; import DataTableModal from './DataTableModal'; import GraphsLegendCustom from './GraphsLegendCustom'; import GraphsLegendStatic from './GraphsLegendStatic'; -import GraphsTooltips from './GraphsTooltips'; +import { GraphsTooltips } from './GraphsTooltips'; import { getAnalysisEventsForDate } from './utils'; interface Props { @@ -88,11 +89,27 @@ export default class GraphHistory extends React.PureComponent { showAreas, graphDescription, } = this.props; + + const modalProp = ({ onClose }: { onClose: () => void }) => ( + + ); + const { tooltipIdx, tooltipXPos } = this.state; const events = getAnalysisEventsForDate(analyses, selectedDate); return ( -
+
{isCustom && this.props.removeCustomMetric ? ( ) : ( @@ -104,7 +121,6 @@ export default class GraphHistory extends React.PureComponent { {({ height, width }) => (
{
{canShowDataAsTable && ( - ( - - )} - > + {({ onClick }) => (