瀏覽代碼

SONAR-15258 Add App Report Settings

tags/9.1.0.47736
Jeremy Davis 2 年之前
父節點
當前提交
94ff5648e0

+ 18
- 0
server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx 查看文件

@@ -291,6 +291,7 @@ export class Menu extends React.PureComponent<Props> {
this.renderBranchesLink(query, isProject),
this.renderBaselineLink(query, isApplication, isPortfolio),
this.renderConsoleAppLink(query, isApplication),
this.renderReportSettingsLink(query, isApplication),
...this.renderAdminExtensions(query, isApplication),
this.renderProfilesLink(query),
this.renderQualityGateLink(query),
@@ -385,6 +386,23 @@ export class Menu extends React.PureComponent<Props> {
);
};

renderReportSettingsLink = (query: Query, isApplication: boolean) => {
const extensions = this.getConfiguration().extensions || [];
const hasGovernance = extensions.find(e => e.key === 'governance/console');

if (!isApplication || !hasGovernance) {
return null;
}

return (
<li key="report-settings">
<Link activeClassName="active" to={{ pathname: '/application/settings', query }}>
{translate('application_settings.report')}
</Link>
</li>
);
};

renderProfilesLink = (query: Query) => {
if (!this.getConfiguration().showQualityProfiles) {
return null;

+ 2
- 0
server/sonar-web/src/main/js/app/utils/startReactApp.tsx 查看文件

@@ -32,6 +32,7 @@ import getHistory from 'sonar-ui-common/helpers/getHistory';
import aboutRoutes from '../../apps/about/routes';
import accountRoutes from '../../apps/account/routes';
import applicationConsoleRoutes from '../../apps/application-console/routes';
import applicationSettingsRoutes from '../../apps/application-settings/routes';
import auditLogsRoutes from '../../apps/audit-logs/routes';
import backgroundTasksRoutes from '../../apps/background-tasks/routes';
import codeRoutes from '../../apps/code/routes';
@@ -203,6 +204,7 @@ function renderComponentRoutes() {
<RouteWithChildRoutes path="project/settings" childRoutes={settingsRoutes} />
<RouteWithChildRoutes path="project_roles" childRoutes={projectPermissionsRoutes} />
<RouteWithChildRoutes path="application/console" childRoutes={applicationConsoleRoutes} />
<RouteWithChildRoutes path="application/settings" childRoutes={applicationSettingsRoutes} />
<RouteWithChildRoutes path="project/webhooks" childRoutes={webhooksRoutes} />
<Route
path="project/deletion"

+ 136
- 0
server/sonar-web/src/main/js/apps/application-settings/ApplicationSettingsApp.tsx 查看文件

@@ -0,0 +1,136 @@
/*
* SonarQube
* Copyright (C) 2009-2021 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 * as React from 'react';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { getDefinitions, getValues, setSimpleSettingValue } from '../../api/settings';
import { SettingCategoryDefinition, SettingsKey } from '../../types/settings';
import ReportFrequencyForm from './ReportFrequencyForm';

interface Props {
component: T.Component;
}

interface State {
definition?: SettingCategoryDefinition;
frequency?: string;
loading: boolean;
}

export default class ApplicationSettingsApp extends React.PureComponent<Props, State> {
mounted = false;

state: State = {
loading: true
};

componentDidMount() {
this.mounted = true;
this.fetchInitialData();
}

async componentDidUpdate(prevProps: Props) {
if (prevProps.component.key !== this.props.component.key) {
this.setState({ loading: true });
await this.fetchSetting();
this.setState({ loading: false });
}
}

componentWillUnmount() {
this.mounted = false;
}

fetchInitialData = async () => {
this.setState({ loading: true });
await Promise.all([this.fetchDefinition(), this.fetchSetting()]);
this.setState({ loading: false });
};

fetchDefinition = async () => {
const { component } = this.props;
try {
const definitions = await getDefinitions(component.key);
const definition = definitions.find(d => d.key === SettingsKey.ProjectReportFrequency);

if (this.mounted) {
this.setState({ definition });
}
} catch (_) {
/* do nothing */
}
};

fetchSetting = async () => {
const { component } = this.props;
try {
const setting = (
await getValues({ component: component.key, keys: SettingsKey.ProjectReportFrequency })
).pop();

if (this.mounted) {
this.setState({
frequency: setting ? setting.value : undefined
});
}
} catch (_) {
/* do nothing */
}
};

handleSubmit = async (frequency: string) => {
const { component } = this.props;

try {
await setSimpleSettingValue({
component: component.key,
key: SettingsKey.ProjectReportFrequency,
value: frequency
});

this.setState({ frequency });
} catch (_) {
/* Do nothing */
}
};

render() {
const { definition, frequency, loading } = this.state;

return (
<div className="page page-limited application-settings">
<h1>{translate('application_settings.page')}</h1>
<div className="boxed-group big-padded big-spacer-top">
<DeferredSpinner loading={loading}>
<div>
{definition && frequency && (
<ReportFrequencyForm
definition={definition}
frequency={frequency}
onSave={this.handleSubmit}
/>
)}
</div>
</DeferredSpinner>
</div>
</div>
);
}
}

