@@ -18,10 +18,14 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import throwGlobalError from '../app/utils/throwGlobalError'; | |||
import { getJSON } from '../helpers/request'; | |||
import { getJSON, postJSON } from '../helpers/request'; | |||
export function getProjectBadgesToken(project: string) { | |||
return getJSON('/api/project_badges/token', { project }) | |||
.then(({ token }) => token) | |||
.catch(throwGlobalError); | |||
} | |||
export function renewProjectBadgesToken(project: string) { | |||
return postJSON('/api/project_badges/renew_token', { project }).catch(throwGlobalError); | |||
} |
@@ -107,12 +107,7 @@ export class ProjectInformation extends React.PureComponent<Props, State> { | |||
<InfoDrawerPage | |||
displayed={page === ProjectInformationPages.badges} | |||
onPageChange={this.setPage}> | |||
<ProjectBadges | |||
branchLike={branchLike} | |||
metrics={metrics} | |||
project={component.key} | |||
qualifier={component.qualifier} | |||
/> | |||
<ProjectBadges branchLike={branchLike} metrics={metrics} component={component} /> | |||
</InfoDrawerPage> | |||
)} | |||
{canConfigureNotifications && ( |
@@ -35,6 +35,28 @@ exports[`should render correctly: default 1`] = ` | |||
onPageChange={[Function]} | |||
> | |||
<ProjectBadges | |||
component={ | |||
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 [], | |||
} | |||
} | |||
metrics={ | |||
Object { | |||
"coverage": Object { | |||
@@ -45,8 +67,6 @@ exports[`should render correctly: default 1`] = ` | |||
}, | |||
} | |||
} | |||
project="my-project" | |||
qualifier="TRK" | |||
/> | |||
</InfoDrawerPage> | |||
</Fragment> | |||
@@ -87,6 +107,28 @@ exports[`should render correctly: logged in user 1`] = ` | |||
onPageChange={[Function]} | |||
> | |||
<ProjectBadges | |||
component={ | |||
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 [], | |||
} | |||
} | |||
metrics={ | |||
Object { | |||
"coverage": Object { | |||
@@ -97,8 +139,6 @@ exports[`should render correctly: logged in user 1`] = ` | |||
}, | |||
} | |||
} | |||
project="my-project" | |||
qualifier="TRK" | |||
/> | |||
</InfoDrawerPage> | |||
<InfoDrawerPage | |||
@@ -182,6 +222,28 @@ exports[`should render correctly: measures loaded 1`] = ` | |||
onPageChange={[Function]} | |||
> | |||
<ProjectBadges | |||
component={ | |||
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 [], | |||
} | |||
} | |||
metrics={ | |||
Object { | |||
"coverage": Object { | |||
@@ -192,8 +254,6 @@ exports[`should render correctly: measures loaded 1`] = ` | |||
}, | |||
} | |||
} | |||
project="my-project" | |||
qualifier="TRK" | |||
/> | |||
</InfoDrawerPage> | |||
</Fragment> | |||
@@ -235,6 +295,29 @@ exports[`should render correctly: private 1`] = ` | |||
onPageChange={[Function]} | |||
> | |||
<ProjectBadges | |||
component={ | |||
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 [], | |||
"visibility": "private", | |||
} | |||
} | |||
metrics={ | |||
Object { | |||
"coverage": Object { | |||
@@ -245,8 +328,6 @@ exports[`should render correctly: private 1`] = ` | |||
}, | |||
} | |||
} | |||
project="my-project" | |||
qualifier="TRK" | |||
/> | |||
</InfoDrawerPage> | |||
</Fragment> |
@@ -18,9 +18,14 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { getProjectBadgesToken } from '../../../../../../api/project-badges'; | |||
import { | |||
getProjectBadgesToken, | |||
renewProjectBadgesToken | |||
} from '../../../../../../api/project-badges'; | |||
import CodeSnippet from '../../../../../../components/common/CodeSnippet'; | |||
import { Button } from '../../../../../../components/controls/buttons'; | |||
import { Alert } from '../../../../../../components/ui/Alert'; | |||
import DeferredSpinner from '../../../../../../components/ui/DeferredSpinner'; | |||
import { getBranchLikeQuery } from '../../../../../../helpers/branch-like'; | |||
import { translate } from '../../../../../../helpers/l10n'; | |||
import { BranchLike } from '../../../../../../types/branch-like'; | |||
@@ -33,11 +38,11 @@ import { BadgeOptions, BadgeType, getBadgeSnippet, getBadgeUrl } from './utils'; | |||
interface Props { | |||
branchLike?: BranchLike; | |||
metrics: T.Dict<T.Metric>; | |||
project: string; | |||
qualifier: string; | |||
component: T.Component; | |||
} | |||
interface State { | |||
isRenewing: boolean; | |||
token: string; | |||
selectedType: BadgeType; | |||
badgeOptions: BadgeOptions; | |||
@@ -46,6 +51,7 @@ interface State { | |||
export default class ProjectBadges extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { | |||
isRenewing: false, | |||
token: '', | |||
selectedType: BadgeType.measure, | |||
badgeOptions: { metric: MetricKey.alert_status } | |||
@@ -61,8 +67,10 @@ export default class ProjectBadges extends React.PureComponent<Props, State> { | |||
} | |||
async fetchToken() { | |||
const { project } = this.props; | |||
const token = await getProjectBadgesToken(project); | |||
const { | |||
component: { key } | |||
} = this.props; | |||
const token = await getProjectBadgesToken(key).catch(() => ''); | |||
if (this.mounted) { | |||
this.setState({ token }); | |||
} | |||
@@ -73,13 +81,36 @@ export default class ProjectBadges extends React.PureComponent<Props, State> { | |||
}; | |||
handleUpdateOptions = (options: Partial<BadgeOptions>) => { | |||
this.setState(state => ({ badgeOptions: { ...state.badgeOptions, ...options } })); | |||
this.setState(state => ({ | |||
badgeOptions: { ...state.badgeOptions, ...options } | |||
})); | |||
}; | |||
handleRenew = async () => { | |||
const { | |||
component: { key } | |||
} = this.props; | |||
this.setState({ isRenewing: true }); | |||
await renewProjectBadgesToken(key).catch(() => {}); | |||
await this.fetchToken(); | |||
if (this.mounted) { | |||
this.setState({ isRenewing: false }); | |||
} | |||
}; | |||
render() { | |||
const { branchLike, project, qualifier } = this.props; | |||
const { selectedType, badgeOptions, token } = this.state; | |||
const fullBadgeOptions = { project, ...badgeOptions, ...getBranchLikeQuery(branchLike) }; | |||
const { | |||
branchLike, | |||
component: { key: project, qualifier, configuration } | |||
} = this.props; | |||
const { isRenewing, selectedType, badgeOptions, token } = this.state; | |||
const fullBadgeOptions = { | |||
project, | |||
...badgeOptions, | |||
...getBranchLikeQuery(branchLike) | |||
}; | |||
const canRenew = configuration?.showSettings; | |||
return ( | |||
<div className="display-flex-column"> | |||
@@ -110,11 +141,31 @@ export default class ProjectBadges extends React.PureComponent<Props, State> { | |||
type={selectedType} | |||
updateOptions={this.handleUpdateOptions} | |||
/> | |||
<Alert variant="warning">{translate('overview.badges.leak_warning')}</Alert> | |||
<CodeSnippet | |||
isOneLine={true} | |||
snippet={getBadgeSnippet(selectedType, fullBadgeOptions, token)} | |||
/> | |||
{isRenewing ? ( | |||
<div className="spacer-top spacer-bottom display-flex-row display-flex-justify-center"> | |||
<DeferredSpinner className="spacer-top spacer-bottom" loading={isRenewing} /> | |||
</div> | |||
) : ( | |||
<CodeSnippet | |||
isOneLine={true} | |||
snippet={getBadgeSnippet(selectedType, fullBadgeOptions, token)} | |||
/> | |||
)} | |||
<Alert variant="warning"> | |||
<p> | |||
{translate('overview.badges.leak_warning')}{' '} | |||
{canRenew && translate('overview.badges.renew.description')} | |||
</p> | |||
{canRenew && ( | |||
<Button | |||
disabled={isRenewing} | |||
className="spacer-top it__project-info-renew-badge" | |||
onClick={this.handleRenew}> | |||
{translate('overview.badges.renew')} | |||
</Button> | |||
)} | |||
</Alert> | |||
</div> | |||
); | |||
} |
@@ -19,11 +19,16 @@ | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { getProjectBadgesToken } from '../../../../../../../api/project-badges'; | |||
import CodeSnippet from '../../../../../../../components/common/CodeSnippet'; | |||
import { mockBranch } from '../../../../../../../helpers/mocks/branch-like'; | |||
import { mockComponent } from '../../../../../../../helpers/mocks/component'; | |||
import { mockMetric } from '../../../../../../../helpers/testMocks'; | |||
import { waitAndUpdate } from '../../../../../../../helpers/testUtils'; | |||
import { Location } from '../../../../../../../helpers/urls'; | |||
import { ComponentQualifier } from '../../../../../../../types/component'; | |||
import { MetricKey } from '../../../../../../../types/metrics'; | |||
import BadgeButton from '../BadgeButton'; | |||
import ProjectBadges from '../ProjectBadges'; | |||
jest.mock('../../../../../../../helpers/urls', () => ({ | |||
@@ -33,7 +38,8 @@ jest.mock('../../../../../../../helpers/urls', () => ({ | |||
})); | |||
jest.mock('../../../../../../../api/project-badges', () => ({ | |||
getProjectBadgesToken: jest.fn().mockResolvedValue('foo') | |||
getProjectBadgesToken: jest.fn().mockResolvedValue('foo'), | |||
renewProjectBadgesToken: jest.fn().mockResolvedValue({}) | |||
})); | |||
it('should display correctly', async () => { | |||
@@ -42,6 +48,27 @@ it('should display correctly', async () => { | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should renew token', async () => { | |||
(getProjectBadgesToken as jest.Mock).mockResolvedValueOnce('foo').mockResolvedValueOnce('bar'); | |||
const wrapper = shallowRender({ | |||
component: mockComponent({ configuration: { showSettings: true } }) | |||
}); | |||
await waitAndUpdate(wrapper); | |||
wrapper.find('.it__project-info-renew-badge').simulate('click'); | |||
// it shoud be loading | |||
expect(wrapper.find('.it__project-info-renew-badge').props().disabled).toBe(true); | |||
await waitAndUpdate(wrapper); | |||
const buttons = wrapper.find(BadgeButton); | |||
expect(buttons.at(0).props().url).toMatch('token=bar'); | |||
expect(buttons.at(1).props().url).toMatch('token=bar'); | |||
expect(wrapper.find(CodeSnippet).props().snippet).toMatch('token=bar'); | |||
// let's check that the loading has correclty ends. | |||
expect(wrapper.find('.it__project-info-renew-badge').props().disabled).toBe(false); | |||
}); | |||
function shallowRender(overrides = {}) { | |||
return shallow( | |||
<ProjectBadges | |||
@@ -50,8 +77,7 @@ function shallowRender(overrides = {}) { | |||
[MetricKey.coverage]: mockMetric({ key: MetricKey.coverage }), | |||
[MetricKey.new_code_smells]: mockMetric({ key: MetricKey.new_code_smells }) | |||
}} | |||
project="foo" | |||
qualifier="TRK" | |||
component={mockComponent({ key: 'foo', qualifier: ComponentQualifier.Project })} | |||
{...overrides} | |||
/> | |||
); |
@@ -60,14 +60,17 @@ exports[`should display correctly 1`] = ` | |||
type="measure" | |||
updateOptions={[Function]} | |||
/> | |||
<Alert | |||
variant="warning" | |||
> | |||
overview.badges.leak_warning | |||
</Alert> | |||
<CodeSnippet | |||
isOneLine={true} | |||
snippet="[![alert_status](host/api/project_badges/measure?branch=branch-6.7&project=foo&metric=alert_status&token=foo)](/dashboard)" | |||
/> | |||
<Alert | |||
variant="warning" | |||
> | |||
<p> | |||
overview.badges.leak_warning | |||
</p> | |||
</Alert> | |||
</div> | |||
`; |
@@ -2997,7 +2997,7 @@ project_dump.pending_import=Import was scheduled on {0}, waiting to be processed | |||
project_dump.in_progress_import=Import is in progress, started {0}. | |||
project_dump.failed_import=The last import has failed. Please try once again. | |||
project_dump.import_form_description=A dump has been found on the file system for this project. You can import it by clicking on the button below. | |||
project_dump.import_form_description_disabled=Projects cannot be imported. This feature is only available starting from Enterprise Edition | |||
project_dump.import_form_description_disabled=Projects cannot be imported. This feature is only available starting from Enterprise Edition. | |||
#------------------------------------------------------------------------------ | |||
# | |||
@@ -3170,6 +3170,9 @@ overview.badges.quality_gate.description.APP=Displays the current quality gate s | |||
overview.badges.quality_gate.description.TRK=Displays the current quality gate status of your project. | |||
overview.badges.quality_gate.description.VW=Displays the current quality gate status of your portfolio. | |||
overview.badges.leak_warning=Project badges can expose your security rating and other measures. Only use project badges in trusted environments. | |||
overview.badges.renew=Renew Token | |||
overview.badges.renew.description=If your project badge security token has leaked to an unsafe environment, you can renew it: | |||
#------------------------------------------------------------------------------ | |||
# |