diff options
author | stanislavh <stanislav.honcharov@sonarsource.com> | 2024-11-22 11:12:52 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2024-11-22 20:03:10 +0000 |
commit | 6e89b695f15534c743b4b3e2d8eef295d1dfccdd (patch) | |
tree | 1dae6d54d420f995b430228d4061074861aad033 | |
parent | 664e392ebfedaf163c6b6cf9a2f60b37b3005955 (diff) | |
download | sonarqube-6e89b695f15534c743b4b3e2d8eef295d1dfccdd.tar.gz sonarqube-6e89b695f15534c743b4b3e2d8eef295d1dfccdd.zip |
SONAR-22309 Fix a11y issues on Activity page
11 files changed, 310 insertions, 279 deletions
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/Event.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/Event.tsx index 06d19395a9b..124763d4a57 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/Event.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/Event.tsx @@ -18,10 +18,16 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { + ButtonIcon, + ButtonSize, + ButtonVariety, + IconDelete, + IconEdit, +} from '@sonarsource/echoes-react'; import * as React from 'react'; -import { DestructiveIcon, InteractiveIcon, PencilIcon, TrashIcon } from '~design-system'; +import { useIntl } from 'react-intl'; import EventInner from '../../../components/activity-graph/EventInner'; -import Tooltip from '../../../components/controls/Tooltip'; import { translate } from '../../../helpers/l10n'; import { AnalysisEvent, ProjectAnalysisEventCategory } from '../../../types/project-activity'; import ChangeEventForm from './forms/ChangeEventForm'; @@ -36,6 +42,7 @@ export interface EventProps { function Event(props: Readonly<EventProps>) { const { analysisKey, event, canAdmin, isFirst } = props; + const intl = useIntl(); const [changing, setChanging] = React.useState(false); const [deleting, setDeleting] = React.useState(false); @@ -46,6 +53,15 @@ function Event(props: Readonly<EventProps>) { const canDelete = isOther || (isVersion && !isFirst); const showActions = canAdmin && (canChange || canDelete); + const editEventLabel = intl.formatMessage( + { id: 'project_activity.events.tooltip.edit' }, + { event: `${event.category} ${event.name}` }, + ); + const deleteEventLabel = intl.formatMessage( + { id: 'project_activity.events.tooltip.delete' }, + { event: `${event.category} ${event.name}` }, + ); + return ( <div className="it__project-activity-event sw-flex sw-justify-between"> <EventInner event={event} /> @@ -53,28 +69,28 @@ function Event(props: Readonly<EventProps>) { {showActions && ( <div className="sw-grow-0 sw-shrink-0 sw-ml-2"> {canChange && ( - <Tooltip content={translate('project_activity.events.tooltip.edit')}> - <InteractiveIcon - Icon={PencilIcon} - aria-label={translate('project_activity.events.tooltip.edit')} - data-test="project-activity__edit-event" - onClick={() => setChanging(true)} - stopPropagation - size="small" - /> - </Tooltip> + <ButtonIcon + Icon={IconEdit} + className="-sw-mt-1" + variety={ButtonVariety.PrimaryGhost} + size={ButtonSize.Medium} + tooltipContent={editEventLabel} + ariaLabel={editEventLabel} + data-test="project-activity__edit-event" + onClick={() => setChanging(true)} + /> )} {canDelete && ( - <Tooltip content={translate('project_activity.events.tooltip.delete')}> - <DestructiveIcon - Icon={TrashIcon} - aria-label={translate('project_activity.events.tooltip.delete')} - data-test="project-activity__delete-event" - onClick={() => setDeleting(true)} - stopPropagation - size="small" - /> - </Tooltip> + <ButtonIcon + className="-sw-mt-1" + Icon={IconDelete} + size={ButtonSize.Medium} + variety={ButtonVariety.DangerGhost} + tooltipContent={deleteEventLabel} + ariaLabel={deleteEventLabel} + data-test="project-activity__delete-event" + onClick={() => setDeleting(true)} + /> )} </div> )} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.tsx index c55aec00f8a..e155e6d07ae 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.tsx @@ -19,10 +19,11 @@ */ import styled from '@emotion/styled'; +import { Spinner, Text } from '@sonarsource/echoes-react'; import classNames from 'classnames'; import { isEqual } from 'date-fns'; import * as React from 'react'; -import { Badge, HelperHintIcon, LightLabel, Spinner, themeColor } from '~design-system'; +import { Badge, HelperHintIcon, themeColor } from '~design-system'; import Tooltip from '../../../components/controls/Tooltip'; import DateFormatter from '../../../components/intl/DateFormatter'; import { toShortISO8601String } from '../../../helpers/dates'; @@ -120,106 +121,107 @@ export default class ProjectActivityAnalysesList extends React.PureComponent<Pro } render() { - const byVersionByDay = getAnalysesByVersionByDay(this.props.analyses, this.props.query); + const { analyses, query, initializing } = this.props; + const byVersionByDay = getAnalysesByVersionByDay(analyses, query); const newCodePeriod = this.getNewCodePeriodStartKey(byVersionByDay); const hasFilteredData = byVersionByDay.length > 1 || (byVersionByDay.length === 1 && Object.keys(byVersionByDay[0].byDay).length > 0); - if (this.props.analyses.length === 0 || !hasFilteredData) { - return ( - <div> - {this.props.initializing ? ( - <div className="sw-p-4 sw-typo-default"> - <Spinner /> - </div> - ) : ( - <div className="sw-p-4 sw-typo-default"> - <LightLabel>{translate('no_results')}</LightLabel> - </div> - )} - </div> - ); - } + const hasData = analyses.length > 0 && hasFilteredData; return ( - <ul - className="it__project-activity-versions-list sw-box-border sw-overflow-auto sw-grow sw-shrink-0 sw-py-0 sw-px-4" - ref={(element) => (this.scrollContainer = element)} - style={{ - height: 'calc(100vh - 250px)', - marginTop: - this.props.project.qualifier === ComponentQualifier.Project - ? LIST_MARGIN_TOP - : undefined, - }} - > - {newCodePeriod.baselineAnalysisKey !== undefined && - newCodePeriod.firstNewCodeAnalysisKey === undefined && ( - <BaselineMarker className="sw-typo-default sw-mb-2"> - <span className="sw-py-1/2 sw-px-1"> - {translate('project_activity.new_code_period_start')} - </span> - <Tooltip - content={translate('project_activity.new_code_period_start.help')} - side="top" - > - <HelperHintIcon className="sw-ml-1" /> - </Tooltip> - </BaselineMarker> - )} - - {byVersionByDay.map((version, idx) => { - const days = Object.keys(version.byDay); - if (days.length <= 0) { - return null; - } + <> + <div aria-live="polite"> + <Spinner isLoading={initializing}> + {!hasData && ( + <div className="sw-p-4 sw-typo-default"> + <Text isSubdued>{translate('no_results')}</Text> + </div> + )} + </Spinner> + </div> - return ( - <li key={version.key || 'noversion'}> - {version.version && ( - <VersionTagStyled - className={classNames( - 'sw-sticky sw-top-0 sw-left-0 sw-pb-1 -sw-ml-4 sw-z-normal', - { - 'sw-top-0 sw-pt-0': idx === 0, - }, - )} - > + {hasData && ( + <ul + className="it__project-activity-versions-list sw-box-border sw-overflow-auto sw-grow sw-shrink-0 sw-py-0 sw-px-4" + ref={(element) => (this.scrollContainer = element)} + style={{ + height: 'calc(100vh - 250px)', + marginTop: + this.props.project.qualifier === ComponentQualifier.Project + ? LIST_MARGIN_TOP + : undefined, + }} + > + {newCodePeriod.baselineAnalysisKey !== undefined && + newCodePeriod.firstNewCodeAnalysisKey === undefined && ( + <BaselineMarker className="sw-typo-default sw-mb-2"> + <span className="sw-py-1/2 sw-px-1"> + {translate('project_activity.new_code_period_start')} + </span> <Tooltip - mouseEnterDelay={0.5} - content={`${translate('version')} ${version.version}`} + content={translate('project_activity.new_code_period_start.help')} + side="top" > - <Badge className="sw-p-1">{version.version}</Badge> + <HelperHintIcon className="sw-ml-1" /> </Tooltip> - </VersionTagStyled> + </BaselineMarker> )} - <ul className="it__project-activity-days-list"> - {days.map((day) => ( - <li - className="it__project-activity-day sw-mt-1 sw-mb-4" - data-day={toShortISO8601String(Number(day))} - key={day} - > - <div className="sw-typo-lg-semibold sw-mb-3"> - <DateFormatter date={Number(day)} long /> - </div> - <ul className="it__project-activity-analyses-list"> - {version.byDay[day]?.map((analysis) => - this.renderAnalysis(analysis, newCodePeriod.firstNewCodeAnalysisKey), + + {byVersionByDay.map((version, idx) => { + const days = Object.keys(version.byDay); + if (days.length <= 0) { + return null; + } + + return ( + <li key={version.key || 'noversion'}> + {version.version && ( + <VersionTagStyled + className={classNames( + 'sw-sticky sw-top-0 sw-left-0 sw-pb-1 -sw-ml-4 sw-z-normal', + { + 'sw-top-0 sw-pt-0': idx === 0, + }, )} - </ul> - </li> - ))} - </ul> - </li> - ); - })} - {this.props.analysesLoading && ( - <li className="sw-text-center"> - <Spinner /> - </li> + > + <Tooltip + mouseEnterDelay={0.5} + content={`${translate('version')} ${version.version}`} + > + <Badge className="sw-p-1">{version.version}</Badge> + </Tooltip> + </VersionTagStyled> + )} + <ul className="it__project-activity-days-list"> + {days.map((day) => ( + <li + className="it__project-activity-day sw-mt-1 sw-mb-4" + data-day={toShortISO8601String(Number(day))} + key={day} + > + <div className="sw-typo-lg-semibold sw-mb-3"> + <DateFormatter date={Number(day)} long /> + </div> + <ul className="it__project-activity-analyses-list"> + {version.byDay[day]?.map((analysis) => + this.renderAnalysis(analysis, newCodePeriod.firstNewCodeAnalysisKey), + )} + </ul> + </li> + ))} + </ul> + </li> + ); + })} + {this.props.analysesLoading && ( + <li className="sw-text-center"> + <Spinner /> + </li> + )} + </ul> )} - </ul> + </> ); } } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityDateInput.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityDateInput.tsx index 51b7db8fee5..bef9e20b434 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityDateInput.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityDateInput.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { Button } from '@sonarsource/echoes-react'; +import { Button, ButtonGroup, Label } from '@sonarsource/echoes-react'; import * as React from 'react'; import { DateRangePicker, PopupZLevel } from '~design-system'; import { translate } from '../../../helpers/l10n'; @@ -41,7 +41,8 @@ export default class ProjectActivityDateInput extends React.PureComponent<Props> render() { return ( - <div className="sw-flex"> + <ButtonGroup> + <Label htmlFor="date-from">{translate('project_activity.filter_date_range')}</Label> <DateRangePicker className="sw-w-abs-350" startClearButtonLabel={translate('clear.start')} @@ -60,7 +61,7 @@ export default class ProjectActivityDateInput extends React.PureComponent<Props> > {translate('project_activity.reset_dates')} </Button> - </div> + </ButtonGroup> ); } } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFilters.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFilters.tsx index 0ee0d13708c..08a344243ab 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFilters.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFilters.tsx @@ -18,8 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { ButtonGroup, InputSize, Label, Select } from '@sonarsource/echoes-react'; import * as React from 'react'; -import { InputSelect, LabelValueSelectOption } from '~design-system'; +import { LabelValueSelectOption } from '~design-system'; import { isPortfolioLike } from '~sonar-aligned/helpers/component'; import { ComponentQualifier } from '~sonar-aligned/types/component'; import { translate } from '../../../helpers/l10n'; @@ -52,8 +53,8 @@ export default function ProjectActivityPageFilters(props: ProjectActivityPageFil })); const handleCategoryChange = React.useCallback( - (option: { value: string } | null) => { - updateQuery({ category: option ? option.value : '' }); + (option: string | null) => { + updateQuery({ category: option ?? '' }); }, [updateQuery], ); @@ -61,19 +62,19 @@ export default function ProjectActivityPageFilters(props: ProjectActivityPageFil return ( <div className="sw-flex sw-mb-5 sw-items-center"> {!isPortfolioLike(project.qualifier) && ( - <InputSelect - aria-label={translate('project_activity.filter_events')} - className="sw-mr-8 sw-typo-default sw-w-abs-200" - isClearable - onChange={(data: LabelValueSelectOption) => handleCategoryChange(data)} - options={options} - placeholder={translate('project_activity.filter_events')} - size="full" - value={options.find((o) => o.value === category)} - classNames={{ - menu: () => 'sw-z-dropdown-menu-page', - }} - /> + <ButtonGroup> + <Label htmlFor="graph-type">{translate('project_activity.filter_events')}</Label> + <Select + id="events-filter" + className="sw-mr-8 sw-typo-default sw-w-abs-200" + hasDropdownAutoWidth + placeholder={translate('project_activity.filter_events.placeholder')} + onChange={(value) => handleCategoryChange(value)} + value={options.find((o) => o.value === category)?.value} + size={InputSize.Small} + data={options} + /> + </ButtonGroup> )} <ProjectActivityDateInput from={from} onChange={props.updateQuery} to={to} /> </div> 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 380572a181c..9be3a7fffe4 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 @@ -325,10 +325,10 @@ describe('CRUD', () => { await ui.addVersionEvent('1.1.0.1', initialValue); expect(screen.getAllByText(initialValue).length).toBeGreaterThan(0); - await ui.updateEvent(1, updatedValue); + await ui.updateEvent(`VERSION ${initialValue}`, updatedValue); expect(screen.getAllByText(updatedValue).length).toBeGreaterThan(0); - await ui.deleteEvent(0); + await ui.deleteEvent(`VERSION ${updatedValue}`); expect(screen.queryByText(updatedValue)).not.toBeInTheDocument(); }); @@ -351,10 +351,10 @@ describe('CRUD', () => { await ui.addCustomEvent('1.1.0.1', initialValue); expect(screen.getAllByText(initialValue).length).toBeGreaterThan(0); - await ui.updateEvent(1, updatedValue); + await ui.updateEvent(`OTHER ${initialValue}`, updatedValue); expect(screen.getAllByText(updatedValue).length).toBeGreaterThan(0); - await ui.deleteEvent(0); + await ui.deleteEvent(`OTHER ${updatedValue}`); expect(screen.queryByText(updatedValue)).not.toBeInTheDocument(); }); @@ -799,8 +799,10 @@ function getPageObject() { addCustomEventBtn: byRole('menuitem', { name: 'project_activity.add_custom_event' }), addVersionEvenBtn: byRole('menuitem', { name: 'project_activity.add_version' }), deleteAnalysisBtn: byRole('menuitem', { name: 'project_activity.delete_analysis' }), - editEventBtn: byRole('button', { name: 'project_activity.events.tooltip.edit' }), - deleteEventBtn: byRole('button', { name: 'project_activity.events.tooltip.delete' }), + editEventBtn: (event: string) => + byRole('button', { name: `project_activity.events.tooltip.edit.${event}` }), + deleteEventBtn: (event: string) => + byRole('button', { name: `project_activity.events.tooltip.delete.${event}` }), // Event modal. nameInput: byLabelText('name'), @@ -876,15 +878,15 @@ function getPageObject() { await user.click(ui.saveBtn.get()); }, - async updateEvent(index: number, value: string) { - await user.click(ui.editEventBtn.getAll()[index]); + async updateEvent(event: string, value: string) { + await user.click(ui.editEventBtn(event).get()); 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]); + async deleteEvent(event: string) { + await user.click(ui.deleteEventBtn(event).get()); await user.click(ui.deleteBtn.get()); }, diff --git a/server/sonar-web/src/main/js/components/activity-graph/EventInner.tsx b/server/sonar-web/src/main/js/components/activity-graph/EventInner.tsx index 050fac489ad..6fa0899a2a4 100644 --- a/server/sonar-web/src/main/js/components/activity-graph/EventInner.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/EventInner.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { Tooltip } from '@sonarsource/echoes-react'; import * as React from 'react'; import { Note } from '~design-system'; import { ComponentContext } from '../../app/components/componentContext/ComponentContext'; @@ -25,7 +26,6 @@ import { translate } from '../../helpers/l10n'; import { useCurrentBranchQuery } from '../../queries/branch'; import { StaleTime } from '../../queries/common'; import { AnalysisEvent, ProjectAnalysisEventCategory } from '../../types/project-activity'; -import Tooltip from '../controls/Tooltip'; import { DefinitionChangeEventInner, isDefinitionChangeEvent } from './DefinitionChangeEventInner'; import { RichQualityGateEventInner, isRichQualityGateEvent } from './RichQualityGateEventInner'; import { 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 b6388998833..d287575cefc 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 @@ -20,14 +20,13 @@ import styled from '@emotion/styled'; import * as React from 'react'; +import { useIntl } from 'react-intl'; import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer'; -import { ButtonSecondary } from '~design-system'; import { formatMeasure } from '~sonar-aligned/helpers/measures'; import { AdvancedTimeline } from '../../components/charts/AdvancedTimeline'; -import { translate } from '../../helpers/l10n'; +import { KeyboardKeys } from '../../helpers/keycodes'; import { getShortType } from '../../helpers/measures'; import { MeasureHistory, ParsedAnalysis, Serie } from '../../types/project-activity'; -import ModalButton from '../controls/ModalButton'; import DataTableModal from './DataTableModal'; import GraphsLegendCustom from './GraphsLegendCustom'; import GraphsLegendStatic from './GraphsLegendStatic'; @@ -70,8 +69,10 @@ export default function GraphHistory(props: Readonly<Props>) { showAreas, graphDescription, } = props; + const intl = useIntl(); const [tooltipIdx, setTooltipIdx] = React.useState<number | undefined>(undefined); const [tooltipXPos, setTooltipXPos] = React.useState<number | undefined>(undefined); + const [tableIsVisible, setTableIsVisible] = React.useState(false); const formatValue = (tick: string | number) => { return formatMeasure(tick, getShortType(metricsType)); @@ -87,84 +88,88 @@ export default function GraphHistory(props: Readonly<Props>) { setTooltipXPos(tooltipXPos); }; - const modalProp = ({ onClose }: { onClose: () => void }) => ( - <DataTableModal - analyses={analyses} - graphEndDate={graphEndDate} - graphStartDate={graphStartDate} - series={series} - onClose={onClose} - /> - ); - const events = getAnalysisEventsForDate(analyses, selectedDate); return ( - <StyledGraphContainer className="sw-flex sw-flex-col sw-justify-center sw-items-stretch sw-grow sw-py-2"> - {isCustom && props.removeCustomMetric ? ( - <GraphsLegendCustom - leakPeriodDate={leakPeriodDate} - removeMetric={props.removeCustomMetric} - series={series} - /> - ) : ( - <GraphsLegendStatic leakPeriodDate={leakPeriodDate} series={series} /> - )} + <> + <StyledGraphContainer + tabIndex={canShowDataAsTable ? 0 : -1} + aria-label={`${intl.formatMessage( + { id: 'project_activity.graphs.graph_shown_x' }, + { '0': isCustom ? series.map((s) => s.translatedName).join(',') : graph }, + )} ${intl.formatMessage({ id: 'project_activity.graphs.open_in_table' })}`} + onKeyUp={(event) => { + if (event.key === KeyboardKeys.Enter) { + setTableIsVisible(true); + } + }} + className="sw-flex sw-flex-col sw-justify-center sw-items-stretch sw-grow sw-py-2" + > + {isCustom && props.removeCustomMetric ? ( + <GraphsLegendCustom + leakPeriodDate={leakPeriodDate} + removeMetric={props.removeCustomMetric} + series={series} + /> + ) : ( + <GraphsLegendStatic leakPeriodDate={leakPeriodDate} series={series} /> + )} - <div className="sw-flex-1"> - <AutoSizer> - {({ height, width }) => ( - <div> - <AdvancedTimeline - endDate={graphEndDate} - formatYTick={formatValue} - height={height} - leakPeriodDate={leakPeriodDate} - splitPointDate={measuresHistory.find((m) => m.splitPointDate)?.splitPointDate} - metricType={metricsType} - selectedDate={selectedDate} - series={series} - showAreas={showAreas} - startDate={graphStartDate} - graphDescription={graphDescription} - updateSelectedDate={props.updateSelectedDate} - updateTooltip={updateTooltip} - updateZoom={props.updateGraphZoom} - width={width} - /> + <div className="sw-flex-1"> + <AutoSizer> + {({ height, width }) => ( + <div> + <AdvancedTimeline + endDate={graphEndDate} + formatYTick={formatValue} + height={height} + leakPeriodDate={leakPeriodDate} + splitPointDate={measuresHistory.find((m) => m.splitPointDate)?.splitPointDate} + metricType={metricsType} + selectedDate={selectedDate} + series={series} + showAreas={showAreas} + startDate={graphStartDate} + graphDescription={graphDescription} + updateSelectedDate={props.updateSelectedDate} + updateTooltip={updateTooltip} + updateZoom={props.updateGraphZoom} + width={width} + /> - {selectedDate !== undefined && - tooltipIdx !== undefined && - tooltipXPos !== undefined && ( - <GraphsTooltips - events={events} - formatValue={formatTooltipValue} - graph={graph} - graphWidth={width} - measuresHistory={measuresHistory} - selectedDate={selectedDate} - series={series} - tooltipIdx={tooltipIdx} - tooltipPos={tooltipXPos} - /> - )} - </div> - )} - </AutoSizer> - </div> - {canShowDataAsTable && ( - <ModalButton modal={modalProp}> - {({ onClick }) => ( - <ButtonSecondary className="sw-sr-only" onClick={onClick}> - {translate('project_activity.graphs.open_in_table')} - </ButtonSecondary> - )} - </ModalButton> + {selectedDate !== undefined && + tooltipIdx !== undefined && + tooltipXPos !== undefined && ( + <GraphsTooltips + events={events} + formatValue={formatTooltipValue} + graph={graph} + graphWidth={width} + measuresHistory={measuresHistory} + selectedDate={selectedDate} + series={series} + tooltipIdx={tooltipIdx} + tooltipPos={tooltipXPos} + /> + )} + </div> + )} + </AutoSizer> + </div> + </StyledGraphContainer> + {tableIsVisible && ( + <DataTableModal + analyses={analyses} + graphEndDate={graphEndDate} + graphStartDate={graphStartDate} + series={series} + onClose={() => setTableIsVisible(false)} + /> )} - </StyledGraphContainer> + </> ); } -const StyledGraphContainer = styled.div` +const StyledGraphContainer = styled.section` height: 300px; `; diff --git a/server/sonar-web/src/main/js/components/activity-graph/GraphsHistory.tsx b/server/sonar-web/src/main/js/components/activity-graph/GraphsHistory.tsx index d390e385f35..bbf04b46025 100644 --- a/server/sonar-web/src/main/js/components/activity-graph/GraphsHistory.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/GraphsHistory.tsx @@ -69,65 +69,60 @@ export default class GraphsHistory extends React.PureComponent<Props, State> { render() { const { analyses, graph, loading, series, ariaLabel, canShowDataAsTable } = this.props; const isCustom = isCustomGraph(graph); + const showAreas = [GraphType.coverage, GraphType.duplications].includes(graph); - if (loading) { - return ( - <div className="sw-flex sw-justify-center sw-flex-col sw-items-stretch sw-grow"> - <div className="sw-text-center"> - <Spinner isLoading={loading} /> - </div> - </div> - ); - } - - if (!hasHistoryData(series)) { - return ( - <div className="sw-flex sw-items-center sw-justify-center sw-h-full"> - <Text isSubdued> - {translate( - isCustom - ? 'project_activity.graphs.custom.no_history' - : 'component_measures.no_history', + return ( + <div className="sw-flex sw-justify-center sw-flex-col sw-items-stretch sw-text-center sw-grow"> + <div aria-live="polite" aria-busy={loading}> + <Spinner isLoading={loading}> + {!hasHistoryData(series) && ( + <Text isSubdued className="sw-max-w-full"> + {translate( + isCustom + ? 'project_activity.graphs.custom.no_history' + : 'component_measures.no_history', + )} + </Text> )} - </Text> + </Spinner> </div> - ); - } - const showAreas = [GraphType.coverage, GraphType.duplications].includes(graph); - return ( - <div className="sw-flex sw-justify-center sw-flex-col sw-items-stretch sw-grow"> - {this.props.graphs.map((graphSeries, idx) => { - return ( - <GraphHistory - analyses={analyses} - canShowDataAsTable={canShowDataAsTable} - graph={graph} - graphEndDate={this.props.graphEndDate} - graphStartDate={this.props.graphStartDate} - isCustom={isCustom} - key={idx} - leakPeriodDate={this.props.leakPeriodDate} - measuresHistory={this.props.measuresHistory} - metricsType={getSeriesMetricType(graphSeries)} - removeCustomMetric={this.props.removeCustomMetric} - selectedDate={this.state.selectedDate} - series={graphSeries} - graphDescription={ - ariaLabel ?? - translateWithParameters( - 'project_activity.graphs.explanation_x', - uniqBy(graphSeries, 'name') - .map(({ translatedName }) => translatedName) - .join(', '), - ) - } - showAreas={showAreas} - updateGraphZoom={this.props.updateGraphZoom} - updateSelectedDate={this.props.updateSelectedDate} - updateTooltip={this.updateTooltip} - /> - ); - })} + + {hasHistoryData(series) && !loading && ( + <> + {this.props.graphs.map((graphSeries, idx) => { + return ( + <GraphHistory + analyses={analyses} + canShowDataAsTable={canShowDataAsTable} + graph={graph} + graphEndDate={this.props.graphEndDate} + graphStartDate={this.props.graphStartDate} + isCustom={isCustom} + key={idx} + leakPeriodDate={this.props.leakPeriodDate} + measuresHistory={this.props.measuresHistory} + metricsType={getSeriesMetricType(graphSeries)} + removeCustomMetric={this.props.removeCustomMetric} + selectedDate={this.state.selectedDate} + series={graphSeries} + graphDescription={ + ariaLabel ?? + translateWithParameters( + 'project_activity.graphs.explanation_x', + uniqBy(graphSeries, 'name') + .map(({ translatedName }) => translatedName) + .join(', '), + ) + } + showAreas={showAreas} + updateGraphZoom={this.props.updateGraphZoom} + updateSelectedDate={this.props.updateSelectedDate} + updateTooltip={this.updateTooltip} + /> + ); + })} + </> + )} </div> ); } diff --git a/server/sonar-web/src/main/js/components/activity-graph/__tests__/ActivityGraph-it.tsx b/server/sonar-web/src/main/js/components/activity-graph/__tests__/ActivityGraph-it.tsx index 5e76f3e45c8..058b8eb3ca9 100644 --- a/server/sonar-web/src/main/js/components/activity-graph/__tests__/ActivityGraph-it.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/ActivityGraph-it.tsx @@ -101,7 +101,7 @@ describe('data table modal', () => { await ui.closeDataTable(); await ui.changeGraphType(GraphType.coverage); - await ui.openDataTable(); + await ui.openDataTable(true); expect(ui.dataTable.get()).toBeInTheDocument(); expect(ui.dataTableColHeaders.getAll()).toHaveLength(4); expect(ui.dataTableRows.getAll()).toHaveLength(HISTORY_COUNT + 1); @@ -229,7 +229,7 @@ function getPageObject() { noDataText: byText('project_activity.graphs.custom.no_history'), // Data in table. - openInTableBtn: byRole('button', { name: 'project_activity.graphs.open_in_table' }), + openInTableRegion: byRole('region', { name: /project_activity.graphs.open_in_table/ }), closeDataTableBtn: byRole('button', { name: 'close' }), dataTable: byRole('table'), dataTableRows: byRole('row'), @@ -260,8 +260,15 @@ function getPageObject() { async removeMetric(metric: MetricKey) { await user.click(ui.legendRemoveMetricBtn(metric).get()); }, - async openDataTable() { - await user.click(ui.openInTableBtn.get()); + async openDataTable(tabFromGraphSelection = false) { + // tab to graph selection and close popup + if (!tabFromGraphSelection) { + await user.tab(); + await user.keyboard('{escape}'); + } + // tab to graph region and open data table + await user.tab(); + await user.keyboard('{enter}'); }, async closeDataTable() { await user.click(ui.closeDataTableBtn.get()); diff --git a/server/sonar-web/src/main/js/design-system/components/input/DatePickerCustomCalendarNavigation.tsx b/server/sonar-web/src/main/js/design-system/components/input/DatePickerCustomCalendarNavigation.tsx index 8020048a2bb..1ac08f07ceb 100644 --- a/server/sonar-web/src/main/js/design-system/components/input/DatePickerCustomCalendarNavigation.tsx +++ b/server/sonar-web/src/main/js/design-system/components/input/DatePickerCustomCalendarNavigation.tsx @@ -52,9 +52,8 @@ export function CustomCalendarNavigation(props: CaptionProps) { if (date === undefined) { return intl.formatMessage({ id: 'disabled_' }); } - return `${intl.formatDate(date, { month: 'long', format: 'M' })} ${intl.formatDate(date, { + return `${intl.formatDate(date, { month: 'long' })} ${intl.formatDate(date, { year: 'numeric', - format: 'y', })}`; }; diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index b802dd322a3..cd2165b253d 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2157,9 +2157,11 @@ project_activity.remove_custom_event.question=Are you sure you want to delete th project_activity.reset_dates=Reset dates project_activity.delete_analysis=Delete Analysis project_activity.delete_analysis.question=Are you sure you want to delete this analysis from the history? -project_activity.filter_events=Filter events -project_activity.events.tooltip.edit=Edit this event -project_activity.events.tooltip.delete=Delete this event +project_activity.filter_events=Events filter +project_activity.filter_events.placeholder=Select an event +project_activity.filter_date_range=Date range filter +project_activity.events.tooltip.edit=Edit this event: {event} +project_activity.events.tooltip.delete=Delete this event: {event} project_activity.new_code_period_start=Everything above this line is New Code project_activity.new_code_period_start.help=The analysis below this mark is the baseline for New Code comparison @@ -2178,7 +2180,8 @@ project_activity.graphs.custom.no_history=There isn't enough data to generate an project_activity.graphs.custom.metric_no_history=This metric has no historical data to display. project_activity.graphs.custom.search=Search for a metric by name project_activity.graphs.custom.type_x_message=Only "{0}" metrics are available with your current selection. -project_activity.graphs.open_in_table=Show the graph data in a table +project_activity.graphs.graph_shown_x=Graph shown for {0}. +project_activity.graphs.open_in_table=Press 'Enter' to show the graph data in a table. project_activity.graphs.data_table.title=Graph data in table format project_activity.graphs.data_table.max_lines_warning=Only the {0} most recent data entries are shown. If you want to see different data, change the date filters on the main page. project_activity.graphs.data_table.no_data_warning=There is no data for the selected series. |