aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorstanislavh <stanislav.honcharov@sonarsource.com>2024-11-22 11:12:52 +0100
committersonartech <sonartech@sonarsource.com>2024-11-22 20:03:10 +0000
commit6e89b695f15534c743b4b3e2d8eef295d1dfccdd (patch)
tree1dae6d54d420f995b430228d4061074861aad033
parent664e392ebfedaf163c6b6cf9a2f60b37b3005955 (diff)
downloadsonarqube-6e89b695f15534c743b4b3e2d8eef295d1dfccdd.tar.gz
sonarqube-6e89b695f15534c743b4b3e2d8eef295d1dfccdd.zip
SONAR-22309 Fix a11y issues on Activity page
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/Event.tsx60
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.tsx178
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityDateInput.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFilters.tsx33
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx22
-rw-r--r--server/sonar-web/src/main/js/components/activity-graph/EventInner.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/activity-graph/GraphHistory.tsx151
-rw-r--r--server/sonar-web/src/main/js/components/activity-graph/GraphsHistory.tsx107
-rw-r--r--server/sonar-web/src/main/js/components/activity-graph/__tests__/ActivityGraph-it.tsx15
-rw-r--r--server/sonar-web/src/main/js/design-system/components/input/DatePickerCustomCalendarNavigation.tsx3
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties11
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.