@@ -24,10 +24,12 @@ document.documentElement.appendChild(content); | |||
const baseUrl = ''; | |||
(window as any).baseUrl = baseUrl; | |||
Element.prototype.scrollIntoView = () => {}; | |||
jest.mock('../../src/main/js/helpers/l10n', () => ({ | |||
...jest.requireActual('../../src/main/js/helpers/l10n'), | |||
hasMessage: () => true, | |||
translate: (...keys: string[]) => keys.join('.'), | |||
translateWithParameters: (messageKey: string, ...parameters: Array<string | number>) => | |||
[messageKey, ...parameters].join('.') | |||
[messageKey, ...parameters].join('.'), | |||
})); |
@@ -0,0 +1,177 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 { cloneDeep, uniqueId } from 'lodash'; | |||
import { mockAnalysis, mockAnalysisEvent } from '../../helpers/mocks/project-activity'; | |||
import { Analysis } from '../../types/project-activity'; | |||
import { | |||
changeEvent, | |||
createEvent, | |||
deleteAnalysis, | |||
deleteEvent, | |||
getProjectActivity, | |||
} from '../projectActivity'; | |||
export class ProjectActivityServiceMock { | |||
readOnlyAnalysisList: Analysis[]; | |||
analysisList: Analysis[]; | |||
constructor(analyses?: Analysis[]) { | |||
this.readOnlyAnalysisList = analyses || [ | |||
mockAnalysis({ | |||
key: 'AXJMbIUGPAOIsUIE3eNT', | |||
date: '2017-03-03T09:36:01+0100', | |||
projectVersion: '1.1', | |||
buildString: '1.1.0.2', | |||
events: [ | |||
mockAnalysisEvent({ category: 'VERSION', key: 'IsUIEAXJMbIUGPAO3eND', name: '1.1' }), | |||
], | |||
}), | |||
mockAnalysis({ | |||
key: 'AXJMbIUGPAOIsUIE3eND', | |||
date: '2017-03-02T09:36:01+0100', | |||
projectVersion: '1.1', | |||
buildString: '1.1.0.1', | |||
}), | |||
mockAnalysis({ | |||
key: 'AXJMbIUGPAOIsUIE3eNE', | |||
date: '2017-03-01T10:36:01+0100', | |||
projectVersion: '1.0', | |||
buildString: '1.0.0.2', | |||
events: [ | |||
mockAnalysisEvent({ category: 'VERSION', key: 'IUGPAOAXJMbIsUIE3eNE', name: '1.0' }), | |||
], | |||
}), | |||
mockAnalysis({ | |||
key: 'AXJMbIUGPAOIsUIE3eNC', | |||
date: '2017-03-01T09:36:01+0100', | |||
projectVersion: '1.0', | |||
buildString: '1.0.0.1', | |||
}), | |||
]; | |||
this.analysisList = cloneDeep(this.readOnlyAnalysisList); | |||
(getProjectActivity as jest.Mock).mockImplementation(this.getActivityHandler); | |||
(deleteAnalysis as jest.Mock).mockImplementation(this.deleteAnalysisHandler); | |||
(createEvent as jest.Mock).mockImplementation(this.createEventHandler); | |||
(changeEvent as jest.Mock).mockImplementation(this.changeEventHandler); | |||
(deleteEvent as jest.Mock).mockImplementation(this.deleteEventHandler); | |||
} | |||
reset = () => { | |||
this.analysisList = cloneDeep(this.readOnlyAnalysisList); | |||
}; | |||
getActivityHandler = () => { | |||
return this.reply({ | |||
analyses: this.analysisList, | |||
paging: { | |||
pageIndex: 1, | |||
pageSize: 100, | |||
total: this.analysisList.length, | |||
}, | |||
}); | |||
}; | |||
deleteAnalysisHandler = (analysisKey: string) => { | |||
const i = this.analysisList.findIndex(({ key }) => key === analysisKey); | |||
if (i !== undefined) { | |||
this.analysisList.splice(i, 1); | |||
return this.reply(); | |||
} | |||
throw new Error(`Could not find analysis with key: ${analysisKey}`); | |||
}; | |||
createEventHandler = ( | |||
analysisKey: string, | |||
name: string, | |||
category = 'OTHER', | |||
description?: string | |||
) => { | |||
const analysis = this.findAnalysis(analysisKey); | |||
const key = uniqueId(analysisKey); | |||
analysis.events.push({ key, name, category, description }); | |||
return this.reply({ | |||
analysis: analysisKey, | |||
key, | |||
name, | |||
category, | |||
description, | |||
}); | |||
}; | |||
changeEventHandler = (eventKey: string, name: string, description?: string) => { | |||
const [eventIndex, analysisKey] = this.findEvent(eventKey); | |||
const analysis = this.findAnalysis(analysisKey); | |||
const event = analysis.events[eventIndex]; | |||
event.name = name; | |||
event.description = description; | |||
return this.reply({ analysis: analysisKey, ...event }); | |||
}; | |||
deleteEventHandler = (eventKey: string) => { | |||
const [eventIndex, analysisKey] = this.findEvent(eventKey); | |||
const analysis = this.findAnalysis(analysisKey); | |||
analysis.events.splice(eventIndex, 1); | |||
return this.reply(); | |||
}; | |||
findEvent = (eventKey: string): [number, string] => { | |||
let analysisKey; | |||
const eventIndex = this.analysisList.reduce((acc, { key, events }) => { | |||
if (acc === undefined) { | |||
const i = events.findIndex(({ key }) => key === eventKey); | |||
if (i > -1) { | |||
analysisKey = key; | |||
return i; | |||
} | |||
} | |||
return acc; | |||
}, undefined); | |||
if (eventIndex !== undefined && analysisKey !== undefined) { | |||
return [eventIndex, analysisKey]; | |||
} | |||
throw new Error(`Could not find event with key: ${eventKey}`); | |||
}; | |||
findAnalysis = (analysisKey: string) => { | |||
const analysis = this.analysisList.find(({ key }) => key === analysisKey); | |||
if (analysis !== undefined) { | |||
return analysis; | |||
} | |||
throw new Error(`Could not find analysis with key: ${analysisKey}`); | |||
}; | |||
reply<T>(response?: T): Promise<T | void> { | |||
return Promise.resolve(response ? cloneDeep(response) : undefined); | |||
} | |||
} |
@@ -19,12 +19,15 @@ | |||
*/ | |||
import classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import { injectIntl, WrappedComponentProps } from 'react-intl'; | |||
import ActionsDropdown, { | |||
ActionsDropdownDivider, | |||
ActionsDropdownItem, | |||
} from '../../../components/controls/ActionsDropdown'; | |||
import { ButtonPlain } from '../../../components/controls/buttons'; | |||
import ClickEventBoundary from '../../../components/controls/ClickEventBoundary'; | |||
import HelpTooltip from '../../../components/controls/HelpTooltip'; | |||
import { formatterOption } from '../../../components/intl/DateTimeFormatter'; | |||
import TimeFormatter from '../../../components/intl/TimeFormatter'; | |||
import { PopupPlacement } from '../../../components/ui/popups'; | |||
import { parseDate } from '../../../helpers/dates'; | |||
@@ -34,7 +37,7 @@ import Events from './Events'; | |||
import AddEventForm from './forms/AddEventForm'; | |||
import RemoveAnalysisForm from './forms/RemoveAnalysisForm'; | |||
export interface ProjectActivityAnalysisProps { | |||
export interface ProjectActivityAnalysisProps extends WrappedComponentProps { | |||
addCustomEvent: (analysis: string, name: string, category?: string) => Promise<void>; | |||
addVersion: (analysis: string, version: string) => Promise<void>; | |||
analysis: ParsedAnalysis; | |||
@@ -53,7 +56,15 @@ export interface ProjectActivityAnalysisProps { | |||
export function ProjectActivityAnalysis(props: ProjectActivityAnalysisProps) { | |||
let node: HTMLLIElement | null = null; | |||
const { analysis, isBaseline, isFirst, canAdmin, canCreateVersion, selected } = props; | |||
const { | |||
analysis, | |||
isBaseline, | |||
isFirst, | |||
canAdmin, | |||
canCreateVersion, | |||
selected, | |||
intl: { formatDate }, | |||
} = props; | |||
React.useEffect(() => { | |||
if (node && selected) { | |||
@@ -85,9 +96,18 @@ export function ProjectActivityAnalysis(props: ProjectActivityAnalysisProps) { | |||
<div className="project-activity-time"> | |||
<TimeFormatter date={parsedDate} long={false}> | |||
{(formattedTime) => ( | |||
<time className="text-middle" dateTime={parsedDate.toISOString()}> | |||
{formattedTime} | |||
</time> | |||
<ButtonPlain | |||
aria-current={selected} | |||
aria-label={translateWithParameters( | |||
'project_activity.show_analysis_X_on_graph', | |||
analysis.buildString || formatDate(parsedDate, formatterOption) | |||
)} | |||
onClick={() => props.updateSelectedDate(analysis.date)} | |||
> | |||
<time className="text-middle" dateTime={parsedDate.toISOString()}> | |||
{formattedTime} | |||
</time> | |||
</ButtonPlain> | |||
)} | |||
</TimeFormatter> | |||
</div> | |||
@@ -105,6 +125,10 @@ export function ProjectActivityAnalysis(props: ProjectActivityAnalysisProps) { | |||
<ClickEventBoundary> | |||
<div className="project-activity-analysis-actions big-spacer-left"> | |||
<ActionsDropdown | |||
ariaLabel={translateWithParameters( | |||
'project_activity.analysis_X_actions', | |||
analysis.buildString || formatDate(parsedDate, formatterOption) | |||
)} | |||
overlayPlacement={PopupPlacement.BottomRight} | |||
small={true} | |||
toggleClassName="js-analysis-actions" | |||
@@ -196,4 +220,4 @@ export function ProjectActivityAnalysis(props: ProjectActivityAnalysisProps) { | |||
); | |||
} | |||
export default React.memo(ProjectActivityAnalysis); | |||
export default injectIntl(ProjectActivityAnalysis); |
@@ -19,7 +19,6 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { useSearchParams } from 'react-router-dom'; | |||
import { getAllMetrics } from '../../../api/metrics'; | |||
import { | |||
changeEvent, | |||
createEvent, | |||
@@ -30,6 +29,7 @@ import { | |||
} from '../../../api/projectActivity'; | |||
import { getAllTimeMachineData } from '../../../api/time-machine'; | |||
import withComponentContext from '../../../app/components/componentContext/withComponentContext'; | |||
import withMetricsContext from '../../../app/components/metrics/withMetricsContext'; | |||
import { | |||
DEFAULT_GRAPH, | |||
getActivityGraph, | |||
@@ -41,9 +41,10 @@ import { getBranchLikeQuery } from '../../../helpers/branch-like'; | |||
import { parseDate } from '../../../helpers/dates'; | |||
import { serializeStringArray } from '../../../helpers/query'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { ComponentQualifier, isPortfolioLike } from '../../../types/component'; | |||
import { MetricKey } from '../../../types/metrics'; | |||
import { GraphType, MeasureHistory, ParsedAnalysis } from '../../../types/project-activity'; | |||
import { Component, Metric, Paging, RawQuery } from '../../../types/types'; | |||
import { Component, Dict, Metric, Paging, RawQuery } from '../../../types/types'; | |||
import * as actions from '../actions'; | |||
import { | |||
customMetricsChanged, | |||
@@ -58,6 +59,7 @@ interface Props { | |||
branchLike?: BranchLike; | |||
component: Component; | |||
location: Location; | |||
metrics: Dict<Metric>; | |||
router: Router; | |||
} | |||
@@ -66,7 +68,6 @@ export interface State { | |||
analysesLoading: boolean; | |||
graphLoading: boolean; | |||
initialized: boolean; | |||
metrics: Metric[]; | |||
measuresHistory: MeasureHistory[]; | |||
query: Query; | |||
} | |||
@@ -87,7 +88,6 @@ export class ProjectActivityApp extends React.PureComponent<Props, State> { | |||
graphLoading: true, | |||
initialized: false, | |||
measuresHistory: [], | |||
metrics: [], | |||
query: parseQuery(props.location.query), | |||
}; | |||
} | |||
@@ -251,18 +251,35 @@ export class ProjectActivityApp extends React.PureComponent<Props, State> { | |||
let current = component.breadcrumbs.length - 1; | |||
while ( | |||
current > 0 && | |||
!['TRK', 'VW', 'APP'].includes(component.breadcrumbs[current].qualifier) | |||
!( | |||
[ | |||
ComponentQualifier.Project, | |||
ComponentQualifier.Portfolio, | |||
ComponentQualifier.Application, | |||
] as string[] | |||
).includes(component.breadcrumbs[current].qualifier) | |||
) { | |||
current--; | |||
} | |||
return component.breadcrumbs[current].key; | |||
}; | |||
filterMetrics({ qualifier }: Component, metrics: Metric[]) { | |||
return ['VW', 'SVW'].includes(qualifier) | |||
? metrics.filter((metric) => metric.key !== MetricKey.security_hotspots_reviewed) | |||
: metrics.filter((metric) => metric.key !== MetricKey.security_review_rating); | |||
} | |||
filterMetrics = () => { | |||
const { | |||
component: { qualifier }, | |||
metrics, | |||
} = this.props; | |||
if (isPortfolioLike(qualifier)) { | |||
return Object.values(metrics).filter( | |||
(metric) => metric.key !== MetricKey.security_hotspots_reviewed | |||
); | |||
} | |||
return Object.values(metrics).filter( | |||
(metric) => metric.key !== MetricKey.security_review_rating | |||
); | |||
}; | |||
firstLoadData(query: Query, component: Component) { | |||
const graphMetrics = getHistoryMetrics(query.graph || DEFAULT_GRAPH, query.customMetrics); | |||
@@ -278,17 +295,15 @@ export class ProjectActivityApp extends React.PureComponent<Props, State> { | |||
ACTIVITY_PAGE_SIZE_FIRST_BATCH, | |||
serializeQuery(query) | |||
), | |||
getAllMetrics(), | |||
this.fetchMeasuresHistory(graphMetrics), | |||
]).then( | |||
([{ analyses }, metrics, measuresHistory]) => { | |||
([{ analyses }, measuresHistory]) => { | |||
if (this.mounted) { | |||
this.setState({ | |||
analyses, | |||
graphLoading: false, | |||
initialized: true, | |||
measuresHistory, | |||
metrics: this.filterMetrics(component, metrics), | |||
}); | |||
this.fetchAllActivities(topLevelComponent); | |||
@@ -335,6 +350,7 @@ export class ProjectActivityApp extends React.PureComponent<Props, State> { | |||
}; | |||
render() { | |||
const metrics = this.filterMetrics(); | |||
return ( | |||
<ProjectActivityAppRenderer | |||
addCustomEvent={this.addCustomEvent} | |||
@@ -347,7 +363,7 @@ export class ProjectActivityApp extends React.PureComponent<Props, State> { | |||
graphLoading={!this.state.initialized || this.state.graphLoading} | |||
initializing={!this.state.initialized} | |||
measuresHistory={this.state.measuresHistory} | |||
metrics={this.state.metrics} | |||
metrics={metrics} | |||
project={this.props.component} | |||
query={this.state.query} | |||
updateQuery={this.updateQuery} | |||
@@ -393,4 +409,4 @@ function RedirectWrapper(props: Props) { | |||
return shouldRedirect ? null : <ProjectActivityApp {...props} />; | |||
} | |||
export default withComponentContext(withRouter(RedirectWrapper)); | |||
export default withComponentContext(withRouter(withMetricsContext(RedirectWrapper))); |
@@ -87,7 +87,7 @@ export default function ProjectActivityAppRenderer(props: Props) { | |||
props.project.leakPeriodDate ? parseDate(props.project.leakPeriodDate) : undefined | |||
} | |||
project={props.project} | |||
query={props.query} | |||
query={query} | |||
updateQuery={props.updateQuery} | |||
/> | |||
</div> |
@@ -1,90 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { DeleteButton, EditButton } from '../../../../components/controls/buttons'; | |||
import { mockAnalysisEvent } from '../../../../helpers/mocks/project-activity'; | |||
import { click } from '../../../../helpers/testUtils'; | |||
import { Event, EventProps } from '../Event'; | |||
import ChangeEventForm from '../forms/ChangeEventForm'; | |||
import RemoveEventForm from '../forms/RemoveEventForm'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot('default'); | |||
expect(shallowRender({ canAdmin: true })).toMatchSnapshot('with admin options'); | |||
}); | |||
it('should correctly allow deletion', () => { | |||
expect( | |||
shallowRender({ | |||
canAdmin: true, | |||
event: mockAnalysisEvent({ category: 'VERSION' }), | |||
isFirst: true, | |||
}) | |||
.find(DeleteButton) | |||
.exists() | |||
).toBe(false); | |||
expect( | |||
shallowRender({ canAdmin: true, event: mockAnalysisEvent() }).find(DeleteButton).exists() | |||
).toBe(false); | |||
expect(shallowRender({ canAdmin: true }).find(DeleteButton).exists()).toBe(true); | |||
}); | |||
it('should correctly allow edition', () => { | |||
expect(shallowRender({ canAdmin: true }).find(EditButton).exists()).toBe(true); | |||
expect(shallowRender({ canAdmin: true, isFirst: true }).find(EditButton).exists()).toBe(true); | |||
expect( | |||
shallowRender({ canAdmin: true, event: mockAnalysisEvent() }).find(EditButton).exists() | |||
).toBe(false); | |||
}); | |||
it('should correctly show edit form', () => { | |||
const wrapper = shallowRender({ canAdmin: true }); | |||
click(wrapper.find(EditButton)); | |||
const changeEventForm = wrapper.find(ChangeEventForm); | |||
expect(changeEventForm.exists()).toBe(true); | |||
changeEventForm.prop('onClose')(); | |||
expect(wrapper.find(ChangeEventForm).exists()).toBe(false); | |||
}); | |||
it('should correctly show delete form', () => { | |||
const wrapper = shallowRender({ canAdmin: true }); | |||
click(wrapper.find(DeleteButton)); | |||
const removeEventForm = wrapper.find(RemoveEventForm); | |||
expect(removeEventForm.exists()).toBe(true); | |||
removeEventForm.prop('onClose')(); | |||
expect(wrapper.find(RemoveEventForm).exists()).toBe(false); | |||
}); | |||
function shallowRender(props: Partial<EventProps> = {}) { | |||
return shallow<Event>( | |||
<Event | |||
analysisKey="foo" | |||
event={mockAnalysisEvent({ category: 'OTHER' })} | |||
onChange={jest.fn()} | |||
onDelete={jest.fn()} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -1,43 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockAnalysisEvent } from '../../../../helpers/mocks/project-activity'; | |||
import { Events, EventsProps } from '../Events'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props: Partial<EventsProps> = {}) { | |||
return shallow( | |||
<Events | |||
analysisKey="foo" | |||
events={[ | |||
mockAnalysisEvent(), | |||
mockAnalysisEvent({ category: 'VERSION' }), | |||
mockAnalysisEvent({ category: 'OTHER' }), | |||
]} | |||
onChange={jest.fn()} | |||
onDelete={jest.fn()} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -1,128 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import TimeFormatter from '../../../../components/intl/TimeFormatter'; | |||
import { mockAnalysisEvent, mockParsedAnalysis } from '../../../../helpers/mocks/project-activity'; | |||
import { click } from '../../../../helpers/testUtils'; | |||
import AddEventForm from '../forms/AddEventForm'; | |||
import RemoveAnalysisForm from '../forms/RemoveAnalysisForm'; | |||
import { ProjectActivityAnalysis, ProjectActivityAnalysisProps } from '../ProjectActivityAnalysis'; | |||
jest.mock('../../../../helpers/dates', () => ({ | |||
parseDate: () => ({ | |||
valueOf: () => 1546333200000, | |||
toISOString: () => '2019-01-01T09:00:00.000Z', | |||
}), | |||
})); | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot('default'); | |||
expect( | |||
shallowRender({ analysis: mockParsedAnalysis({ events: [mockAnalysisEvent()] }) }) | |||
).toMatchSnapshot('with events'); | |||
expect( | |||
shallowRender({ analysis: mockParsedAnalysis({ buildString: '1.0.234' }) }) | |||
).toMatchSnapshot('with build string'); | |||
expect(shallowRender({ isBaseline: true })).toMatchSnapshot('with baseline marker'); | |||
expect( | |||
shallowRender({ | |||
canAdmin: true, | |||
canCreateVersion: true, | |||
canDeleteAnalyses: true, | |||
}) | |||
).toMatchSnapshot('with admin options'); | |||
const timeFormatter = shallowRender().find(TimeFormatter).prop('children'); | |||
expect(timeFormatter!('formatted_time')).toMatchSnapshot('formatted time'); | |||
}); | |||
it('should show the correct admin options', () => { | |||
const wrapper = shallowRender({ | |||
canAdmin: true, | |||
canCreateVersion: true, | |||
canDeleteAnalyses: true, | |||
}); | |||
expect(wrapper.find('.js-add-version').exists()).toBe(true); | |||
click(wrapper.find('.js-add-version')); | |||
const addVersionForm = wrapper.find(AddEventForm); | |||
expect(addVersionForm.exists()).toBe(true); | |||
addVersionForm.prop('onClose')(); | |||
expect(wrapper.find(AddEventForm).exists()).toBe(false); | |||
expect(wrapper.find('.js-add-event').exists()).toBe(true); | |||
click(wrapper.find('.js-add-event')); | |||
const addEventForm = wrapper.find(AddEventForm); | |||
expect(addEventForm.exists()).toBe(true); | |||
addEventForm.prop('onClose')(); | |||
expect(wrapper.find(AddEventForm).exists()).toBe(false); | |||
expect(wrapper.find('.js-delete-analysis').exists()).toBe(true); | |||
click(wrapper.find('.js-delete-analysis')); | |||
const removeAnalysisForm = wrapper.find(RemoveAnalysisForm); | |||
expect(removeAnalysisForm.exists()).toBe(true); | |||
removeAnalysisForm.prop('onClose')(); | |||
expect(wrapper.find(RemoveAnalysisForm).exists()).toBe(false); | |||
}); | |||
it('should not allow the first item to be deleted', () => { | |||
expect( | |||
shallowRender({ | |||
canAdmin: true, | |||
canCreateVersion: true, | |||
canDeleteAnalyses: true, | |||
isFirst: true, | |||
}) | |||
.find('.js-delete-analysis') | |||
.exists() | |||
).toBe(false); | |||
}); | |||
it('should be clickable', () => { | |||
const date = new Date('2018-03-01T09:37:01+0100'); | |||
const updateSelectedDate = jest.fn(); | |||
const wrapper = shallowRender({ analysis: mockParsedAnalysis({ date }), updateSelectedDate }); | |||
click(wrapper); | |||
expect(updateSelectedDate).toHaveBeenCalledWith(date); | |||
}); | |||
function shallowRender(props: Partial<ProjectActivityAnalysisProps> = {}) { | |||
return shallow(createComponent(props)); | |||
} | |||
function createComponent(props: Partial<ProjectActivityAnalysisProps> = {}) { | |||
return ( | |||
<ProjectActivityAnalysis | |||
addCustomEvent={jest.fn()} | |||
addVersion={jest.fn()} | |||
analysis={mockParsedAnalysis()} | |||
canCreateVersion={false} | |||
changeEvent={jest.fn()} | |||
deleteAnalysis={jest.fn()} | |||
deleteEvent={jest.fn()} | |||
isBaseline={false} | |||
isFirst={false} | |||
selected={false} | |||
updateSelectedDate={jest.fn()} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -17,106 +17,238 @@ | |||
* 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 { screen, waitFor } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import { keyBy } from 'lodash'; | |||
import React from 'react'; | |||
import { ComponentContext } from '../../../../app/components/componentContext/ComponentContext'; | |||
import { getActivityGraph } from '../../../../components/activity-graph/utils'; | |||
import { Route } from 'react-router-dom'; | |||
import { byLabelText, byRole, byText } from 'testing-library-selector'; | |||
import { ProjectActivityServiceMock } from '../../../../api/mocks/ProjectActivityServiceMock'; | |||
import { getAllTimeMachineData } from '../../../../api/time-machine'; | |||
import { mockComponent } from '../../../../helpers/mocks/component'; | |||
import { renderApp } from '../../../../helpers/testReactTestingUtils'; | |||
import { get } from '../../../../helpers/storage'; | |||
import { mockMetric, mockPaging } from '../../../../helpers/testMocks'; | |||
import { renderAppWithComponentContext } from '../../../../helpers/testReactTestingUtils'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
import { Component } from '../../../../types/types'; | |||
import { MetricKey } from '../../../../types/metrics'; | |||
import { GraphType } from '../../../../types/project-activity'; | |||
import ProjectActivityAppContainer from '../ProjectActivityApp'; | |||
jest.mock('../../../../api/time-machine', () => { | |||
const { mockPaging } = jest.requireActual('../../../../helpers/testMocks'); | |||
return { | |||
getAllTimeMachineData: jest.fn().mockResolvedValue({ | |||
measures: [ | |||
{ | |||
metric: 'bugs', | |||
history: [{ date: '2022-01-01', value: '10' }], | |||
}, | |||
], | |||
paging: mockPaging({ total: 1 }), | |||
}), | |||
}; | |||
}); | |||
jest.mock('../../../../api/projectActivity'); | |||
jest.mock('../../../../api/metrics', () => { | |||
const { mockMetric } = jest.requireActual('../../../../helpers/testMocks'); | |||
return { | |||
getAllMetrics: jest.fn().mockResolvedValue([mockMetric()]), | |||
}; | |||
}); | |||
jest.mock('../../../../api/time-machine', () => ({ | |||
getAllTimeMachineData: jest.fn(), | |||
})); | |||
jest.mock('../../../../api/projectActivity', () => { | |||
const { mockPaging } = jest.requireActual('../../../../helpers/testMocks'); | |||
const { mockAnalysis } = jest.requireActual('../../../../helpers/mocks/project-activity'); | |||
return { | |||
...jest.requireActual('../../../../api/projectActivity'), | |||
createEvent: jest.fn(), | |||
changeEvent: jest.fn(), | |||
getProjectActivity: jest.fn().mockResolvedValue({ | |||
analyses: [mockAnalysis({ key: 'foo' })], | |||
paging: mockPaging({ total: 1 }), | |||
}), | |||
}; | |||
}); | |||
jest.mock('../../../../helpers/storage', () => ({ | |||
...jest.requireActual('../../../../helpers/storage'), | |||
get: jest.fn(), | |||
})); | |||
jest.mock('../../../../components/activity-graph/utils', () => { | |||
const actual = jest.requireActual('../../../../components/activity-graph/utils'); | |||
return { | |||
...actual, | |||
getActivityGraph: jest.fn(), | |||
}; | |||
}); | |||
let handler: ProjectActivityServiceMock; | |||
it('should render default graph', async () => { | |||
(getActivityGraph as jest.Mock).mockImplementation(() => { | |||
return { | |||
graph: 'issues', | |||
}; | |||
beforeAll(() => { | |||
handler = new ProjectActivityServiceMock(); | |||
(getAllTimeMachineData as jest.Mock).mockResolvedValue({ | |||
measures: [ | |||
{ | |||
metric: MetricKey.reliability_rating, | |||
history: handler.analysisList.map(({ date }) => ({ date, value: '2.0' })), | |||
}, | |||
{ | |||
metric: MetricKey.bugs, | |||
history: handler.analysisList.map(({ date }) => ({ date, value: '10' })), | |||
}, | |||
], | |||
paging: mockPaging(), | |||
}); | |||
}); | |||
beforeEach(jest.clearAllMocks); | |||
afterEach(() => handler.reset()); | |||
const ui = { | |||
// Graph types. | |||
graphTypeIssues: byText('project_activity.graphs.issues'), | |||
graphTypeCustom: byText('project_activity.graphs.custom'), | |||
// Add metrics. | |||
addMetricBtn: byRole('button', { name: 'project_activity.graphs.custom.add' }), | |||
reviewedHotspotsCheckbox: byRole('checkbox', { name: MetricKey.security_hotspots_reviewed }), | |||
reviewRatingCheckbox: byRole('checkbox', { name: MetricKey.security_review_rating }), | |||
// Analysis interactions. | |||
cogBtn: (id: string) => byRole('button', { name: `project_activity.analysis_X_actions.${id}` }), | |||
seeDetailsBtn: (time: string) => | |||
byRole('button', { name: `project_activity.show_analysis_X_on_graph.${time}` }), | |||
addCustomEventBtn: byRole('link', { name: 'project_activity.add_custom_event' }), | |||
addVersionEvenBtn: byRole('link', { name: 'project_activity.add_version' }), | |||
deleteAnalysisBtn: byRole('link', { name: 'project_activity.delete_analysis' }), | |||
editEventBtn: byRole('button', { name: 'project_activity.events.tooltip.edit' }), | |||
deleteEventBtn: byRole('button', { name: 'project_activity.events.tooltip.delete' }), | |||
// Event modal. | |||
nameInput: byLabelText('name'), | |||
saveBtn: byRole('button', { name: 'save' }), | |||
changeBtn: byRole('button', { name: 'change_verb' }), | |||
deleteBtn: byRole('button', { name: 'delete' }), | |||
// Misc. | |||
loading: byLabelText('loading'), | |||
baseline: byText('project_activity.new_code_period_start'), | |||
bugsPopupCell: byRole('cell', { name: 'bugs' }), | |||
}; | |||
it('should render issues as default graph', async () => { | |||
renderProjectActivityAppContainer(); | |||
expect(await screen.findByText('project_activity.graphs.issues')).toBeInTheDocument(); | |||
expect(await ui.graphTypeIssues.find()).toBeInTheDocument(); | |||
}); | |||
it('should reload custom graph from local storage', async () => { | |||
(getActivityGraph as jest.Mock).mockImplementation(() => { | |||
return { | |||
graph: 'custom', | |||
customGraphs: ['bugs', 'code_smells'], | |||
}; | |||
}); | |||
(get as jest.Mock).mockImplementation((namespace: string) => | |||
// eslint-disable-next-line jest/no-conditional-in-test | |||
namespace.includes('.custom') ? 'bugs,code_smells' : GraphType.custom | |||
); | |||
renderProjectActivityAppContainer(); | |||
expect(await screen.findByText('project_activity.graphs.custom')).toBeInTheDocument(); | |||
expect(await ui.graphTypeCustom.find()).toBeInTheDocument(); | |||
}); | |||
function renderProjectActivityAppContainer( | |||
{ component, navigateTo }: { component: Component; navigateTo?: string } = { | |||
component: mockComponent({ | |||
it.each([ | |||
['OTHER', ui.addCustomEventBtn, 'Custom event name', 'Custom event updated name'], | |||
['VERSION', ui.addVersionEvenBtn, '1.1-SNAPSHOT', '1.1--SNAPSHOT'], | |||
])( | |||
'should correctly create, update, and delete %s events', | |||
async (_, btn, initialValue, updatedValue) => { | |||
const user = userEvent.setup(); | |||
renderProjectActivityAppContainer( | |||
mockComponent({ | |||
breadcrumbs: [ | |||
{ key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project }, | |||
], | |||
configuration: { showHistory: true }, | |||
}) | |||
); | |||
await waitOnDataLoaded(); | |||
await user.click(ui.cogBtn('1.1.0.1').get()); | |||
await user.click(btn.get()); | |||
await user.type(ui.nameInput.get(), initialValue); | |||
await user.click(ui.saveBtn.get()); | |||
expect(screen.getAllByText(initialValue)[0]).toBeInTheDocument(); | |||
await user.click(ui.editEventBtn.getAll()[1]); | |||
await user.clear(ui.nameInput.get()); | |||
await user.type(ui.nameInput.get(), updatedValue); | |||
await user.click(ui.changeBtn.get()); | |||
expect(screen.getAllByText(updatedValue)[0]).toBeInTheDocument(); | |||
await user.click(ui.deleteEventBtn.getAll()[0]); | |||
await user.click(ui.deleteBtn.get()); | |||
expect(screen.queryByText(updatedValue)).not.toBeInTheDocument(); | |||
} | |||
); | |||
it('should correctly allow deletion of specific analyses', async () => { | |||
const user = userEvent.setup(); | |||
renderProjectActivityAppContainer( | |||
mockComponent({ | |||
breadcrumbs: [ | |||
{ key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project }, | |||
], | |||
}), | |||
configuration: { showHistory: true }, | |||
}) | |||
); | |||
await waitOnDataLoaded(); | |||
// Most recent analysis is not deletable. | |||
await user.click(ui.cogBtn('1.1.0.2').get()); | |||
expect(ui.deleteAnalysisBtn.query()).not.toBeInTheDocument(); | |||
await user.click(ui.cogBtn('1.1.0.1').get()); | |||
await user.click(ui.deleteAnalysisBtn.get()); | |||
await user.click(ui.deleteBtn.get()); | |||
expect(screen.queryByText('1.1.0.1')).not.toBeInTheDocument(); | |||
}); | |||
it('should correctly show the baseline marker', async () => { | |||
renderProjectActivityAppContainer( | |||
mockComponent({ | |||
leakPeriodDate: '2017-03-01T10:36:01+0100', | |||
breadcrumbs: [ | |||
{ key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project }, | |||
], | |||
}) | |||
); | |||
await waitOnDataLoaded(); | |||
expect(ui.baseline.get()).toBeInTheDocument(); | |||
}); | |||
it.each([ | |||
[ComponentQualifier.Project, ui.reviewedHotspotsCheckbox, ui.reviewRatingCheckbox], | |||
[ComponentQualifier.Portfolio, ui.reviewRatingCheckbox, ui.reviewedHotspotsCheckbox], | |||
[ComponentQualifier.SubPortfolio, ui.reviewRatingCheckbox, ui.reviewedHotspotsCheckbox], | |||
])( | |||
'should only show certain security hotspot-related metrics for a component with qualifier %s', | |||
async (qualifier, visible, invisible) => { | |||
const user = userEvent.setup(); | |||
renderProjectActivityAppContainer( | |||
mockComponent({ | |||
qualifier, | |||
breadcrumbs: [{ key: 'breadcrumb', name: 'breadcrumb', qualifier }], | |||
}) | |||
); | |||
await user.click(ui.addMetricBtn.get()); | |||
expect(visible.get()).toBeInTheDocument(); | |||
expect(invisible.query()).not.toBeInTheDocument(); | |||
} | |||
); | |||
it('should allow analyses to be clicked', async () => { | |||
const user = userEvent.setup(); | |||
renderProjectActivityAppContainer(); | |||
await waitOnDataLoaded(); | |||
expect(ui.bugsPopupCell.query()).not.toBeInTheDocument(); | |||
await user.click(ui.seeDetailsBtn('1.0.0.1').get()); | |||
expect(ui.bugsPopupCell.get()).toBeInTheDocument(); | |||
}); | |||
async function waitOnDataLoaded() { | |||
await waitFor(() => { | |||
expect(ui.loading.query()).not.toBeInTheDocument(); | |||
}); | |||
} | |||
function renderProjectActivityAppContainer( | |||
component = mockComponent({ | |||
breadcrumbs: [{ key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project }], | |||
}) | |||
) { | |||
return renderApp( | |||
return renderAppWithComponentContext( | |||
'project/activity', | |||
<ComponentContext.Provider | |||
value={{ | |||
branchLikes: [], | |||
onBranchesChange: jest.fn(), | |||
onComponentChange: jest.fn(), | |||
component, | |||
}} | |||
> | |||
<ProjectActivityAppContainer /> | |||
</ComponentContext.Provider>, | |||
{ navigateTo } | |||
() => <Route path="*" element={<ProjectActivityAppContainer />} />, | |||
{ | |||
metrics: keyBy( | |||
[ | |||
mockMetric({ key: MetricKey.bugs, type: 'INT' }), | |||
mockMetric({ key: MetricKey.security_hotspots_reviewed }), | |||
mockMetric({ key: MetricKey.security_review_rating, type: 'RATING' }), | |||
], | |||
'key' | |||
), | |||
}, | |||
{ component } | |||
); | |||
} |
@@ -1,129 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { changeEvent, createEvent } from '../../../../api/projectActivity'; | |||
import { mockComponent } from '../../../../helpers/mocks/component'; | |||
import { mockAnalysisEvent } from '../../../../helpers/mocks/project-activity'; | |||
import { mockLocation, mockMetric, mockRouter } from '../../../../helpers/testMocks'; | |||
import { waitAndUpdate } from '../../../../helpers/testUtils'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
import { MetricKey } from '../../../../types/metrics'; | |||
import { ProjectActivityApp } from '../ProjectActivityApp'; | |||
jest.mock('../../../../helpers/dates', () => ({ | |||
parseDate: jest.fn((date) => `PARSED:${date}`), | |||
})); | |||
jest.mock('../../../../api/time-machine', () => { | |||
const { mockPaging } = jest.requireActual('../../../../helpers/testMocks'); | |||
return { | |||
getAllTimeMachineData: jest.fn().mockResolvedValue({ | |||
measures: [ | |||
{ | |||
metric: 'bugs', | |||
history: [{ date: '2022-01-01', value: '10' }], | |||
}, | |||
], | |||
paging: mockPaging({ total: 1 }), | |||
}), | |||
}; | |||
}); | |||
jest.mock('../../../../api/metrics', () => { | |||
const { mockMetric } = jest.requireActual('../../../../helpers/testMocks'); | |||
return { | |||
getAllMetrics: jest.fn().mockResolvedValue([mockMetric()]), | |||
}; | |||
}); | |||
jest.mock('../../../../api/projectActivity', () => { | |||
const { mockPaging } = jest.requireActual('../../../../helpers/testMocks'); | |||
const { mockAnalysis } = jest.requireActual('../../../../helpers/mocks/project-activity'); | |||
return { | |||
...jest.requireActual('../../../../api/projectActivity'), | |||
createEvent: jest.fn(), | |||
changeEvent: jest.fn(), | |||
getProjectActivity: jest.fn().mockResolvedValue({ | |||
analyses: [mockAnalysis({ key: 'foo' })], | |||
paging: mockPaging({ total: 1 }), | |||
}), | |||
}; | |||
}); | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('should filter metric correctly', () => { | |||
const wrapper = shallowRender(); | |||
let metrics = wrapper | |||
.instance() | |||
.filterMetrics(mockComponent({ qualifier: ComponentQualifier.Project }), [ | |||
mockMetric({ key: MetricKey.bugs }), | |||
mockMetric({ key: MetricKey.security_review_rating }), | |||
]); | |||
expect(metrics).toHaveLength(1); | |||
metrics = wrapper | |||
.instance() | |||
.filterMetrics(mockComponent({ qualifier: ComponentQualifier.Portfolio }), [ | |||
mockMetric({ key: MetricKey.bugs }), | |||
mockMetric({ key: MetricKey.security_hotspots_reviewed }), | |||
]); | |||
expect(metrics).toHaveLength(1); | |||
}); | |||
it('should correctly create and update custom events', async () => { | |||
const analysisKey = 'foo'; | |||
const name = 'bar'; | |||
const newName = 'baz'; | |||
const event = mockAnalysisEvent({ name }); | |||
(createEvent as jest.Mock).mockResolvedValueOnce({ analysis: analysisKey, ...event }); | |||
(changeEvent as jest.Mock).mockResolvedValueOnce({ | |||
analysis: analysisKey, | |||
...event, | |||
name: newName, | |||
}); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
const instance = wrapper.instance(); | |||
instance.addCustomEvent(analysisKey, name); | |||
expect(createEvent).toHaveBeenCalledWith(analysisKey, name, undefined); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().analyses[0].events[0]).toEqual(event); | |||
instance.changeEvent(event.key, newName); | |||
expect(changeEvent).toHaveBeenCalledWith(event.key, newName); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().analyses[0].events[0]).toEqual({ ...event, name: newName }); | |||
}); | |||
function shallowRender(props: Partial<ProjectActivityApp['props']> = {}) { | |||
return shallow<ProjectActivityApp>( | |||
<ProjectActivityApp | |||
component={mockComponent({ breadcrumbs: [mockComponent()] })} | |||
location={mockLocation()} | |||
router={mockRouter()} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -1,98 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { DEFAULT_GRAPH } from '../../../../components/activity-graph/utils'; | |||
import { parseDate } from '../../../../helpers/dates'; | |||
import ProjectActivityAppRenderer from '../ProjectActivityAppRenderer'; | |||
const ANALYSES = [ | |||
{ | |||
key: 'A1', | |||
date: parseDate('2016-10-27T16:33:50+0200'), | |||
events: [ | |||
{ | |||
key: 'E1', | |||
category: 'VERSION', | |||
name: '6.5-SNAPSHOT', | |||
}, | |||
], | |||
}, | |||
{ | |||
key: 'A2', | |||
date: parseDate('2016-10-27T12:21:15+0200'), | |||
events: [], | |||
}, | |||
{ | |||
key: 'A3', | |||
date: parseDate('2016-10-26T12:17:29+0200'), | |||
events: [ | |||
{ | |||
key: 'E2', | |||
category: 'VERSION', | |||
name: '6.4', | |||
}, | |||
{ | |||
key: 'E3', | |||
category: 'OTHER', | |||
name: 'foo', | |||
}, | |||
], | |||
}, | |||
]; | |||
const DEFAULT_PROPS = { | |||
addCustomEvent: jest.fn().mockResolvedValue(undefined), | |||
addVersion: jest.fn().mockResolvedValue(undefined), | |||
analyses: ANALYSES, | |||
analysesLoading: false, | |||
branch: { isMain: true }, | |||
changeEvent: jest.fn().mockResolvedValue(undefined), | |||
deleteAnalysis: jest.fn().mockResolvedValue(undefined), | |||
deleteEvent: jest.fn().mockResolvedValue(undefined), | |||
graphLoading: false, | |||
initializing: false, | |||
project: { | |||
key: 'foo', | |||
leakPeriodDate: '2017-05-16T13:50:02+0200', | |||
qualifier: 'TRK', | |||
}, | |||
metrics: [{ id: '1', key: 'code_smells', name: 'Code Smells', type: 'INT' }], | |||
measuresHistory: [ | |||
{ | |||
metric: 'code_smells', | |||
history: [ | |||
{ date: parseDate('Fri Mar 04 2016 10:40:12 GMT+0100 (CET)'), value: '1749' }, | |||
{ date: parseDate('Fri Mar 04 2016 18:40:16 GMT+0100 (CET)'), value: '2286' }, | |||
], | |||
}, | |||
], | |||
query: { | |||
category: '', | |||
customMetrics: [], | |||
graph: DEFAULT_GRAPH, | |||
project: 'org.sonarsource.sonarqube:sonarqube', | |||
}, | |||
updateQuery: () => {}, | |||
}; | |||
it('should render correctly', () => { | |||
expect(shallow(<ProjectActivityAppRenderer {...DEFAULT_PROPS} />)).toMatchSnapshot(); | |||
}); |
@@ -1,85 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly: default 1`] = ` | |||
<div | |||
className="project-activity-event" | |||
> | |||
<EventInner | |||
event={ | |||
Object { | |||
"category": "OTHER", | |||
"description": "Lorem ipsum dolor sit amet", | |||
"key": "E11", | |||
"name": "Lorem ipsum", | |||
"qualityGate": Object { | |||
"failing": Array [ | |||
Object { | |||
"branch": "master", | |||
"key": "foo", | |||
"name": "Foo", | |||
}, | |||
Object { | |||
"branch": "feature/bar", | |||
"key": "bar", | |||
"name": "Bar", | |||
}, | |||
], | |||
"status": "ERROR", | |||
"stillFailing": true, | |||
}, | |||
} | |||
} | |||
/> | |||
</div> | |||
`; | |||
exports[`should render correctly: with admin options 1`] = ` | |||
<div | |||
className="project-activity-event" | |||
> | |||
<EventInner | |||
event={ | |||
Object { | |||
"category": "OTHER", | |||
"description": "Lorem ipsum dolor sit amet", | |||
"key": "E11", | |||
"name": "Lorem ipsum", | |||
"qualityGate": Object { | |||
"failing": Array [ | |||
Object { | |||
"branch": "master", | |||
"key": "foo", | |||
"name": "Foo", | |||
}, | |||
Object { | |||
"branch": "feature/bar", | |||
"key": "bar", | |||
"name": "Bar", | |||
}, | |||
], | |||
"status": "ERROR", | |||
"stillFailing": true, | |||
}, | |||
} | |||
} | |||
/> | |||
<span | |||
className="nowrap" | |||
> | |||
<EditButton | |||
aria-label="project_activity.events.tooltip.edit" | |||
className="button-small" | |||
data-test="project-activity__edit-event" | |||
onClick={[Function]} | |||
stopPropagation={true} | |||
/> | |||
<DeleteButton | |||
aria-label="project_activity.events.tooltip.delete" | |||
className="button-small" | |||
data-test="project-activity__delete-event" | |||
onClick={[Function]} | |||
stopPropagation={true} | |||
/> | |||
</span> | |||
</div> | |||
`; |
@@ -1,98 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<div | |||
className="big-spacer-top" | |||
> | |||
<Memo(Event) | |||
analysisKey="foo" | |||
event={ | |||
Object { | |||
"category": "OTHER", | |||
"description": "Lorem ipsum dolor sit amet", | |||
"key": "E11", | |||
"name": "Lorem ipsum", | |||
"qualityGate": Object { | |||
"failing": Array [ | |||
Object { | |||
"branch": "master", | |||
"key": "foo", | |||
"name": "Foo", | |||
}, | |||
Object { | |||
"branch": "feature/bar", | |||
"key": "bar", | |||
"name": "Bar", | |||
}, | |||
], | |||
"status": "ERROR", | |||
"stillFailing": true, | |||
}, | |||
} | |||
} | |||
key="E11" | |||
onChange={[MockFunction]} | |||
onDelete={[MockFunction]} | |||
/> | |||
<Memo(Event) | |||
analysisKey="foo" | |||
event={ | |||
Object { | |||
"category": "QUALITY_GATE", | |||
"description": "Lorem ipsum dolor sit amet", | |||
"key": "E11", | |||
"name": "Lorem ipsum", | |||
"qualityGate": Object { | |||
"failing": Array [ | |||
Object { | |||
"branch": "master", | |||
"key": "foo", | |||
"name": "Foo", | |||
}, | |||
Object { | |||
"branch": "feature/bar", | |||
"key": "bar", | |||
"name": "Bar", | |||
}, | |||
], | |||
"status": "ERROR", | |||
"stillFailing": true, | |||
}, | |||
} | |||
} | |||
key="E11" | |||
onChange={[MockFunction]} | |||
onDelete={[MockFunction]} | |||
/> | |||
<Memo(Event) | |||
analysisKey="foo" | |||
event={ | |||
Object { | |||
"category": "VERSION", | |||
"description": "Lorem ipsum dolor sit amet", | |||
"key": "E11", | |||
"name": "Lorem ipsum", | |||
"qualityGate": Object { | |||
"failing": Array [ | |||
Object { | |||
"branch": "master", | |||
"key": "foo", | |||
"name": "Foo", | |||
}, | |||
Object { | |||
"branch": "feature/bar", | |||
"key": "bar", | |||
"name": "Bar", | |||
}, | |||
], | |||
"status": "ERROR", | |||
"stillFailing": true, | |||
}, | |||
} | |||
} | |||
key="E11" | |||
onChange={[MockFunction]} | |||
onDelete={[MockFunction]} | |||
/> | |||
</div> | |||
`; |
@@ -44,7 +44,7 @@ exports[`should correctly filter analyses by category 1`] = ` | |||
<ul | |||
className="project-activity-analyses-list" | |||
> | |||
<Memo(ProjectActivityAnalysis) | |||
<injectIntl(ProjectActivityAnalysis) | |||
addCustomEvent={[MockFunction]} | |||
addVersion={[MockFunction]} | |||
analysis={ | |||
@@ -123,7 +123,7 @@ exports[`should correctly filter analyses by date range 1`] = ` | |||
<ul | |||
className="project-activity-analyses-list" | |||
> | |||
<Memo(ProjectActivityAnalysis) | |||
<injectIntl(ProjectActivityAnalysis) | |||
addCustomEvent={[MockFunction]} | |||
addVersion={[MockFunction]} | |||
analysis={ | |||
@@ -202,7 +202,7 @@ exports[`should render correctly: application 1`] = ` | |||
<ul | |||
className="project-activity-analyses-list" | |||
> | |||
<Memo(ProjectActivityAnalysis) | |||
<injectIntl(ProjectActivityAnalysis) | |||
addCustomEvent={[MockFunction]} | |||
addVersion={[MockFunction]} | |||
analysis={ | |||
@@ -230,7 +230,7 @@ exports[`should render correctly: application 1`] = ` | |||
selected={false} | |||
updateSelectedDate={[Function]} | |||
/> | |||
<Memo(ProjectActivityAnalysis) | |||
<injectIntl(ProjectActivityAnalysis) | |||
addCustomEvent={[MockFunction]} | |||
addVersion={[MockFunction]} | |||
analysis={ | |||
@@ -290,7 +290,7 @@ exports[`should render correctly: application 1`] = ` | |||
<ul | |||
className="project-activity-analyses-list" | |||
> | |||
<Memo(ProjectActivityAnalysis) | |||
<injectIntl(ProjectActivityAnalysis) | |||
addCustomEvent={[MockFunction]} | |||
addVersion={[MockFunction]} | |||
analysis={ | |||
@@ -339,7 +339,7 @@ exports[`should render correctly: application 1`] = ` | |||
<ul | |||
className="project-activity-analyses-list" | |||
> | |||
<Memo(ProjectActivityAnalysis) | |||
<injectIntl(ProjectActivityAnalysis) | |||
addCustomEvent={[MockFunction]} | |||
addVersion={[MockFunction]} | |||
analysis={ | |||
@@ -418,7 +418,7 @@ exports[`should render correctly: default 1`] = ` | |||
<ul | |||
className="project-activity-analyses-list" | |||
> | |||
<Memo(ProjectActivityAnalysis) | |||
<injectIntl(ProjectActivityAnalysis) | |||
addCustomEvent={[MockFunction]} | |||
addVersion={[MockFunction]} | |||
analysis={ | |||
@@ -446,7 +446,7 @@ exports[`should render correctly: default 1`] = ` | |||
selected={false} | |||
updateSelectedDate={[Function]} | |||
/> | |||
<Memo(ProjectActivityAnalysis) | |||
<injectIntl(ProjectActivityAnalysis) | |||
addCustomEvent={[MockFunction]} | |||
addVersion={[MockFunction]} | |||
analysis={ | |||
@@ -506,7 +506,7 @@ exports[`should render correctly: default 1`] = ` | |||
<ul | |||
className="project-activity-analyses-list" | |||
> | |||
<Memo(ProjectActivityAnalysis) | |||
<injectIntl(ProjectActivityAnalysis) | |||
addCustomEvent={[MockFunction]} | |||
addVersion={[MockFunction]} | |||
analysis={ | |||
@@ -555,7 +555,7 @@ exports[`should render correctly: default 1`] = ` | |||
<ul | |||
className="project-activity-analyses-list" | |||
> | |||
<Memo(ProjectActivityAnalysis) | |||
<injectIntl(ProjectActivityAnalysis) | |||
addCustomEvent={[MockFunction]} | |||
addVersion={[MockFunction]} | |||
analysis={ |
@@ -1,232 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly: default 1`] = ` | |||
<li | |||
className="project-activity-analysis bordered-top bordered-bottom" | |||
onClick={[Function]} | |||
> | |||
<div | |||
className="display-flex-center display-flex-space-between" | |||
> | |||
<div | |||
className="project-activity-time" | |||
> | |||
<TimeFormatter | |||
date={ | |||
Object { | |||
"toISOString": [Function], | |||
"valueOf": [Function], | |||
} | |||
} | |||
long={false} | |||
> | |||
<Component /> | |||
</TimeFormatter> | |||
</div> | |||
</div> | |||
</li> | |||
`; | |||
exports[`should render correctly: formatted time 1`] = ` | |||
<time | |||
className="text-middle" | |||
dateTime="2019-01-01T09:00:00.000Z" | |||
> | |||
formatted_time | |||
</time> | |||
`; | |||
exports[`should render correctly: with admin options 1`] = ` | |||
<li | |||
className="project-activity-analysis bordered-top bordered-bottom" | |||
onClick={[Function]} | |||
> | |||
<div | |||
className="display-flex-center display-flex-space-between" | |||
> | |||
<div | |||
className="project-activity-time" | |||
> | |||
<TimeFormatter | |||
date={ | |||
Object { | |||
"toISOString": [Function], | |||
"valueOf": [Function], | |||
} | |||
} | |||
long={false} | |||
> | |||
<Component /> | |||
</TimeFormatter> | |||
</div> | |||
<ClickEventBoundary> | |||
<div | |||
className="project-activity-analysis-actions big-spacer-left" | |||
> | |||
<ActionsDropdown | |||
overlayPlacement="bottom-right" | |||
small={true} | |||
toggleClassName="js-analysis-actions" | |||
> | |||
<ActionsDropdownItem | |||
className="js-add-version" | |||
onClick={[Function]} | |||
> | |||
project_activity.add_version | |||
</ActionsDropdownItem> | |||
<ActionsDropdownItem | |||
className="js-add-event" | |||
onClick={[Function]} | |||
> | |||
project_activity.add_custom_event | |||
</ActionsDropdownItem> | |||
<ActionsDropdownDivider /> | |||
<ActionsDropdownItem | |||
className="js-delete-analysis" | |||
destructive={true} | |||
onClick={[Function]} | |||
> | |||
project_activity.delete_analysis | |||
</ActionsDropdownItem> | |||
</ActionsDropdown> | |||
</div> | |||
</ClickEventBoundary> | |||
</div> | |||
</li> | |||
`; | |||
exports[`should render correctly: with baseline marker 1`] = ` | |||
<li | |||
className="project-activity-analysis bordered-top bordered-bottom" | |||
onClick={[Function]} | |||
> | |||
<div | |||
className="display-flex-center display-flex-space-between" | |||
> | |||
<div | |||
className="project-activity-time" | |||
> | |||
<TimeFormatter | |||
date={ | |||
Object { | |||
"toISOString": [Function], | |||
"valueOf": [Function], | |||
} | |||
} | |||
long={false} | |||
> | |||
<Component /> | |||
</TimeFormatter> | |||
</div> | |||
</div> | |||
<div | |||
className="baseline-marker" | |||
> | |||
<div | |||
className="wedge" | |||
/> | |||
<hr /> | |||
<div | |||
className="label display-flex-center" | |||
> | |||
project_activity.new_code_period_start | |||
<HelpTooltip | |||
className="little-spacer-left" | |||
overlay="project_activity.new_code_period_start.help" | |||
placement="top" | |||
/> | |||
</div> | |||
</div> | |||
</li> | |||
`; | |||
exports[`should render correctly: with build string 1`] = ` | |||
<li | |||
className="project-activity-analysis bordered-top bordered-bottom" | |||
onClick={[Function]} | |||
> | |||
<div | |||
className="display-flex-center display-flex-space-between" | |||
> | |||
<div | |||
className="project-activity-time" | |||
> | |||
<TimeFormatter | |||
date={ | |||
Object { | |||
"toISOString": [Function], | |||
"valueOf": [Function], | |||
} | |||
} | |||
long={false} | |||
> | |||
<Component /> | |||
</TimeFormatter> | |||
</div> | |||
<div | |||
className="flex-shrink small text-muted text-ellipsis" | |||
> | |||
project_activity.analysis_build_string_X.1.0.234 | |||
</div> | |||
</div> | |||
</li> | |||
`; | |||
exports[`should render correctly: with events 1`] = ` | |||
<li | |||
className="project-activity-analysis bordered-top bordered-bottom" | |||
onClick={[Function]} | |||
> | |||
<div | |||
className="display-flex-center display-flex-space-between" | |||
> | |||
<div | |||
className="project-activity-time" | |||
> | |||
<TimeFormatter | |||
date={ | |||
Object { | |||
"toISOString": [Function], | |||
"valueOf": [Function], | |||
} | |||
} | |||
long={false} | |||
> | |||
<Component /> | |||
</TimeFormatter> | |||
</div> | |||
</div> | |||
<Memo(Events) | |||
analysisKey="foo" | |||
events={ | |||
Array [ | |||
Object { | |||
"category": "QUALITY_GATE", | |||
"description": "Lorem ipsum dolor sit amet", | |||
"key": "E11", | |||
"name": "Lorem ipsum", | |||
"qualityGate": Object { | |||
"failing": Array [ | |||
Object { | |||
"branch": "master", | |||
"key": "foo", | |||
"name": "Foo", | |||
}, | |||
Object { | |||
"branch": "feature/bar", | |||
"key": "bar", | |||
"name": "Bar", | |||
}, | |||
], | |||
"status": "ERROR", | |||
"stillFailing": true, | |||
}, | |||
}, | |||
] | |||
} | |||
isFirst={false} | |||
onChange={[MockFunction]} | |||
onDelete={[MockFunction]} | |||
/> | |||
</li> | |||
`; |
@@ -1,72 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<ProjectActivityAppRenderer | |||
addCustomEvent={[Function]} | |||
addVersion={[Function]} | |||
analyses={Array []} | |||
analysesLoading={false} | |||
changeEvent={[Function]} | |||
deleteAnalysis={[Function]} | |||
deleteEvent={[Function]} | |||
graphLoading={true} | |||
initializing={true} | |||
measuresHistory={Array []} | |||
metrics={Array []} | |||
project={ | |||
Object { | |||
"breadcrumbs": Array [ | |||
Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"qualifier": "TRK", | |||
"qualityGate": Object { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
}, | |||
"qualityProfiles": Array [ | |||
Object { | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
], | |||
"tags": Array [], | |||
}, | |||
], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"qualifier": "TRK", | |||
"qualityGate": Object { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
}, | |||
"qualityProfiles": Array [ | |||
Object { | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
], | |||
"tags": Array [], | |||
} | |||
} | |||
query={ | |||
Object { | |||
"category": "", | |||
"customMetrics": Array [], | |||
"from": undefined, | |||
"graph": "issues", | |||
"project": "", | |||
"selectedDate": undefined, | |||
"to": undefined, | |||
} | |||
} | |||
updateQuery={[Function]} | |||
/> | |||
`; |
@@ -1,185 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<div | |||
className="page page-limited" | |||
id="project-activity" | |||
> | |||
<Suggestions | |||
suggestions="project_activity" | |||
/> | |||
<Helmet | |||
defer={false} | |||
encodeSpecialCharacters={true} | |||
prioritizeSeoTags={false} | |||
title="project_activity.page" | |||
/> | |||
<A11ySkipTarget | |||
anchor="activity_main" | |||
/> | |||
<ProjectActivityPageFilters | |||
category="" | |||
project={ | |||
Object { | |||
"key": "foo", | |||
"leakPeriodDate": "2017-05-16T13:50:02+0200", | |||
"qualifier": "TRK", | |||
} | |||
} | |||
updateQuery={[Function]} | |||
/> | |||
<div | |||
className="layout-page project-activity-page" | |||
> | |||
<div | |||
className="layout-page-side-outer project-activity-page-side-outer boxed-group" | |||
> | |||
<ProjectActivityAnalysesList | |||
addCustomEvent={[MockFunction]} | |||
addVersion={[MockFunction]} | |||
analyses={ | |||
Array [ | |||
Object { | |||
"date": 2016-10-27T14:33:50.000Z, | |||
"events": Array [ | |||
Object { | |||
"category": "VERSION", | |||
"key": "E1", | |||
"name": "6.5-SNAPSHOT", | |||
}, | |||
], | |||
"key": "A1", | |||
}, | |||
Object { | |||
"date": 2016-10-27T10:21:15.000Z, | |||
"events": Array [], | |||
"key": "A2", | |||
}, | |||
Object { | |||
"date": 2016-10-26T10:17:29.000Z, | |||
"events": Array [ | |||
Object { | |||
"category": "VERSION", | |||
"key": "E2", | |||
"name": "6.4", | |||
}, | |||
Object { | |||
"category": "OTHER", | |||
"key": "E3", | |||
"name": "foo", | |||
}, | |||
], | |||
"key": "A3", | |||
}, | |||
] | |||
} | |||
analysesLoading={false} | |||
canAdmin={false} | |||
canDeleteAnalyses={false} | |||
changeEvent={[MockFunction]} | |||
deleteAnalysis={[MockFunction]} | |||
deleteEvent={[MockFunction]} | |||
initializing={false} | |||
leakPeriodDate={2017-05-16T11:50:02.000Z} | |||
project={ | |||
Object { | |||
"key": "foo", | |||
"leakPeriodDate": "2017-05-16T13:50:02+0200", | |||
"qualifier": "TRK", | |||
} | |||
} | |||
query={ | |||
Object { | |||
"category": "", | |||
"customMetrics": Array [], | |||
"graph": "issues", | |||
"project": "org.sonarsource.sonarqube:sonarqube", | |||
} | |||
} | |||
updateQuery={[Function]} | |||
/> | |||
</div> | |||
<div | |||
className="project-activity-layout-page-main" | |||
> | |||
<ProjectActivityGraphs | |||
analyses={ | |||
Array [ | |||
Object { | |||
"date": 2016-10-27T14:33:50.000Z, | |||
"events": Array [ | |||
Object { | |||
"category": "VERSION", | |||
"key": "E1", | |||
"name": "6.5-SNAPSHOT", | |||
}, | |||
], | |||
"key": "A1", | |||
}, | |||
Object { | |||
"date": 2016-10-27T10:21:15.000Z, | |||
"events": Array [], | |||
"key": "A2", | |||
}, | |||
Object { | |||
"date": 2016-10-26T10:17:29.000Z, | |||
"events": Array [ | |||
Object { | |||
"category": "VERSION", | |||
"key": "E2", | |||
"name": "6.4", | |||
}, | |||
Object { | |||
"category": "OTHER", | |||
"key": "E3", | |||
"name": "foo", | |||
}, | |||
], | |||
"key": "A3", | |||
}, | |||
] | |||
} | |||
leakPeriodDate={2017-05-16T11:50:02.000Z} | |||
loading={false} | |||
measuresHistory={ | |||
Array [ | |||
Object { | |||
"history": Array [ | |||
Object { | |||
"date": 2016-03-04T09:40:12.000Z, | |||
"value": "1749", | |||
}, | |||
Object { | |||
"date": 2016-03-04T17:40:16.000Z, | |||
"value": "2286", | |||
}, | |||
], | |||
"metric": "code_smells", | |||
}, | |||
] | |||
} | |||
metrics={ | |||
Array [ | |||
Object { | |||
"id": "1", | |||
"key": "code_smells", | |||
"name": "Code Smells", | |||
"type": "INT", | |||
}, | |||
] | |||
} | |||
project="foo" | |||
query={ | |||
Object { | |||
"category": "", | |||
"customMetrics": Array [], | |||
"graph": "issues", | |||
"project": "org.sonarsource.sonarqube:sonarqube", | |||
} | |||
} | |||
updateQuery={[Function]} | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
`; |
@@ -55,8 +55,9 @@ export default class AddEventForm extends React.PureComponent<Props, State> { | |||
size="small" | |||
> | |||
<div className="modal-field"> | |||
<label>{translate('name')}</label> | |||
<label htmlFor="name">{translate('name')}</label> | |||
<input | |||
id="name" | |||
autoFocus={true} | |||
onChange={this.handleNameChange} | |||
type="text" |
@@ -59,8 +59,8 @@ export default class ChangeEventForm extends React.PureComponent<Props, State> { | |||
size="small" | |||
> | |||
<div className="modal-field"> | |||
<label>{translate('name')}</label> | |||
<input autoFocus={true} onChange={this.changeInput} type="text" value={name} /> | |||
<label htmlFor="name">{translate('name')}</label> | |||
<input id="name" autoFocus={true} onChange={this.changeInput} type="text" value={name} /> | |||
</div> | |||
</ConfirmModal> | |||
); |
@@ -12,11 +12,14 @@ exports[`should render correctly 1`] = ` | |||
<div | |||
className="modal-field" | |||
> | |||
<label> | |||
<label | |||
htmlFor="name" | |||
> | |||
name | |||
</label> | |||
<input | |||
autoFocus={true} | |||
id="name" | |||
onChange={[Function]} | |||
type="text" | |||
value="" |
@@ -12,11 +12,14 @@ exports[`should render correctly 1`] = ` | |||
<div | |||
className="modal-field" | |||
> | |||
<label> | |||
<label | |||
htmlFor="name" | |||
> | |||
name | |||
</label> | |||
<input | |||
autoFocus={true} | |||
id="name" | |||
onChange={[Function]} | |||
type="text" | |||
value="1.0" |
@@ -28,6 +28,7 @@ import { Button } from './buttons'; | |||
import Dropdown from './Dropdown'; | |||
export interface ActionsDropdownProps { | |||
ariaLabel?: string; | |||
className?: string; | |||
children: React.ReactNode; | |||
onOpen?: () => void; | |||
@@ -37,7 +38,7 @@ export interface ActionsDropdownProps { | |||
} | |||
export default function ActionsDropdown(props: ActionsDropdownProps) { | |||
const { children, className, overlayPlacement, small, toggleClassName } = props; | |||
const { ariaLabel, children, className, overlayPlacement, small, toggleClassName } = props; | |||
return ( | |||
<Dropdown | |||
className={className} | |||
@@ -46,6 +47,7 @@ export default function ActionsDropdown(props: ActionsDropdownProps) { | |||
overlayPlacement={overlayPlacement} | |||
> | |||
<Button | |||
aria-label={ariaLabel} | |||
className={classNames('dropdown-toggle', toggleClassName, { | |||
'button-small': small, | |||
})} |
@@ -1526,6 +1526,8 @@ project_activity.analysis_build_string_X=Build string: {0} | |||
project_activity.add_version=Create Version | |||
project_activity.analyzed.TRK=Project Analyzed | |||
project_activity.analyzed.APP=Application Analyzed | |||
project_activity.analysis_X_actions=Show actions for analysis {0} | |||
project_activity.show_analysis_X_on_graph=Show details on interactive graph for analysis {0}. Note: this data is also available as a table. Click on the button below the graph. | |||
project_activity.remove_version=Remove Version | |||
project_activity.remove_version.question=Are you sure you want to delete this version? | |||
project_activity.change_version=Change Version |