+ 96
- 0
server/sonar-web/src/main/js/apps/application-settings/ReportFrequencyForm.tsx 查看文件

@@ -0,0 +1,96 @@
/*
* SonarQube
* Copyright (C) 2009-2021 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 * as React from 'react';
import { Button, ResetButtonLink } from 'sonar-ui-common/components/controls/buttons';
import Select from 'sonar-ui-common/components/controls/Select';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { sanitizeStringRestricted } from '../../helpers/sanitize';
import { SettingCategoryDefinition } from '../../types/settings';

export interface ReportFrequencyFormProps {
definition: SettingCategoryDefinition;
frequency: string;
onSave: (value: string) => Promise<void>;
}

export default function ReportFrequencyForm(props: ReportFrequencyFormProps) {
const { definition, frequency } = props;
const { defaultValue } = definition;

const [currentSelection, setCurrentSelection] = React.useState(frequency);

const options = props.definition.options.map(option => ({
label: option,
value: option
}));

const handleReset = () => {
if (defaultValue) {
setCurrentSelection(defaultValue);
props.onSave(defaultValue);
}
};

return (
<div>
<h2>{translate('application_settings.report.frequency')}</h2>
{definition.description && (
<div
className="markdown"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: sanitizeStringRestricted(definition.description)
}}
/>
)}

<Select
className="input-medium"
clearable={false}
name={definition.name}
onChange={({ value }: { value: string }) => setCurrentSelection(value)}
options={options}
value={currentSelection}
/>

<div className="display-flex-center big-spacer-top">
{frequency !== currentSelection && (
<Button
className="spacer-right button-success"
onClick={() => props.onSave(currentSelection)}>
{translate('save')}
</Button>
)}

{defaultValue !== undefined && frequency !== defaultValue && (
<Button className="spacer-right" onClick={handleReset}>
{translate('reset_verb')}
</Button>
)}

{frequency !== currentSelection && (
<ResetButtonLink className="spacer-right" onClick={() => setCurrentSelection(frequency)}>
{translate('cancel')}
</ResetButtonLink>
)}
</div>
</div>
);
}

+ 70
- 0
server/sonar-web/src/main/js/apps/application-settings/__tests__/ApplicationSettingsApp-test.tsx 查看文件

@@ -0,0 +1,70 @@
/*
* SonarQube
* Copyright (C) 2009-2021 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 { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { setSimpleSettingValue } from '../../../api/settings';
import { mockComponent } from '../../../helpers/testMocks';
import { SettingsKey } from '../../../types/settings';
import ApplicationSettingsApp from '../ApplicationSettingsApp';

jest.mock('../../../api/settings', () => {
const { mockDefinition } = jest.requireActual('../../../helpers/mocks/settings');

const definition = mockDefinition({
key: 'sonar.governance.report.project.branch.frequency', // SettingsKey.ProjectReportFrequency
defaultValue: 'Monthly',
description: 'description',
options: ['Daily', 'Weekly', 'Monthly']
});

return {
getDefinitions: jest.fn().mockResolvedValue([definition]),
getValues: jest.fn().mockResolvedValue([{ value: 'Monthly' }]),
setSimpleSettingValue: jest.fn().mockResolvedValue(undefined)
};
});

it('should render correctly', async () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot('loading');
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot('default');
});

it('should handle submission', async () => {
const component = mockComponent({ key: 'app-key' });
const wrapper = shallowRender({ component });
await waitAndUpdate(wrapper);

wrapper.instance().handleSubmit('Daily');

expect(setSimpleSettingValue).toBeCalledWith({
component: component.key,
key: SettingsKey.ProjectReportFrequency,
value: 'Daily'
});
});

function shallowRender(props: Partial<ApplicationSettingsApp['props']> = {}) {
return shallow<ApplicationSettingsApp>(
<ApplicationSettingsApp component={mockComponent()} {...props} />
);
}

+ 70
- 0
server/sonar-web/src/main/js/apps/application-settings/__tests__/ReportFrequencyForm-test.tsx 查看文件

@@ -0,0 +1,70 @@
/*
* SonarQube
* Copyright (C) 2009-2021 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 { Button, ResetButtonLink } from 'sonar-ui-common/components/controls/buttons';
import Select from 'sonar-ui-common/components/controls/Select';
import { mockDefinition } from '../../../helpers/mocks/settings';
import ReportFrequencyForm, { ReportFrequencyFormProps } from '../ReportFrequencyForm';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('default');
expect(shallowRender({ frequency: 'Weekly' })).toMatchSnapshot('changed');
expect(shallowRender({ definition: mockDefinition() })).toMatchSnapshot('no description');
});

it('should handle changes', () => {
const onSave = jest.fn();
const wrapper = shallowRender({ onSave });

wrapper.find(Select).simulate('change', { value: 'Daily' });

expect(wrapper.find('.button-success').exists()).toBe(true);
expect(wrapper.find(ResetButtonLink).exists()).toBe(true);

wrapper.find(Button).simulate('click');

expect(onSave).toBeCalledWith('Daily');
});

it('should handle reset', () => {
const onSave = jest.fn();
const wrapper = shallowRender({ frequency: 'Weekly', onSave });

expect(wrapper.find('.button-success').exists()).toBe(false);
wrapper.find(Button).simulate('click');

expect(onSave).toBeCalledWith('Monthly');
});

function shallowRender(props: Partial<ReportFrequencyFormProps> = {}) {
return shallow<ReportFrequencyFormProps>(
<ReportFrequencyForm
definition={mockDefinition({
defaultValue: 'Monthly',
description: 'description',
options: ['Daily', 'Weekly', 'Monthly']
})}
frequency="Monthly"
onSave={jest.fn()}
{...props}
/>
);
}

+ 59
- 0
server/sonar-web/src/main/js/apps/application-settings/__tests__/__snapshots__/ApplicationSettingsApp-test.tsx.snap 查看文件

@@ -0,0 +1,59 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly: default 1`] = `
<div
className="page page-limited application-settings"
>
<h1>
application_settings.page
</h1>
<div
className="boxed-group big-padded big-spacer-top"
>
<DeferredSpinner
loading={false}
>
<div>
<ReportFrequencyForm
definition={
Object {
"category": "foo category",
"defaultValue": "Monthly",
"description": "description",
"fields": Array [],
"key": "sonar.governance.report.project.branch.frequency",
"options": Array [
"Daily",
"Weekly",
"Monthly",
],
"subCategory": "foo subCat",
}
}
frequency="Monthly"
onSave={[Function]}
/>
</div>
</DeferredSpinner>
</div>
</div>
`;

exports[`should render correctly: loading 1`] = `
<div
className="page page-limited application-settings"
>
<h1>
application_settings.page
</h1>
<div
className="boxed-group big-padded big-spacer-top"
>
<DeferredSpinner
loading={true}
>
<div />
</DeferredSpinner>
</div>
</div>
`;

+ 108
- 0
server/sonar-web/src/main/js/apps/application-settings/__tests__/__snapshots__/ReportFrequencyForm-test.tsx.snap 查看文件

@@ -0,0 +1,108 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly: changed 1`] = `
<div>
<h2>
application_settings.report.frequency
</h2>
<div
className="markdown"
dangerouslySetInnerHTML={
Object {
"__html": "description",
}
}
/>
<Select
className="input-medium"
clearable={false}
onChange={[Function]}
options={
Array [
Object {
"label": "Daily",
"value": "Daily",
},
Object {
"label": "Weekly",
"value": "Weekly",
},
Object {
"label": "Monthly",
"value": "Monthly",
},
]
}
value="Weekly"
/>
<div
className="display-flex-center big-spacer-top"
>
<Button
className="spacer-right"
onClick={[Function]}
>
reset_verb
</Button>
</div>
</div>
`;

exports[`should render correctly: default 1`] = `
<div>
<h2>
application_settings.report.frequency
</h2>
<div
className="markdown"
dangerouslySetInnerHTML={
Object {
"__html": "description",
}
}
/>
<Select
className="input-medium"
clearable={false}
onChange={[Function]}
options={
Array [
Object {
"label": "Daily",
"value": "Daily",
},
Object {
"label": "Weekly",
"value": "Weekly",
},
Object {
"label": "Monthly",
"value": "Monthly",
},
]
}
value="Monthly"
/>
<div
className="display-flex-center big-spacer-top"
/>
</div>
`;

exports[`should render correctly: no description 1`] = `
<div>
<h2>
application_settings.report.frequency
</h2>
<Select
className="input-medium"
clearable={false}
onChange={[Function]}
options={Array []}
value="Monthly"
/>
<div
className="display-flex-center big-spacer-top"
/>
</div>
`;

+ 28
- 0
server/sonar-web/src/main/js/apps/application-settings/routes.ts 查看文件

@@ -0,0 +1,28 @@
/*
* SonarQube
* Copyright (C) 2009-2021 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 { lazyLoadComponent } from 'sonar-ui-common/components/lazyLoadComponent';

const routes = [
{
indexRoute: { component: lazyLoadComponent(() => import('./ApplicationSettingsApp')) }
}
];

export default routes;

+ 14
- 1
server/sonar-web/src/main/js/helpers/mocks/settings.ts 查看文件

@@ -17,7 +17,20 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { Setting, SettingWithCategory } from '../../types/settings';
import { Setting, SettingCategoryDefinition, SettingWithCategory } from '../../types/settings';

export function mockDefinition(
overrides: Partial<SettingCategoryDefinition> = {}
): SettingCategoryDefinition {
return {
key: 'foo',
category: 'foo category',
fields: [],
options: [],
subCategory: 'foo subCat',
...overrides
};
}

export function mockSetting(overrides: Partial<Setting> = {}): Setting {
return {

+ 2
- 1
server/sonar-web/src/main/js/types/settings.ts 查看文件

@@ -21,7 +21,8 @@ export const enum SettingsKey {
DaysBeforeDeletingInactiveBranchesAndPRs = 'sonar.dbcleaner.daysBeforeDeletingInactiveBranchesAndPRs',
DefaultProjectVisibility = 'projects.default.visibility',
ServerBaseUrl = 'sonar.core.serverBaseURL',
PluginRiskConsent = 'sonar.plugins.risk.consent'
PluginRiskConsent = 'sonar.plugins.risk.consent',
ProjectReportFrequency = 'sonar.governance.report.project.branch.frequency'
}

export type Setting = SettingValue & { definition: SettingDefinition };

+ 3
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties 查看文件

@@ -562,6 +562,9 @@ application_console.delete_application=Delete Application
application_console.recompute=Recompute
application_console.refresh_started=Your application will be recomputed soon
application_console.do_you_want_to_delete=Are you sure that you want to delete "{0}"?
application_settings.page=Application Settings
application_settings.report=Application Report Settings
application_settings.report.frequency=Application Reports Frequency
coding_rules.page=Rules
global_permissions.page=Global Permissions
global_permissions.page.description=Grant and revoke permissions to make changes at the global level. These permissions include editing Quality Profiles, executing analysis, and performing global system administration.

Loading…
取消
儲存