}
export function searchQualityProfiles(
- parameters: SearchQualityProfilesParameters
+ parameters?: SearchQualityProfilesParameters
): Promise<SearchQualityProfilesResponse> {
return getJSON('/api/qualityprofiles/search', parameters).catch(throwGlobalError);
}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2020 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 { Helmet } from 'react-helmet-async';
-import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
-import {
- associateProject,
- dissociateProject,
- Profile,
- searchQualityProfiles
-} from '../../api/quality-profiles';
-import A11ySkipTarget from '../../app/components/a11y/A11ySkipTarget';
-import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
-import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage';
-import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization';
-import Header from './Header';
-import Table from './Table';
-
-interface Props {
- component: T.Component;
-}
-
-interface State {
- allProfiles?: Profile[];
- loading: boolean;
- profiles?: Profile[];
-}
-
-export default class QualityProfiles extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = { loading: true };
-
- componentDidMount() {
- this.mounted = true;
- if (this.checkPermissions()) {
- this.fetchProfiles();
- } else {
- handleRequiredAuthorization();
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- checkPermissions() {
- const { configuration } = this.props.component;
- const hasPermission = configuration && configuration.showQualityProfiles;
- return !!hasPermission;
- }
-
- fetchProfiles() {
- const { key, organization } = this.props.component;
- Promise.all([
- searchQualityProfiles({ organization }).then(r => r.profiles),
- searchQualityProfiles({ organization, project: key }).then(r => r.profiles)
- ]).then(
- ([allProfiles, profiles]) => {
- if (this.mounted) {
- this.setState({ loading: false, allProfiles, profiles });
- }
- },
- () => {
- if (this.mounted) {
- this.setState({ loading: false });
- }
- }
- );
- }
-
- handleChangeProfile = (oldKey: string, newKey: string) => {
- const { component } = this.props;
- const { allProfiles, profiles } = this.state;
- const oldProfile = allProfiles && allProfiles.find(profile => profile.key === oldKey);
- const newProfile = allProfiles && allProfiles.find(profile => profile.key === newKey);
-
- let request;
-
- if (newProfile) {
- if (newProfile.isDefault && oldProfile) {
- request = dissociateProject(oldProfile, component.key);
- } else {
- request = associateProject(newProfile, component.key);
- }
- }
-
- if (request) {
- return request.then(() => {
- if (this.mounted && profiles && newProfile) {
- // remove old profile, add new one
- const nextProfiles = [...profiles.filter(profile => profile.key !== oldKey), newProfile];
- this.setState({ profiles: nextProfiles });
-
- addGlobalSuccessMessage(
- translateWithParameters(
- 'project_quality_profile.successfully_updated',
- newProfile.languageName
- )
- );
- }
- });
- } else {
- return Promise.resolve();
- }
- };
-
- render() {
- if (!this.checkPermissions()) {
- return null;
- }
-
- const { allProfiles, loading, profiles } = this.state;
-
- return (
- <div className="page page-limited">
- <Suggestions suggestions="project_quality_profiles" />
- <Helmet defer={false} title={translate('project_quality_profiles.page')} />
-
- <A11ySkipTarget anchor="profiles_main" />
-
- <Header />
-
- {loading ? (
- <i className="spinner" />
- ) : (
- allProfiles &&
- profiles && (
- <Table
- allProfiles={allProfiles}
- onChangeProfile={this.handleChangeProfile}
- profiles={profiles}
- />
- )
- )}
- </div>
- );
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2020 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 HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip';
-import { translate } from 'sonar-ui-common/helpers/l10n';
-
-export default function Header() {
- return (
- <header className="page-header">
- <div className="page-title display-flex-center">
- <h1>{translate('project_quality_profiles.page')}</h1>
- <HelpTooltip
- className="spacer-left"
- overlay={
- <div className="big-padded-top big-padded-bottom">
- {translate('quality_profiles.list.projects.help')}
- </div>
- }
- />
- </div>
- <div className="page-description">
- {translate('project_quality_profiles.page.description')}
- </div>
- </header>
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2020 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 Select from 'sonar-ui-common/components/controls/Select';
-import { translate } from 'sonar-ui-common/helpers/l10n';
-import { Profile } from '../../api/quality-profiles';
-
-interface Props {
- onChangeProfile: (oldProfile: string, newProfile: string) => Promise<void>;
- possibleProfiles: Profile[];
- profile: Profile;
-}
-
-interface State {
- loading: boolean;
-}
-
-export default class ProfileRow extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = { loading: false };
-
- componentDidMount() {
- this.mounted = true;
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- stopLoading = () => {
- if (this.mounted) {
- this.setState({ loading: false });
- }
- };
-
- handleChange = (option: { value: string }) => {
- if (this.props.profile.key !== option.value) {
- this.setState({ loading: true });
- this.props
- .onChangeProfile(this.props.profile.key, option.value)
- .then(this.stopLoading, this.stopLoading);
- }
- };
-
- renderProfileName = (profileOption: { isDefault: boolean; label: string }) => {
- if (profileOption.isDefault) {
- return (
- <span>
- <strong>{translate('default')}</strong>
- {': '}
- {profileOption.label}
- </span>
- );
- }
-
- return <span>{profileOption.label}</span>;
- };
-
- renderProfileSelect() {
- const { profile, possibleProfiles } = this.props;
-
- const options = possibleProfiles.map(profile => ({
- value: profile.key,
- label: profile.name,
- isDefault: profile.isDefault
- }));
-
- return (
- <Select
- clearable={false}
- disabled={this.state.loading}
- onChange={this.handleChange}
- optionRenderer={this.renderProfileName}
- options={options}
- style={{ width: 300 }}
- value={profile.key}
- valueRenderer={this.renderProfileName}
- />
- );
- }
-
- render() {
- const { profile } = this.props;
-
- return (
- <tr data-key={profile.language}>
- <td className="thin nowrap">{profile.languageName}</td>
- <td className="thin nowrap">{this.renderProfileSelect()}</td>
- <td>{this.state.loading && <i className="spinner" />}</td>
- </tr>
- );
- }
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { differenceBy } from 'lodash';
+import * as React from 'react';
+import { translateWithParameters } from 'sonar-ui-common/helpers/l10n';
+import { isDefined } from 'sonar-ui-common/helpers/types';
+import {
+ associateProject,
+ dissociateProject,
+ getProfileProjects,
+ Profile,
+ searchQualityProfiles
+} from '../../api/quality-profiles';
+import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage';
+import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization';
+import ProjectQualityProfilesAppRenderer from './ProjectQualityProfilesAppRenderer';
+import { ProjectProfile } from './types';
+
+interface Props {
+ component: T.Component;
+}
+
+interface State {
+ allProfiles?: Profile[];
+ loading: boolean;
+ projectProfiles?: ProjectProfile[];
+ showAddLanguageModal?: boolean;
+ showProjectProfileInModal?: ProjectProfile;
+}
+
+export default class ProjectQualityProfilesApp extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = { loading: true };
+
+ componentDidMount() {
+ this.mounted = true;
+ if (this.checkPermissions()) {
+ this.fetchProfiles();
+ } else {
+ handleRequiredAuthorization();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ checkPermissions() {
+ const { configuration } = this.props.component;
+ const hasPermission = configuration && configuration.showQualityProfiles;
+ return !!hasPermission;
+ }
+
+ fetchProfiles = async () => {
+ const { component } = this.props;
+
+ const allProfiles = await searchQualityProfiles()
+ .then(({ profiles }) => profiles)
+ .catch(() => [] as Profile[]);
+
+ // We need to know if a profile was explicitly assigned to a project,
+ // even if it's the system default. For this, we need to fetch the info
+ // for each existing profile. We only keep those that were effectively
+ // selected, and discard the rest.
+ const projectProfiles = await Promise.all(
+ allProfiles.map(profile =>
+ getProfileProjects({
+ key: profile.key,
+ q: component.name,
+ selected: 'selected'
+ })
+ .then(({ results }) => ({
+ selected: Boolean(results.find(p => p.key === component.key)?.selected),
+ profile
+ }))
+ .catch(() => ({ selected: false, profile }))
+ )
+ );
+
+ const selectedProjectProfiles = projectProfiles
+ .filter(({ selected }) => selected)
+ .map(({ profile }) => ({
+ profile,
+ selected: true
+ }));
+
+ // Finally, the project uses some profiles implicitly, either inheriting
+ // from the system defaults, OR because the project wasn't re-analyzed
+ // yet (in which case the info is outdated). We also need this information.
+ const componentProfiles = differenceBy(
+ component.qualityProfiles,
+ selectedProjectProfiles.map(p => p.profile),
+ 'key'
+ )
+ // Discard languages we already have up-to-date info for.
+ .filter(({ language }) => !selectedProjectProfiles.some(p => p.profile.language === language))
+ .map(({ key }) => {
+ const profile = allProfiles.find(p => p.key === key);
+ if (profile) {
+ // If the profile is the default profile, all is good.
+ if (profile.isDefault) {
+ return { profile, selected: false };
+ } else {
+ // If it is neither the default, nor explicitly selected, it
+ // means this is outdated information. This can only mean the
+ // user wants to use the default profile, but it will only
+ // be taken into account after a new analysis. Fetch the
+ // default profile.
+ const defaultProfile = allProfiles.find(
+ p => p.isDefault && p.language === profile.language
+ );
+ return (
+ defaultProfile && {
+ profile: defaultProfile,
+ selected: false
+ }
+ );
+ }
+ } else {
+ return undefined;
+ }
+ })
+ .filter(isDefined);
+
+ if (this.mounted) {
+ this.setState({
+ allProfiles,
+ projectProfiles: [...selectedProjectProfiles, ...componentProfiles],
+ loading: false
+ });
+ }
+ };
+
+ handleOpenSetProfileModal = (showProjectProfileInModal: ProjectProfile) => {
+ this.setState({ showProjectProfileInModal });
+ };
+
+ handleOpenAddLanguageModal = () => {
+ this.setState({ showAddLanguageModal: true });
+ };
+
+ handleCloseModal = () => {
+ this.setState({ showAddLanguageModal: false, showProjectProfileInModal: undefined });
+ };
+
+ handleAddLanguage = async (key: string) => {
+ const { component } = this.props;
+ const { allProfiles = [] } = this.state;
+ const newProfile = allProfiles.find(p => p.key === key);
+
+ if (newProfile) {
+ try {
+ await associateProject(newProfile, component.key);
+
+ if (this.mounted) {
+ this.setState(({ projectProfiles = [] }) => {
+ const newProjectProfiles = [
+ ...projectProfiles,
+ {
+ profile: newProfile,
+ selected: true
+ }
+ ];
+
+ return { projectProfiles: newProjectProfiles, showAddLanguageModal: false };
+ });
+
+ addGlobalSuccessMessage(
+ translateWithParameters(
+ 'project_quality_profile.successfully_updated',
+ newProfile.languageName
+ )
+ );
+ }
+ } catch (e) {
+ if (this.mounted) {
+ this.setState({ showAddLanguageModal: false });
+ }
+ }
+ }
+ };
+
+ handleSetProfile = async (newKey: string | undefined, oldKey: string) => {
+ const { component } = this.props;
+ const { allProfiles = [], projectProfiles = [] } = this.state;
+
+ const newProfile = newKey && allProfiles.find(p => p.key === newKey);
+ const oldProjectProfile = projectProfiles.find(p => p.profile.key === oldKey);
+ const defaultProfile = allProfiles.find(
+ p => p.isDefault && p.language === oldProjectProfile?.profile.language
+ );
+
+ if (defaultProfile === undefined || oldProjectProfile === undefined) {
+ // Isn't possible. We're in a messed up state.
+ return;
+ }
+
+ let replaceProfile: Profile | undefined;
+ if (newProfile) {
+ replaceProfile = newProfile;
+
+ // Associate with the new profile.
+ try {
+ await associateProject(newProfile, component.key);
+ } catch (e) {
+ // Something went wrong. Keep the old profile in the UI.
+ replaceProfile = oldProjectProfile.profile;
+ }
+ } else if (newKey === undefined) {
+ replaceProfile = defaultProfile;
+
+ // We want to use the system default. Explicitly dissociate the project
+ // profile, if it was explicitly selected.
+ if (oldProjectProfile.selected) {
+ try {
+ await dissociateProject(oldProjectProfile.profile, component.key);
+ } catch (e) {
+ // Something went wrong. Keep the old profile in the UI.
+ replaceProfile = oldProjectProfile.profile;
+ }
+ }
+ }
+
+ if (this.mounted) {
+ const newProjectProfiles = [
+ // Remove the old profile.
+ ...projectProfiles.filter(p => p.profile.key !== oldKey),
+ // Replace with the "new" profile.
+ replaceProfile && {
+ profile: replaceProfile,
+ selected: newKey !== undefined
+ }
+ ].filter(isDefined);
+
+ this.setState({ projectProfiles: newProjectProfiles, showProjectProfileInModal: undefined });
+
+ addGlobalSuccessMessage(
+ translateWithParameters(
+ 'project_quality_profile.successfully_updated',
+ defaultProfile.languageName
+ )
+ );
+ }
+ };
+
+ render() {
+ if (!this.checkPermissions()) {
+ return null;
+ }
+
+ const {
+ allProfiles,
+ loading,
+ showProjectProfileInModal,
+ projectProfiles,
+ showAddLanguageModal
+ } = this.state;
+
+ return (
+ <ProjectQualityProfilesAppRenderer
+ allProfiles={allProfiles}
+ component={this.props.component}
+ loading={loading}
+ onAddLanguage={this.handleAddLanguage}
+ onCloseModal={this.handleCloseModal}
+ onOpenAddLanguageModal={this.handleOpenAddLanguageModal}
+ onOpenSetProfileModal={this.handleOpenSetProfileModal}
+ onSetProfile={this.handleSetProfile}
+ projectProfiles={projectProfiles}
+ showAddLanguageModal={showAddLanguageModal}
+ showProjectProfileInModal={showProjectProfileInModal}
+ />
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { groupBy, orderBy } from 'lodash';
+import * as React from 'react';
+import { Helmet } from 'react-helmet-async';
+import { Link } from 'react-router';
+import { Button } from 'sonar-ui-common/components/controls/buttons';
+import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip';
+import EditIcon from 'sonar-ui-common/components/icons/EditIcon';
+import PlusCircleIcon from 'sonar-ui-common/components/icons/PlusCircleIcon';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { Profile } from '../../api/quality-profiles';
+import A11ySkipTarget from '../../app/components/a11y/A11ySkipTarget';
+import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
+import { getRulesUrl } from '../../helpers/urls';
+import BuiltInQualityProfileBadge from '../quality-profiles/components/BuiltInQualityProfileBadge';
+import AddLanguageModal from './components/AddLanguageModal';
+import SetQualityProfileModal from './components/SetQualityProfileModal';
+import { ProjectProfile } from './types';
+
+export interface ProjectQualityProfilesAppRendererProps {
+ allProfiles?: Profile[];
+ component: T.Component;
+ loading: boolean;
+ onAddLanguage: (key: string) => Promise<void>;
+ onCloseModal: () => void;
+ onOpenAddLanguageModal: () => void;
+ onOpenSetProfileModal: (projectProfile: ProjectProfile) => void;
+ onSetProfile: (newKey: string | undefined, oldKey: string) => Promise<void>;
+ projectProfiles?: ProjectProfile[];
+ showAddLanguageModal?: boolean;
+ showProjectProfileInModal?: ProjectProfile;
+}
+
+export default function ProjectQualityProfilesAppRenderer(
+ props: ProjectQualityProfilesAppRendererProps
+) {
+ const {
+ allProfiles,
+ component,
+ loading,
+ showProjectProfileInModal,
+ projectProfiles,
+ showAddLanguageModal
+ } = props;
+
+ const profilesByLanguage = groupBy(allProfiles, 'language');
+ const orderedProfiles = orderBy(projectProfiles, p => p.profile.languageName);
+
+ return (
+ <div className="page page-limited" id="project-quality-profiles">
+ <Suggestions suggestions="project_quality_profiles" />
+ <Helmet defer={false} title={translate('project_quality_profiles.page')} />
+ <A11ySkipTarget anchor="profiles_main" />
+
+ <header className="page-header">
+ <div className="page-title display-flex-center">
+ <h1>{translate('project_quality_profiles.page')} </h1>
+ <HelpTooltip
+ className="spacer-left"
+ overlay={
+ <div className="big-padded-top big-padded-bottom">
+ {translate('quality_profiles.list.projects.help')}
+ </div>
+ }
+ />
+ </div>
+ </header>
+
+ <div className="boxed-group">
+ <h2 className="boxed-group-header">{translate('project_quality_profile.subtitle')}</h2>
+
+ <div className="boxed-group-inner">
+ <p className="big-spacer-bottom">
+ {translate('project_quality_profiles.page.description')}
+ </p>
+
+ {loading && <i className="spinner spacer-left" />}
+
+ {!loading && orderedProfiles.length > 0 && (
+ <table className="data zebra">
+ <thead>
+ <tr>
+ <th>{translate('language')}</th>
+ <th className="thin nowrap">{translate('project_quality_profile.current')}</th>
+ <th className="thin nowrap text-right">
+ {translate('coding_rules.filters.activation.active_rules')}
+ </th>
+ <th aria-label={translate('actions')} />
+ </tr>
+ </thead>
+ <tbody>
+ {orderedProfiles.map(projectProfile => {
+ const { profile, selected } = projectProfile;
+ return (
+ <tr key={profile.language}>
+ <td>{profile.languageName}</td>
+ <td className="thin nowrap">
+ <span className="display-inline-flex-center">
+ {!selected && profile.isDefault ? (
+ <em>{translate('project_quality_profile.instance_default')}</em>
+ ) : (
+ <>
+ {profile.name}
+ {profile.isBuiltIn && (
+ <BuiltInQualityProfileBadge className="spacer-left" />
+ )}
+ </>
+ )}
+ </span>
+ </td>
+ <td className="nowrap text-right">
+ <Link to={getRulesUrl({ qprofile: profile.key })}>
+ {profile.activeRuleCount}
+ </Link>
+ </td>
+ <td className="text-right">
+ <Button
+ onClick={() => {
+ props.onOpenSetProfileModal(projectProfile);
+ }}>
+ <EditIcon className="spacer-right" />
+ {translate('project_quality_profile.change_profile')}
+ </Button>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ )}
+
+ <div className="big-spacer-top">
+ <h2>{translate('project_quality_profile.add_language.title')}</h2>
+
+ <p className="spacer-top big-spacer-bottom">
+ {translate('project_quality_profile.add_language.description')}
+ </p>
+
+ <Button disabled={loading} onClick={props.onOpenAddLanguageModal}>
+ <PlusCircleIcon className="little-spacer-right" />
+ {translate('project_quality_profile.add_language.action')}
+ </Button>
+ </div>
+
+ {showProjectProfileInModal && (
+ <SetQualityProfileModal
+ availableProfiles={profilesByLanguage[showProjectProfileInModal.profile.language]}
+ component={component}
+ currentProfile={showProjectProfileInModal.profile}
+ onClose={props.onCloseModal}
+ onSubmit={props.onSetProfile}
+ usesDefault={!showProjectProfileInModal.selected}
+ />
+ )}
+
+ {showAddLanguageModal && projectProfiles && (
+ <AddLanguageModal
+ profilesByLanguage={profilesByLanguage}
+ onClose={props.onCloseModal}
+ onSubmit={props.onAddLanguage}
+ unavailableLanguages={projectProfiles.map(p => p.profile.language)}
+ />
+ )}
+ </div>
+ </div>
+ </div>
+ );
+}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2020 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 { groupBy, orderBy } from 'lodash';
-import * as React from 'react';
-import { translate } from 'sonar-ui-common/helpers/l10n';
-import { Profile } from '../../api/quality-profiles';
-import ProfileRow from './ProfileRow';
-
-interface Props {
- allProfiles: Profile[];
- profiles: Profile[];
- onChangeProfile: (oldProfile: string, newProfile: string) => Promise<void>;
-}
-
-export default function Table(props: Props) {
- const profilesByLanguage = groupBy(props.allProfiles, 'language');
- const orderedProfiles = orderBy(props.profiles, 'languageName');
-
- // set key to language to avoid destroying of component
- const profileRows = orderedProfiles.map(profile => (
- <ProfileRow
- key={profile.language}
- onChangeProfile={props.onChangeProfile}
- possibleProfiles={profilesByLanguage[profile.language]}
- profile={profile}
- />
- ));
-
- return (
- <div className="boxed-group boxed-group-inner">
- <table className="data zebra">
- <thead>
- <tr>
- <th className="thin nowrap">{translate('language')}</th>
- <th className="thin nowrap">{translate('quality_profile')}</th>
- {/* keep one empty cell for the spinner */}
- <th> </th>
- </tr>
- </thead>
- <tbody>{profileRows}</tbody>
- </table>
- </div>
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2020 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 {
- associateProject,
- dissociateProject,
- searchQualityProfiles
-} from '../../../api/quality-profiles';
-import handleRequiredAuthorization from '../../../app/utils/handleRequiredAuthorization';
-import { mockComponent, mockQualityProfile } from '../../../helpers/testMocks';
-import App from '../App';
-import Table from '../Table';
-
-beforeEach(() => jest.clearAllMocks());
-
-jest.mock('../../../api/quality-profiles', () => ({
- associateProject: jest.fn().mockResolvedValue({}),
- dissociateProject: jest.fn().mockResolvedValue({}),
- searchQualityProfiles: jest.fn().mockResolvedValue({})
-}));
-
-jest.mock('../../../app/utils/addGlobalSuccessMessage', () => ({
- default: jest.fn()
-}));
-
-jest.mock('../../../app/utils/handleRequiredAuthorization', () => ({
- default: jest.fn()
-}));
-
-const component = mockComponent({ configuration: { showQualityProfiles: true } });
-
-it('checks permissions', () => {
- shallowRender({ component: { ...component, configuration: undefined } });
- expect(handleRequiredAuthorization).toBeCalled();
-});
-
-it('fetches profiles', () => {
- shallowRender();
- expect(searchQualityProfiles).toHaveBeenCalledTimes(2);
- expect(searchQualityProfiles).toBeCalledWith({ organization: component.organization });
- expect(searchQualityProfiles).toBeCalledWith({
- organization: component.organization,
- project: component.key
- });
-});
-
-it('changes profile', () => {
- const wrapper = shallowRender();
-
- const fooJava = mockQualityProfile({ key: 'foo-java', language: 'java' });
- const fooJs = mockQualityProfile({ key: 'foo-js', language: 'js' });
- const bar = mockQualityProfile({ key: 'bar-java', language: 'java' });
- const baz = mockQualityProfile({ key: 'baz-java', language: 'java', isDefault: true });
- const allProfiles = [fooJava, bar, baz, fooJs];
- const profiles = [fooJava, fooJs];
- wrapper.setState({ allProfiles, loading: false, profiles });
-
- wrapper
- .find(Table)
- .props()
- .onChangeProfile(fooJava.key, bar.key);
- expect(associateProject).toBeCalledWith(bar, component.key);
-
- wrapper
- .find(Table)
- .props()
- .onChangeProfile(fooJava.key, baz.key);
- expect(dissociateProject).toBeCalledWith(fooJava, component.key);
-});
-
-function shallowRender(props: Partial<App['props']> = {}) {
- return shallow<App>(<App component={component} {...props} />);
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2020 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 Header from '../Header';
-
-it('renders', () => {
- expect(shallow(<Header />)).toMatchSnapshot();
-});
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2020 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 ProfileRow from '../ProfileRow';
-
-it('renders', () => {
- expect(
- shallow(
- <ProfileRow
- onChangeProfile={jest.fn()}
- possibleProfiles={[randomProfile('bar'), randomProfile('baz')]}
- profile={randomProfile('foo')}
- />
- )
- ).toMatchSnapshot();
-});
-
-it('changes profile', async () => {
- const onChangeProfile = jest.fn(() => Promise.resolve());
- const wrapper = shallow<ProfileRow>(
- <ProfileRow
- onChangeProfile={onChangeProfile}
- possibleProfiles={[randomProfile('bar'), randomProfile('baz')]}
- profile={randomProfile('foo')}
- />
- );
- (wrapper.instance() as ProfileRow).mounted = true;
- wrapper.find('Select').prop<Function>('onChange')({ value: 'baz' });
- expect(onChangeProfile).toBeCalledWith('foo', 'baz');
- expect(wrapper.state().loading).toBe(true);
- await new Promise(setImmediate);
- expect(wrapper.state().loading).toBe(false);
-});
-
-function randomProfile(key: string) {
- return {
- activeRuleCount: 17,
- activeDeprecatedRuleCount: 0,
- key,
- name: key,
- language: 'xoo',
- languageName: 'xoo',
- organization: 'org'
- };
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 {
+ associateProject,
+ dissociateProject,
+ getProfileProjects,
+ ProfileProject,
+ searchQualityProfiles
+} from '../../../api/quality-profiles';
+import handleRequiredAuthorization from '../../../app/utils/handleRequiredAuthorization';
+import { mockComponent } from '../../../helpers/testMocks';
+import ProjectQualityProfilesApp from '../ProjectQualityProfilesApp';
+
+jest.mock('../../../api/quality-profiles', () => {
+ const { mockQualityProfile } = jest.requireActual('../../../helpers/testMocks');
+
+ return {
+ associateProject: jest.fn().mockResolvedValue({}),
+ dissociateProject: jest.fn().mockResolvedValue({}),
+ searchQualityProfiles: jest.fn().mockResolvedValue({
+ profiles: [
+ mockQualityProfile({ key: 'css', language: 'css' }),
+ mockQualityProfile({ key: 'css2', language: 'css' }),
+ mockQualityProfile({ key: 'css_default', language: 'css', isDefault: true }),
+ mockQualityProfile({ key: 'java', language: 'java' }),
+ mockQualityProfile({ key: 'java_default', language: 'java', isDefault: true }),
+ mockQualityProfile({ key: 'js', language: 'js' }),
+ mockQualityProfile({ key: 'js_default', language: 'js', isDefault: true }),
+ mockQualityProfile({ key: 'ts_default', language: 'ts', isDefault: true }),
+ mockQualityProfile({ key: 'html', language: 'html' }),
+ mockQualityProfile({ key: 'html_default', language: 'html', isDefault: true })
+ ]
+ }),
+ getProfileProjects: jest.fn(({ key }) => {
+ const results: ProfileProject[] = [];
+ if (key === 'js' || key === 'css' || key === 'html_default') {
+ results.push({
+ id: 1,
+ key: 'foo',
+ name: 'Foo',
+ selected: true
+ });
+ } else if (key === 'html') {
+ results.push({
+ id: 2,
+ key: 'foobar',
+ name: 'FooBar',
+ selected: true
+ });
+ }
+ return Promise.resolve({ results });
+ })
+ };
+});
+
+jest.mock('../../../app/utils/addGlobalSuccessMessage', () => ({
+ default: jest.fn()
+}));
+
+jest.mock('../../../app/utils/handleRequiredAuthorization', () => ({
+ default: jest.fn()
+}));
+
+beforeEach(jest.clearAllMocks);
+
+it('renders correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('correctly checks permissions', () => {
+ const wrapper = shallowRender({
+ component: mockComponent({ configuration: { showQualityProfiles: false } })
+ });
+ expect(wrapper.type()).toBeNull();
+ expect(handleRequiredAuthorization).toBeCalled();
+});
+
+it('correctly fetches and treats profile data', async () => {
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+
+ expect(searchQualityProfiles).toBeCalled();
+ expect(getProfileProjects).toBeCalledTimes(10);
+
+ expect(wrapper.state().projectProfiles).toEqual([
+ expect.objectContaining({
+ profile: expect.objectContaining({ key: 'css' }),
+ selected: true
+ }),
+ expect.objectContaining({
+ profile: expect.objectContaining({ key: 'js' }),
+ selected: true
+ }),
+ expect.objectContaining({
+ profile: expect.objectContaining({ key: 'html_default' }),
+ selected: true
+ }),
+ expect.objectContaining({
+ profile: expect.objectContaining({ key: 'ts_default' }),
+ selected: false
+ })
+ ]);
+});
+
+it('correctly sets a profile', async () => {
+ const wrapper = shallowRender();
+ const instance = wrapper.instance();
+ await waitAndUpdate(wrapper);
+
+ // Dissociate a selected profile.
+ instance.handleSetProfile(undefined, 'css');
+ expect(dissociateProject).toHaveBeenLastCalledWith(
+ expect.objectContaining({ key: 'css' }),
+ 'foo'
+ );
+ await waitAndUpdate(wrapper);
+ expect(wrapper.state().projectProfiles).toEqual(
+ expect.arrayContaining([
+ {
+ profile: expect.objectContaining({ key: 'css_default' }),
+ // It's not explicitly selected, as we're inheriting the default.
+ selected: false
+ }
+ ])
+ );
+
+ // Associate a new profile.
+ instance.handleSetProfile('css2', 'css_default');
+ expect(associateProject).toHaveBeenLastCalledWith(
+ expect.objectContaining({ key: 'css2' }),
+ 'foo'
+ );
+ await waitAndUpdate(wrapper);
+ expect(wrapper.state().projectProfiles).toEqual(
+ expect.arrayContaining([
+ {
+ profile: expect.objectContaining({ key: 'css2' }),
+ // It's explicitly selected.
+ selected: true
+ }
+ ])
+ );
+
+ // Dissociate a default profile that was inherited.
+ (dissociateProject as jest.Mock).mockClear();
+ instance.handleSetProfile(undefined, 'ts_default');
+ // It won't call the WS.
+ expect(dissociateProject).not.toBeCalled();
+
+ // Associate a default profile that was already inherited.
+ instance.handleSetProfile('ts_default', 'ts_default');
+ expect(associateProject).toHaveBeenLastCalledWith(
+ expect.objectContaining({ key: 'ts_default' }),
+ 'foo'
+ );
+ await waitAndUpdate(wrapper);
+ expect(wrapper.state().projectProfiles).toEqual(
+ expect.arrayContaining([
+ {
+ profile: expect.objectContaining({ key: 'ts_default' }),
+ // It's explicitly selected, even though it is the default profile.
+ selected: true
+ }
+ ])
+ );
+});
+
+it('correctly adds a new language', async () => {
+ const wrapper = shallowRender();
+ const instance = wrapper.instance();
+ await waitAndUpdate(wrapper);
+
+ instance.handleAddLanguage('java');
+ expect(associateProject).toHaveBeenLastCalledWith(
+ expect.objectContaining({ key: 'java' }),
+ 'foo'
+ );
+ await waitAndUpdate(wrapper);
+ expect(wrapper.state().projectProfiles).toEqual(
+ expect.arrayContaining([
+ {
+ profile: expect.objectContaining({ key: 'java' }),
+ // It must be explicitly selected. Adding an unanalyzed language can
+ // only happen by explicitly choosing a profile.
+ selected: true
+ }
+ ])
+ );
+});
+
+it('correctly handles WS errors', async () => {
+ (searchQualityProfiles as jest.Mock).mockRejectedValueOnce(null);
+ (getProfileProjects as jest.Mock).mockRejectedValueOnce(null);
+
+ const wrapper = shallowRender();
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper.state().allProfiles).toHaveLength(0);
+ expect(wrapper.state().projectProfiles).toHaveLength(0);
+ expect(wrapper.state().loading).toBe(false);
+});
+
+function shallowRender(props: Partial<ProjectQualityProfilesApp['props']> = {}) {
+ return shallow<ProjectQualityProfilesApp>(
+ <ProjectQualityProfilesApp
+ component={mockComponent({
+ key: 'foo',
+ configuration: { showQualityProfiles: true },
+ qualityProfiles: [
+ { key: 'css2', name: 'CSS 2', language: 'css' },
+ { key: 'js', name: 'JS', language: 'js' },
+ { key: 'ts_default', name: 'TS (default)', language: 'ts' },
+ { key: 'html', name: 'HTML', language: 'html' }
+ ]
+ })}
+ {...props}
+ />
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { mockComponent, mockQualityProfile } from '../../../helpers/testMocks';
+import ProjectQualityProfilesAppRenderer, {
+ ProjectQualityProfilesAppRendererProps
+} from '../ProjectQualityProfilesAppRenderer';
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot('default');
+ expect(shallowRender({ loading: true })).toMatchSnapshot('loading');
+ expect(
+ shallowRender({
+ showProjectProfileInModal: {
+ profile: mockQualityProfile({ key: 'foo', language: 'js' }),
+ selected: false
+ }
+ })
+ ).toMatchSnapshot('open profile');
+ expect(shallowRender({ showAddLanguageModal: true })).toMatchSnapshot('add language');
+});
+
+function shallowRender(props: Partial<ProjectQualityProfilesAppRendererProps> = {}) {
+ return shallow<ProjectQualityProfilesAppRendererProps>(
+ <ProjectQualityProfilesAppRenderer
+ allProfiles={[
+ mockQualityProfile({ key: 'foo', language: 'js' }),
+ mockQualityProfile({ key: 'bar', language: 'css' }),
+ mockQualityProfile({ key: 'baz', language: 'html' })
+ ]}
+ component={mockComponent()}
+ loading={false}
+ onAddLanguage={jest.fn()}
+ onCloseModal={jest.fn()}
+ onOpenAddLanguageModal={jest.fn()}
+ onOpenSetProfileModal={jest.fn()}
+ onSetProfile={jest.fn()}
+ projectProfiles={[
+ {
+ profile: mockQualityProfile({
+ key: 'foo',
+ name: 'Foo',
+ isDefault: true,
+ language: 'js',
+ languageName: 'JS'
+ }),
+ selected: false
+ },
+ {
+ profile: mockQualityProfile({
+ key: 'bar',
+ name: 'Bar',
+ isDefault: true,
+ language: 'css',
+ languageName: 'CSS'
+ }),
+ selected: false
+ },
+ {
+ profile: mockQualityProfile({
+ key: 'baz',
+ name: 'Baz',
+ language: 'html',
+ languageName: 'HTML'
+ }),
+ selected: true
+ }
+ ]}
+ {...props}
+ />
+ );
+}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2020 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 Table from '../Table';
-
-it('renders', () => {
- const fooJava = randomProfile('foo-java', 'java');
- const fooJs = randomProfile('foo-js', 'js');
- const allProfiles = [
- fooJava,
- randomProfile('bar-java', 'java'),
- randomProfile('baz-java', 'java'),
- fooJs
- ];
- const profiles = [fooJava, fooJs];
- expect(
- shallow(<Table allProfiles={allProfiles} onChangeProfile={jest.fn()} profiles={profiles} />)
- ).toMatchSnapshot();
-});
-
-function randomProfile(key: string, language: string) {
- return {
- activeRuleCount: 17,
- activeDeprecatedRuleCount: 0,
- key,
- name: key,
- language,
- languageName: language,
- organization: 'org'
- };
-}
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<header
- className="page-header"
->
- <div
- className="page-title display-flex-center"
- >
- <h1>
- project_quality_profiles.page
- </h1>
- <HelpTooltip
- className="spacer-left"
- overlay={
- <div
- className="big-padded-top big-padded-bottom"
- >
- quality_profiles.list.projects.help
- </div>
- }
- />
- </div>
- <div
- className="page-description"
- >
- project_quality_profiles.page.description
- </div>
-</header>
-`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<tr
- data-key="xoo"
->
- <td
- className="thin nowrap"
- >
- xoo
- </td>
- <td
- className="thin nowrap"
- >
- <Select
- clearable={false}
- disabled={false}
- onChange={[Function]}
- optionRenderer={[Function]}
- options={
- Array [
- Object {
- "isDefault": undefined,
- "label": "bar",
- "value": "bar",
- },
- Object {
- "isDefault": undefined,
- "label": "baz",
- "value": "baz",
- },
- ]
- }
- style={
- Object {
- "width": 300,
- }
- }
- value="foo"
- valueRenderer={[Function]}
- />
- </td>
- <td />
-</tr>
-`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders correctly 1`] = `
+<ProjectQualityProfilesAppRenderer
+ component={
+ Object {
+ "breadcrumbs": Array [],
+ "configuration": Object {
+ "showQualityProfiles": true,
+ },
+ "key": "foo",
+ "name": "MyProject",
+ "organization": "foo",
+ "qualifier": "TRK",
+ "qualityGate": Object {
+ "isDefault": true,
+ "key": "30",
+ "name": "Sonar way",
+ },
+ "qualityProfiles": Array [
+ Object {
+ "key": "css2",
+ "language": "css",
+ "name": "CSS 2",
+ },
+ Object {
+ "key": "js",
+ "language": "js",
+ "name": "JS",
+ },
+ Object {
+ "key": "ts_default",
+ "language": "ts",
+ "name": "TS (default)",
+ },
+ Object {
+ "key": "html",
+ "language": "html",
+ "name": "HTML",
+ },
+ ],
+ "tags": Array [],
+ }
+ }
+ loading={true}
+ onAddLanguage={[Function]}
+ onCloseModal={[Function]}
+ onOpenAddLanguageModal={[Function]}
+ onOpenSetProfileModal={[Function]}
+ onSetProfile={[Function]}
+/>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: add language 1`] = `
+<div
+ className="page page-limited"
+ id="project-quality-profiles"
+>
+ <Suggestions
+ suggestions="project_quality_profiles"
+ />
+ <Helmet
+ defer={false}
+ encodeSpecialCharacters={true}
+ title="project_quality_profiles.page"
+ />
+ <A11ySkipTarget
+ anchor="profiles_main"
+ />
+ <header
+ className="page-header"
+ >
+ <div
+ className="page-title display-flex-center"
+ >
+ <h1>
+ project_quality_profiles.page
+
+ </h1>
+ <HelpTooltip
+ className="spacer-left"
+ overlay={
+ <div
+ className="big-padded-top big-padded-bottom"
+ >
+ quality_profiles.list.projects.help
+ </div>
+ }
+ />
+ </div>
+ </header>
+ <div
+ className="boxed-group"
+ >
+ <h2
+ className="boxed-group-header"
+ >
+ project_quality_profile.subtitle
+ </h2>
+ <div
+ className="boxed-group-inner"
+ >
+ <p
+ className="big-spacer-bottom"
+ >
+ project_quality_profiles.page.description
+ </p>
+ <table
+ className="data zebra"
+ >
+ <thead>
+ <tr>
+ <th>
+ language
+ </th>
+ <th
+ className="thin nowrap"
+ >
+ project_quality_profile.current
+ </th>
+ <th
+ className="thin nowrap text-right"
+ >
+ coding_rules.filters.activation.active_rules
+ </th>
+ <th
+ aria-label="actions"
+ />
+ </tr>
+ </thead>
+ <tbody>
+ <tr
+ key="css"
+ >
+ <td>
+ CSS
+ </td>
+ <td
+ className="thin nowrap"
+ >
+ <span
+ className="display-inline-flex-center"
+ >
+ <em>
+ project_quality_profile.instance_default
+ </em>
+ </span>
+ </td>
+ <td
+ className="nowrap text-right"
+ >
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/coding_rules",
+ "query": Object {
+ "qprofile": "bar",
+ },
+ }
+ }
+ >
+ 10
+ </Link>
+ </td>
+ <td
+ className="text-right"
+ >
+ <Button
+ onClick={[Function]}
+ >
+ <EditIcon
+ className="spacer-right"
+ />
+ project_quality_profile.change_profile
+ </Button>
+ </td>
+ </tr>
+ <tr
+ key="html"
+ >
+ <td>
+ HTML
+ </td>
+ <td
+ className="thin nowrap"
+ >
+ <span
+ className="display-inline-flex-center"
+ >
+ Baz
+ </span>
+ </td>
+ <td
+ className="nowrap text-right"
+ >
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/coding_rules",
+ "query": Object {
+ "qprofile": "baz",
+ },
+ }
+ }
+ >
+ 10
+ </Link>
+ </td>
+ <td
+ className="text-right"
+ >
+ <Button
+ onClick={[Function]}
+ >
+ <EditIcon
+ className="spacer-right"
+ />
+ project_quality_profile.change_profile
+ </Button>
+ </td>
+ </tr>
+ <tr
+ key="js"
+ >
+ <td>
+ JS
+ </td>
+ <td
+ className="thin nowrap"
+ >
+ <span
+ className="display-inline-flex-center"
+ >
+ <em>
+ project_quality_profile.instance_default
+ </em>
+ </span>
+ </td>
+ <td
+ className="nowrap text-right"
+ >
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/coding_rules",
+ "query": Object {
+ "qprofile": "foo",
+ },
+ }
+ }
+ >
+ 10
+ </Link>
+ </td>
+ <td
+ className="text-right"
+ >
+ <Button
+ onClick={[Function]}
+ >
+ <EditIcon
+ className="spacer-right"
+ />
+ project_quality_profile.change_profile
+ </Button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <div
+ className="big-spacer-top"
+ >
+ <h2>
+ project_quality_profile.add_language.title
+ </h2>
+ <p
+ className="spacer-top big-spacer-bottom"
+ >
+ project_quality_profile.add_language.description
+ </p>
+ <Button
+ disabled={false}
+ onClick={[MockFunction]}
+ >
+ <PlusCircleIcon
+ className="little-spacer-right"
+ />
+ project_quality_profile.add_language.action
+ </Button>
+ </div>
+ <Connect(AddLanguageModal)
+ onClose={[MockFunction]}
+ onSubmit={[MockFunction]}
+ profilesByLanguage={
+ Object {
+ "css": Array [
+ Object {
+ "activeDeprecatedRuleCount": 2,
+ "activeRuleCount": 10,
+ "childrenCount": 0,
+ "depth": 1,
+ "isBuiltIn": false,
+ "isDefault": false,
+ "isInherited": false,
+ "key": "bar",
+ "language": "css",
+ "languageName": "JavaScript",
+ "name": "name",
+ "organization": "foo",
+ "projectCount": 3,
+ },
+ ],
+ "html": Array [
+ Object {
+ "activeDeprecatedRuleCount": 2,
+ "activeRuleCount": 10,
+ "childrenCount": 0,
+ "depth": 1,
+ "isBuiltIn": false,
+ "isDefault": false,
+ "isInherited": false,
+ "key": "baz",
+ "language": "html",
+ "languageName": "JavaScript",
+ "name": "name",
+ "organization": "foo",
+ "projectCount": 3,
+ },
+ ],
+ "js": Array [
+ Object {
+ "activeDeprecatedRuleCount": 2,
+ "activeRuleCount": 10,
+ "childrenCount": 0,
+ "depth": 1,
+ "isBuiltIn": false,
+ "isDefault": false,
+ "isInherited": false,
+ "key": "foo",
+ "language": "js",
+ "languageName": "JavaScript",
+ "name": "name",
+ "organization": "foo",
+ "projectCount": 3,
+ },
+ ],
+ }
+ }
+ unavailableLanguages={
+ Array [
+ "js",
+ "css",
+ "html",
+ ]
+ }
+ />
+ </div>
+ </div>
+</div>
+`;
+
+exports[`should render correctly: default 1`] = `
+<div
+ className="page page-limited"
+ id="project-quality-profiles"
+>
+ <Suggestions
+ suggestions="project_quality_profiles"
+ />
+ <Helmet
+ defer={false}
+ encodeSpecialCharacters={true}
+ title="project_quality_profiles.page"
+ />
+ <A11ySkipTarget
+ anchor="profiles_main"
+ />
+ <header
+ className="page-header"
+ >
+ <div
+ className="page-title display-flex-center"
+ >
+ <h1>
+ project_quality_profiles.page
+
+ </h1>
+ <HelpTooltip
+ className="spacer-left"
+ overlay={
+ <div
+ className="big-padded-top big-padded-bottom"
+ >
+ quality_profiles.list.projects.help
+ </div>
+ }
+ />
+ </div>
+ </header>
+ <div
+ className="boxed-group"
+ >
+ <h2
+ className="boxed-group-header"
+ >
+ project_quality_profile.subtitle
+ </h2>
+ <div
+ className="boxed-group-inner"
+ >
+ <p
+ className="big-spacer-bottom"
+ >
+ project_quality_profiles.page.description
+ </p>
+ <table
+ className="data zebra"
+ >
+ <thead>
+ <tr>
+ <th>
+ language
+ </th>
+ <th
+ className="thin nowrap"
+ >
+ project_quality_profile.current
+ </th>
+ <th
+ className="thin nowrap text-right"
+ >
+ coding_rules.filters.activation.active_rules
+ </th>
+ <th
+ aria-label="actions"
+ />
+ </tr>
+ </thead>
+ <tbody>
+ <tr
+ key="css"
+ >
+ <td>
+ CSS
+ </td>
+ <td
+ className="thin nowrap"
+ >
+ <span
+ className="display-inline-flex-center"
+ >
+ <em>
+ project_quality_profile.instance_default
+ </em>
+ </span>
+ </td>
+ <td
+ className="nowrap text-right"
+ >
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/coding_rules",
+ "query": Object {
+ "qprofile": "bar",
+ },
+ }
+ }
+ >
+ 10
+ </Link>
+ </td>
+ <td
+ className="text-right"
+ >
+ <Button
+ onClick={[Function]}
+ >
+ <EditIcon
+ className="spacer-right"
+ />
+ project_quality_profile.change_profile
+ </Button>
+ </td>
+ </tr>
+ <tr
+ key="html"
+ >
+ <td>
+ HTML
+ </td>
+ <td
+ className="thin nowrap"
+ >
+ <span
+ className="display-inline-flex-center"
+ >
+ Baz
+ </span>
+ </td>
+ <td
+ className="nowrap text-right"
+ >
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/coding_rules",
+ "query": Object {
+ "qprofile": "baz",
+ },
+ }
+ }
+ >
+ 10
+ </Link>
+ </td>
+ <td
+ className="text-right"
+ >
+ <Button
+ onClick={[Function]}
+ >
+ <EditIcon
+ className="spacer-right"
+ />
+ project_quality_profile.change_profile
+ </Button>
+ </td>
+ </tr>
+ <tr
+ key="js"
+ >
+ <td>
+ JS
+ </td>
+ <td
+ className="thin nowrap"
+ >
+ <span
+ className="display-inline-flex-center"
+ >
+ <em>
+ project_quality_profile.instance_default
+ </em>
+ </span>
+ </td>
+ <td
+ className="nowrap text-right"
+ >
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/coding_rules",
+ "query": Object {
+ "qprofile": "foo",
+ },
+ }
+ }
+ >
+ 10
+ </Link>
+ </td>
+ <td
+ className="text-right"
+ >
+ <Button
+ onClick={[Function]}
+ >
+ <EditIcon
+ className="spacer-right"
+ />
+ project_quality_profile.change_profile
+ </Button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <div
+ className="big-spacer-top"
+ >
+ <h2>
+ project_quality_profile.add_language.title
+ </h2>
+ <p
+ className="spacer-top big-spacer-bottom"
+ >
+ project_quality_profile.add_language.description
+ </p>
+ <Button
+ disabled={false}
+ onClick={[MockFunction]}
+ >
+ <PlusCircleIcon
+ className="little-spacer-right"
+ />
+ project_quality_profile.add_language.action
+ </Button>
+ </div>
+ </div>
+ </div>
+</div>
+`;
+
+exports[`should render correctly: loading 1`] = `
+<div
+ className="page page-limited"
+ id="project-quality-profiles"
+>
+ <Suggestions
+ suggestions="project_quality_profiles"
+ />
+ <Helmet
+ defer={false}
+ encodeSpecialCharacters={true}
+ title="project_quality_profiles.page"
+ />
+ <A11ySkipTarget
+ anchor="profiles_main"
+ />
+ <header
+ className="page-header"
+ >
+ <div
+ className="page-title display-flex-center"
+ >
+ <h1>
+ project_quality_profiles.page
+
+ </h1>
+ <HelpTooltip
+ className="spacer-left"
+ overlay={
+ <div
+ className="big-padded-top big-padded-bottom"
+ >
+ quality_profiles.list.projects.help
+ </div>
+ }
+ />
+ </div>
+ </header>
+ <div
+ className="boxed-group"
+ >
+ <h2
+ className="boxed-group-header"
+ >
+ project_quality_profile.subtitle
+ </h2>
+ <div
+ className="boxed-group-inner"
+ >
+ <p
+ className="big-spacer-bottom"
+ >
+ project_quality_profiles.page.description
+ </p>
+ <i
+ className="spinner spacer-left"
+ />
+ <div
+ className="big-spacer-top"
+ >
+ <h2>
+ project_quality_profile.add_language.title
+ </h2>
+ <p
+ className="spacer-top big-spacer-bottom"
+ >
+ project_quality_profile.add_language.description
+ </p>
+ <Button
+ disabled={true}
+ onClick={[MockFunction]}
+ >
+ <PlusCircleIcon
+ className="little-spacer-right"
+ />
+ project_quality_profile.add_language.action
+ </Button>
+ </div>
+ </div>
+ </div>
+</div>
+`;
+
+exports[`should render correctly: open profile 1`] = `
+<div
+ className="page page-limited"
+ id="project-quality-profiles"
+>
+ <Suggestions
+ suggestions="project_quality_profiles"
+ />
+ <Helmet
+ defer={false}
+ encodeSpecialCharacters={true}
+ title="project_quality_profiles.page"
+ />
+ <A11ySkipTarget
+ anchor="profiles_main"
+ />
+ <header
+ className="page-header"
+ >
+ <div
+ className="page-title display-flex-center"
+ >
+ <h1>
+ project_quality_profiles.page
+
+ </h1>
+ <HelpTooltip
+ className="spacer-left"
+ overlay={
+ <div
+ className="big-padded-top big-padded-bottom"
+ >
+ quality_profiles.list.projects.help
+ </div>
+ }
+ />
+ </div>
+ </header>
+ <div
+ className="boxed-group"
+ >
+ <h2
+ className="boxed-group-header"
+ >
+ project_quality_profile.subtitle
+ </h2>
+ <div
+ className="boxed-group-inner"
+ >
+ <p
+ className="big-spacer-bottom"
+ >
+ project_quality_profiles.page.description
+ </p>
+ <table
+ className="data zebra"
+ >
+ <thead>
+ <tr>
+ <th>
+ language
+ </th>
+ <th
+ className="thin nowrap"
+ >
+ project_quality_profile.current
+ </th>
+ <th
+ className="thin nowrap text-right"
+ >
+ coding_rules.filters.activation.active_rules
+ </th>
+ <th
+ aria-label="actions"
+ />
+ </tr>
+ </thead>
+ <tbody>
+ <tr
+ key="css"
+ >
+ <td>
+ CSS
+ </td>
+ <td
+ className="thin nowrap"
+ >
+ <span
+ className="display-inline-flex-center"
+ >
+ <em>
+ project_quality_profile.instance_default
+ </em>
+ </span>
+ </td>
+ <td
+ className="nowrap text-right"
+ >
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/coding_rules",
+ "query": Object {
+ "qprofile": "bar",
+ },
+ }
+ }
+ >
+ 10
+ </Link>
+ </td>
+ <td
+ className="text-right"
+ >
+ <Button
+ onClick={[Function]}
+ >
+ <EditIcon
+ className="spacer-right"
+ />
+ project_quality_profile.change_profile
+ </Button>
+ </td>
+ </tr>
+ <tr
+ key="html"
+ >
+ <td>
+ HTML
+ </td>
+ <td
+ className="thin nowrap"
+ >
+ <span
+ className="display-inline-flex-center"
+ >
+ Baz
+ </span>
+ </td>
+ <td
+ className="nowrap text-right"
+ >
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/coding_rules",
+ "query": Object {
+ "qprofile": "baz",
+ },
+ }
+ }
+ >
+ 10
+ </Link>
+ </td>
+ <td
+ className="text-right"
+ >
+ <Button
+ onClick={[Function]}
+ >
+ <EditIcon
+ className="spacer-right"
+ />
+ project_quality_profile.change_profile
+ </Button>
+ </td>
+ </tr>
+ <tr
+ key="js"
+ >
+ <td>
+ JS
+ </td>
+ <td
+ className="thin nowrap"
+ >
+ <span
+ className="display-inline-flex-center"
+ >
+ <em>
+ project_quality_profile.instance_default
+ </em>
+ </span>
+ </td>
+ <td
+ className="nowrap text-right"
+ >
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/coding_rules",
+ "query": Object {
+ "qprofile": "foo",
+ },
+ }
+ }
+ >
+ 10
+ </Link>
+ </td>
+ <td
+ className="text-right"
+ >
+ <Button
+ onClick={[Function]}
+ >
+ <EditIcon
+ className="spacer-right"
+ />
+ project_quality_profile.change_profile
+ </Button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <div
+ className="big-spacer-top"
+ >
+ <h2>
+ project_quality_profile.add_language.title
+ </h2>
+ <p
+ className="spacer-top big-spacer-bottom"
+ >
+ project_quality_profile.add_language.description
+ </p>
+ <Button
+ disabled={false}
+ onClick={[MockFunction]}
+ >
+ <PlusCircleIcon
+ className="little-spacer-right"
+ />
+ project_quality_profile.add_language.action
+ </Button>
+ </div>
+ <SetQualityProfileModal
+ availableProfiles={
+ Array [
+ Object {
+ "activeDeprecatedRuleCount": 2,
+ "activeRuleCount": 10,
+ "childrenCount": 0,
+ "depth": 1,
+ "isBuiltIn": false,
+ "isDefault": false,
+ "isInherited": false,
+ "key": "foo",
+ "language": "js",
+ "languageName": "JavaScript",
+ "name": "name",
+ "organization": "foo",
+ "projectCount": 3,
+ },
+ ]
+ }
+ component={
+ Object {
+ "breadcrumbs": Array [],
+ "key": "my-project",
+ "name": "MyProject",
+ "organization": "foo",
+ "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 [],
+ }
+ }
+ currentProfile={
+ Object {
+ "activeDeprecatedRuleCount": 2,
+ "activeRuleCount": 10,
+ "childrenCount": 0,
+ "depth": 1,
+ "isBuiltIn": false,
+ "isDefault": false,
+ "isInherited": false,
+ "key": "foo",
+ "language": "js",
+ "languageName": "JavaScript",
+ "name": "name",
+ "organization": "foo",
+ "projectCount": 3,
+ }
+ }
+ onClose={[MockFunction]}
+ onSubmit={[MockFunction]}
+ usesDefault={true}
+ />
+ </div>
+ </div>
+</div>
+`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<div
- className="boxed-group boxed-group-inner"
->
- <table
- className="data zebra"
- >
- <thead>
- <tr>
- <th
- className="thin nowrap"
- >
- language
- </th>
- <th
- className="thin nowrap"
- >
- quality_profile
- </th>
- <th>
- Â
- </th>
- </tr>
- </thead>
- <tbody>
- <ProfileRow
- key="java"
- onChangeProfile={[MockFunction]}
- possibleProfiles={
- Array [
- Object {
- "activeDeprecatedRuleCount": 0,
- "activeRuleCount": 17,
- "key": "foo-java",
- "language": "java",
- "languageName": "java",
- "name": "foo-java",
- "organization": "org",
- },
- Object {
- "activeDeprecatedRuleCount": 0,
- "activeRuleCount": 17,
- "key": "bar-java",
- "language": "java",
- "languageName": "java",
- "name": "bar-java",
- "organization": "org",
- },
- Object {
- "activeDeprecatedRuleCount": 0,
- "activeRuleCount": 17,
- "key": "baz-java",
- "language": "java",
- "languageName": "java",
- "name": "baz-java",
- "organization": "org",
- },
- ]
- }
- profile={
- Object {
- "activeDeprecatedRuleCount": 0,
- "activeRuleCount": 17,
- "key": "foo-java",
- "language": "java",
- "languageName": "java",
- "name": "foo-java",
- "organization": "org",
- }
- }
- />
- <ProfileRow
- key="js"
- onChangeProfile={[MockFunction]}
- possibleProfiles={
- Array [
- Object {
- "activeDeprecatedRuleCount": 0,
- "activeRuleCount": 17,
- "key": "foo-js",
- "language": "js",
- "languageName": "js",
- "name": "foo-js",
- "organization": "org",
- },
- ]
- }
- profile={
- Object {
- "activeDeprecatedRuleCount": 0,
- "activeRuleCount": 17,
- "key": "foo-js",
- "language": "js",
- "languageName": "js",
- "name": "foo-js",
- "organization": "org",
- }
- }
- />
- </tbody>
- </table>
-</div>
-`;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { difference } from 'lodash';
+import * as React from 'react';
+import { connect } from 'react-redux';
+import { ButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons';
+import Select from 'sonar-ui-common/components/controls/Select';
+import SimpleModal from 'sonar-ui-common/components/controls/SimpleModal';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { Profile } from '../../../api/quality-profiles';
+import { Store } from '../../../store/rootReducer';
+
+export interface AddLanguageModalProps {
+ languages: T.Languages;
+ onClose: () => void;
+ onSubmit: (key: string) => Promise<void>;
+ profilesByLanguage: T.Dict<Profile[]>;
+ unavailableLanguages: string[];
+}
+
+export function AddLanguageModal(props: AddLanguageModalProps) {
+ const { languages, profilesByLanguage, unavailableLanguages } = props;
+
+ const [{ language, key }, setSelected] = React.useState<{ language?: string; key?: string }>({
+ language: undefined,
+ key: undefined
+ });
+
+ const header = translate('project_quality_profile.add_language_modal.title');
+
+ const languageOptions = difference(
+ Object.keys(profilesByLanguage),
+ unavailableLanguages
+ ).map(l => ({ value: l, label: languages[l].name }));
+
+ const profileOptions =
+ language !== undefined
+ ? profilesByLanguage[language].map(p => ({ value: p.key, label: p.name }))
+ : [];
+
+ return (
+ <SimpleModal
+ header={header}
+ onClose={props.onClose}
+ onSubmit={() => {
+ if (language && key) {
+ props.onSubmit(key);
+ }
+ }}>
+ {({ onCloseClick, onFormSubmit, submitting }) => (
+ <>
+ <div className="modal-head">
+ <h2>{header}</h2>
+ </div>
+
+ <form onSubmit={onFormSubmit}>
+ <div className="modal-body">
+ <div className="big-spacer-bottom">
+ <div className="little-spacer-bottom">
+ <label className="text-bold" htmlFor="language">
+ {translate('project_quality_profile.add_language_modal.choose_language')}
+ </label>
+ </div>
+ <Select
+ className="abs-width-300"
+ clearable={false}
+ disabled={submitting}
+ id="language"
+ onChange={({ value }: { value: string }) => setSelected({ language: value })}
+ options={languageOptions}
+ value={language}
+ />
+ </div>
+
+ <div className="big-spacer-bottom">
+ <div className="little-spacer-bottom">
+ <label className="text-bold" htmlFor="profiles">
+ {translate('project_quality_profile.add_language_modal.choose_profile')}
+ </label>
+ </div>
+ <Select
+ className="abs-width-300"
+ clearable={false}
+ disabled={submitting || !language}
+ id="profiles"
+ onChange={({ value }: { value: string }) => setSelected({ language, key: value })}
+ options={profileOptions}
+ value={key}
+ />
+ </div>
+ </div>
+
+ <div className="modal-foot">
+ {submitting && <i className="spinner spacer-right" />}
+ <SubmitButton disabled={submitting || !language || !key}>
+ {translate('save')}
+ </SubmitButton>
+ <ButtonLink disabled={submitting} onClick={onCloseClick}>
+ {translate('cancel')}
+ </ButtonLink>
+ </div>
+ </form>
+ </>
+ )}
+ </SimpleModal>
+ );
+}
+
+function mapStateToProps({ languages }: Store) {
+ return { languages };
+}
+
+export default connect(mapStateToProps)(AddLanguageModal);
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { ButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons';
+import Radio from 'sonar-ui-common/components/controls/Radio';
+import Select from 'sonar-ui-common/components/controls/Select';
+import SimpleModal from 'sonar-ui-common/components/controls/SimpleModal';
+import { Alert } from 'sonar-ui-common/components/ui/Alert';
+import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
+import { Profile } from '../../../api/quality-profiles';
+import BuiltInQualityProfileBadge from '../../quality-profiles/components/BuiltInQualityProfileBadge';
+import { USE_SYSTEM_DEFAULT } from '../constants';
+
+export interface SetQualityProfileModalProps {
+ availableProfiles: Profile[];
+ component: T.Component;
+ currentProfile: Profile;
+ onClose: () => void;
+ onSubmit: (newKey: string | undefined, oldKey: string) => Promise<void>;
+ usesDefault: boolean;
+}
+
+export default function SetQualityProfileModal(props: SetQualityProfileModalProps) {
+ const { availableProfiles, component, currentProfile, usesDefault } = props;
+ const [selected, setSelected] = React.useState(
+ usesDefault ? USE_SYSTEM_DEFAULT : currentProfile.key
+ );
+
+ const defaultProfile = availableProfiles.find(p => p.isDefault);
+
+ if (defaultProfile === undefined) {
+ // Cannot be undefined
+ return null;
+ }
+
+ const header = translateWithParameters(
+ 'project_quality_profile.change_lang_X_profile',
+ currentProfile.languageName
+ );
+ const profileOptions = availableProfiles.map(p => ({ value: p.key, label: p.name }));
+ const hasSelectedSysDefault = selected === USE_SYSTEM_DEFAULT;
+ const hasChanged = usesDefault ? !hasSelectedSysDefault : selected !== currentProfile.key;
+ const needsReanalysis = !component.qualityProfiles?.some(p =>
+ hasSelectedSysDefault ? p.key === defaultProfile.key : p.key === selected
+ );
+
+ return (
+ <SimpleModal
+ header={header}
+ onClose={props.onClose}
+ onSubmit={() =>
+ props.onSubmit(hasSelectedSysDefault ? undefined : selected, currentProfile.key)
+ }>
+ {({ onCloseClick, onFormSubmit, submitting }) => (
+ <>
+ <div className="modal-head">
+ <h2>{header}</h2>
+ </div>
+
+ <form onSubmit={onFormSubmit}>
+ <div className="modal-body">
+ <div className="big-spacer-bottom">
+ <Radio
+ className="display-flex-start"
+ checked={hasSelectedSysDefault}
+ disabled={submitting}
+ onCheck={() => setSelected(USE_SYSTEM_DEFAULT)}
+ value={USE_SYSTEM_DEFAULT}>
+ <div className="spacer-left">
+ <div className="little-spacer-bottom">
+ {translate('project_quality_profile.always_use_default')}
+ </div>
+ <div className="display-flex-center">
+ <span className="text-muted spacer-right">{translate('current_noun')}:</span>
+ {defaultProfile.name}
+ {defaultProfile.isBuiltIn && (
+ <BuiltInQualityProfileBadge className="spacer-left" />
+ )}
+ </div>
+ </div>
+ </Radio>
+ </div>
+
+ <div className="big-spacer-bottom">
+ <Radio
+ className="display-flex-start"
+ checked={!hasSelectedSysDefault}
+ disabled={submitting}
+ onCheck={() =>
+ setSelected(!hasSelectedSysDefault ? selected : currentProfile.key)
+ }
+ value={currentProfile.key}>
+ <div className="spacer-left">
+ <div className="little-spacer-bottom">
+ {translate('project_quality_profile.always_use_specific')}
+ </div>
+ <div className="display-flex-center">
+ <Select
+ className="abs-width-300"
+ clearable={false}
+ disabled={submitting || hasSelectedSysDefault}
+ onChange={({ value }: { value: string }) => setSelected(value)}
+ options={profileOptions}
+ optionRenderer={option => <span>{option.label}</span>}
+ value={!hasSelectedSysDefault ? selected : currentProfile.key}
+ />
+ </div>
+ </div>
+ </Radio>
+ </div>
+
+ {needsReanalysis && (
+ <Alert variant="warning">
+ {translate('project_quality_profile.requires_new_analysis')}
+ </Alert>
+ )}
+ </div>
+
+ <div className="modal-foot">
+ {submitting && <i className="spinner spacer-right" />}
+ <SubmitButton disabled={submitting || !hasChanged}>{translate('save')}</SubmitButton>
+ <ButtonLink disabled={submitting} onClick={onCloseClick}>
+ {translate('cancel')}
+ </ButtonLink>
+ </div>
+ </form>
+ </>
+ )}
+ </SimpleModal>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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, ShallowWrapper } from 'enzyme';
+import * as React from 'react';
+import Select from 'sonar-ui-common/components/controls/Select';
+import SimpleModal from 'sonar-ui-common/components/controls/SimpleModal';
+import { mockQualityProfile } from '../../../../helpers/testMocks';
+import { AddLanguageModal, AddLanguageModalProps } from '../AddLanguageModal';
+
+it('should render correctly', () => {
+ expect(diveIntoSimpleModal(shallowRender())).toMatchSnapshot('default');
+});
+
+it('should correctly handle changes', () => {
+ return new Promise(resolve => {
+ const onSubmit = jest.fn();
+ const wrapper = shallowRender({ onSubmit });
+
+ const langSelect = getLanguageSelect(wrapper);
+ let profileSelect = getProfileSelect(wrapper);
+
+ // Language select should only have 2; JS is not available. Profile Select
+ // should have none, as no language is selected yet.
+ expect(langSelect.props().options).toHaveLength(2);
+ expect(profileSelect.props().options).toHaveLength(0);
+
+ // Choose CSS.
+ const langChange = langSelect.props().onChange;
+ if (langChange) {
+ langChange({ value: 'css' });
+
+ // Should now show 2 available profiles.
+ profileSelect = getProfileSelect(wrapper);
+ expect(profileSelect.props().options).toHaveLength(2);
+
+ // Choose 1 profile.
+ const profileChange = profileSelect.props().onChange;
+ if (profileChange) {
+ profileChange({ value: 'css2' });
+
+ submitSimpleModal(wrapper);
+ expect(onSubmit).toHaveBeenLastCalledWith('css2');
+
+ resolve();
+ }
+ }
+ });
+});
+
+function diveIntoSimpleModal(wrapper: ShallowWrapper) {
+ return wrapper
+ .find(SimpleModal)
+ .dive()
+ .children();
+}
+
+function getLanguageSelect(wrapper: ShallowWrapper) {
+ return diveIntoSimpleModal(wrapper)
+ .find(Select)
+ .at(0);
+}
+
+function getProfileSelect(wrapper: ShallowWrapper) {
+ return diveIntoSimpleModal(wrapper)
+ .find(Select)
+ .at(1);
+}
+
+function submitSimpleModal(wrapper: ShallowWrapper) {
+ wrapper
+ .find(SimpleModal)
+ .props()
+ .onSubmit();
+}
+
+function shallowRender(props: Partial<AddLanguageModalProps> = {}) {
+ return shallow<AddLanguageModalProps>(
+ <AddLanguageModal
+ languages={{
+ css: { key: 'css', name: 'CSS' },
+ ts: { key: 'ts', name: 'TS' },
+ js: { key: 'js', name: 'JS' }
+ }}
+ onClose={jest.fn()}
+ onSubmit={jest.fn()}
+ profilesByLanguage={{
+ css: [
+ mockQualityProfile({ key: 'css', name: 'CSS' }),
+ mockQualityProfile({ key: 'css2', name: 'CSS 2' })
+ ],
+ ts: [mockQualityProfile({ key: 'ts', name: 'TS' })],
+ js: [mockQualityProfile({ key: 'js', name: 'JS' })]
+ }}
+ unavailableLanguages={['js']}
+ {...props}
+ />
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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, ShallowWrapper } from 'enzyme';
+import * as React from 'react';
+import Radio from 'sonar-ui-common/components/controls/Radio';
+import Select from 'sonar-ui-common/components/controls/Select';
+import SimpleModal from 'sonar-ui-common/components/controls/SimpleModal';
+import { mockComponent, mockQualityProfile } from '../../../../helpers/testMocks';
+import SetQualityProfileModal, { SetQualityProfileModalProps } from '../SetQualityProfileModal';
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot('default');
+ expect(shallowRender({ usesDefault: true })).toMatchSnapshot('inherits system default');
+ expect(shallowRender({ component: mockComponent() })).toMatchSnapshot('needs reanalysis');
+});
+
+it('should render select options correctly', () => {
+ return new Promise(resolve => {
+ const wrapper = shallowRender();
+ const render = wrapper.find(Select).props().optionRenderer;
+ if (render) {
+ expect(render({ value: 'bar', label: 'Profile 1' })).toMatchSnapshot('default');
+ resolve();
+ }
+ });
+});
+
+it('should correctly handle changes', () => {
+ return new Promise(resolve => {
+ const onSubmit = jest.fn();
+ const wrapper = shallowRender({ onSubmit }, false);
+
+ diveIntoSimpleModal(wrapper)
+ .find(Radio)
+ .at(0)
+ .props()
+ .onCheck('');
+ submitSimpleModal(wrapper);
+ expect(onSubmit).toHaveBeenLastCalledWith(undefined, 'foo');
+
+ diveIntoSimpleModal(wrapper)
+ .find(Radio)
+ .at(1)
+ .props()
+ .onCheck('');
+ submitSimpleModal(wrapper);
+ expect(onSubmit).toHaveBeenLastCalledWith('foo', 'foo');
+
+ const change = diveIntoSimpleModal(wrapper)
+ .find(Select)
+ .props().onChange;
+ if (change) {
+ change({ value: 'bar' });
+ submitSimpleModal(wrapper);
+ expect(onSubmit).toHaveBeenLastCalledWith('bar', 'foo');
+
+ resolve();
+ }
+ });
+});
+
+function diveIntoSimpleModal(wrapper: ShallowWrapper) {
+ return wrapper
+ .find(SimpleModal)
+ .dive()
+ .children();
+}
+
+function submitSimpleModal(wrapper: ShallowWrapper) {
+ wrapper
+ .find(SimpleModal)
+ .props()
+ .onSubmit();
+}
+
+function shallowRender(props: Partial<SetQualityProfileModalProps> = {}, dive = true) {
+ const wrapper = shallow<SetQualityProfileModalProps>(
+ <SetQualityProfileModal
+ availableProfiles={[
+ mockQualityProfile({ key: 'foo', isDefault: true, language: 'js' }),
+ mockQualityProfile({ key: 'bar', language: 'js' })
+ ]}
+ component={mockComponent({ qualityProfiles: [{ key: 'foo', name: 'Foo', language: 'js' }] })}
+ currentProfile={mockQualityProfile({ key: 'foo', language: 'js' })}
+ onClose={jest.fn()}
+ onSubmit={jest.fn()}
+ usesDefault={false}
+ {...props}
+ />
+ );
+
+ return dive ? diveIntoSimpleModal(wrapper) : wrapper;
+}
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: default 1`] = `
+Array [
+ <div
+ className="modal-head"
+ >
+ <h2>
+ project_quality_profile.add_language_modal.title
+ </h2>
+ </div>,
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="modal-body"
+ >
+ <div
+ className="big-spacer-bottom"
+ >
+ <div
+ className="little-spacer-bottom"
+ >
+ <label
+ className="text-bold"
+ htmlFor="language"
+ >
+ project_quality_profile.add_language_modal.choose_language
+ </label>
+ </div>
+ <Select
+ className="abs-width-300"
+ clearable={false}
+ disabled={false}
+ id="language"
+ onChange={[Function]}
+ options={
+ Array [
+ Object {
+ "label": "CSS",
+ "value": "css",
+ },
+ Object {
+ "label": "TS",
+ "value": "ts",
+ },
+ ]
+ }
+ />
+ </div>
+ <div
+ className="big-spacer-bottom"
+ >
+ <div
+ className="little-spacer-bottom"
+ >
+ <label
+ className="text-bold"
+ htmlFor="profiles"
+ >
+ project_quality_profile.add_language_modal.choose_profile
+ </label>
+ </div>
+ <Select
+ className="abs-width-300"
+ clearable={false}
+ disabled={true}
+ id="profiles"
+ onChange={[Function]}
+ options={Array []}
+ />
+ </div>
+ </div>
+ <div
+ className="modal-foot"
+ >
+ <SubmitButton
+ disabled={true}
+ >
+ save
+ </SubmitButton>
+ <ButtonLink
+ disabled={false}
+ onClick={[Function]}
+ >
+ cancel
+ </ButtonLink>
+ </div>
+ </form>,
+]
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: default 1`] = `
+Array [
+ <div
+ className="modal-head"
+ >
+ <h2>
+ project_quality_profile.change_lang_X_profile.JavaScript
+ </h2>
+ </div>,
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="modal-body"
+ >
+ <div
+ className="big-spacer-bottom"
+ >
+ <Radio
+ checked={false}
+ className="display-flex-start"
+ disabled={false}
+ onCheck={[Function]}
+ value="-1"
+ >
+ <div
+ className="spacer-left"
+ >
+ <div
+ className="little-spacer-bottom"
+ >
+ project_quality_profile.always_use_default
+ </div>
+ <div
+ className="display-flex-center"
+ >
+ <span
+ className="text-muted spacer-right"
+ >
+ current_noun
+ :
+ </span>
+ name
+ </div>
+ </div>
+ </Radio>
+ </div>
+ <div
+ className="big-spacer-bottom"
+ >
+ <Radio
+ checked={true}
+ className="display-flex-start"
+ disabled={false}
+ onCheck={[Function]}
+ value="foo"
+ >
+ <div
+ className="spacer-left"
+ >
+ <div
+ className="little-spacer-bottom"
+ >
+ project_quality_profile.always_use_specific
+ </div>
+ <div
+ className="display-flex-center"
+ >
+ <Select
+ className="abs-width-300"
+ clearable={false}
+ disabled={false}
+ onChange={[Function]}
+ optionRenderer={[Function]}
+ options={
+ Array [
+ Object {
+ "label": "name",
+ "value": "foo",
+ },
+ Object {
+ "label": "name",
+ "value": "bar",
+ },
+ ]
+ }
+ value="foo"
+ />
+ </div>
+ </div>
+ </Radio>
+ </div>
+ </div>
+ <div
+ className="modal-foot"
+ >
+ <SubmitButton
+ disabled={true}
+ >
+ save
+ </SubmitButton>
+ <ButtonLink
+ disabled={false}
+ onClick={[Function]}
+ >
+ cancel
+ </ButtonLink>
+ </div>
+ </form>,
+]
+`;
+
+exports[`should render correctly: inherits system default 1`] = `
+Array [
+ <div
+ className="modal-head"
+ >
+ <h2>
+ project_quality_profile.change_lang_X_profile.JavaScript
+ </h2>
+ </div>,
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="modal-body"
+ >
+ <div
+ className="big-spacer-bottom"
+ >
+ <Radio
+ checked={true}
+ className="display-flex-start"
+ disabled={false}
+ onCheck={[Function]}
+ value="-1"
+ >
+ <div
+ className="spacer-left"
+ >
+ <div
+ className="little-spacer-bottom"
+ >
+ project_quality_profile.always_use_default
+ </div>
+ <div
+ className="display-flex-center"
+ >
+ <span
+ className="text-muted spacer-right"
+ >
+ current_noun
+ :
+ </span>
+ name
+ </div>
+ </div>
+ </Radio>
+ </div>
+ <div
+ className="big-spacer-bottom"
+ >
+ <Radio
+ checked={false}
+ className="display-flex-start"
+ disabled={false}
+ onCheck={[Function]}
+ value="foo"
+ >
+ <div
+ className="spacer-left"
+ >
+ <div
+ className="little-spacer-bottom"
+ >
+ project_quality_profile.always_use_specific
+ </div>
+ <div
+ className="display-flex-center"
+ >
+ <Select
+ className="abs-width-300"
+ clearable={false}
+ disabled={true}
+ onChange={[Function]}
+ optionRenderer={[Function]}
+ options={
+ Array [
+ Object {
+ "label": "name",
+ "value": "foo",
+ },
+ Object {
+ "label": "name",
+ "value": "bar",
+ },
+ ]
+ }
+ value="foo"
+ />
+ </div>
+ </div>
+ </Radio>
+ </div>
+ </div>
+ <div
+ className="modal-foot"
+ >
+ <SubmitButton
+ disabled={true}
+ >
+ save
+ </SubmitButton>
+ <ButtonLink
+ disabled={false}
+ onClick={[Function]}
+ >
+ cancel
+ </ButtonLink>
+ </div>
+ </form>,
+]
+`;
+
+exports[`should render correctly: needs reanalysis 1`] = `
+Array [
+ <div
+ className="modal-head"
+ >
+ <h2>
+ project_quality_profile.change_lang_X_profile.JavaScript
+ </h2>
+ </div>,
+ <form
+ onSubmit={[Function]}
+ >
+ <div
+ className="modal-body"
+ >
+ <div
+ className="big-spacer-bottom"
+ >
+ <Radio
+ checked={false}
+ className="display-flex-start"
+ disabled={false}
+ onCheck={[Function]}
+ value="-1"
+ >
+ <div
+ className="spacer-left"
+ >
+ <div
+ className="little-spacer-bottom"
+ >
+ project_quality_profile.always_use_default
+ </div>
+ <div
+ className="display-flex-center"
+ >
+ <span
+ className="text-muted spacer-right"
+ >
+ current_noun
+ :
+ </span>
+ name
+ </div>
+ </div>
+ </Radio>
+ </div>
+ <div
+ className="big-spacer-bottom"
+ >
+ <Radio
+ checked={true}
+ className="display-flex-start"
+ disabled={false}
+ onCheck={[Function]}
+ value="foo"
+ >
+ <div
+ className="spacer-left"
+ >
+ <div
+ className="little-spacer-bottom"
+ >
+ project_quality_profile.always_use_specific
+ </div>
+ <div
+ className="display-flex-center"
+ >
+ <Select
+ className="abs-width-300"
+ clearable={false}
+ disabled={false}
+ onChange={[Function]}
+ optionRenderer={[Function]}
+ options={
+ Array [
+ Object {
+ "label": "name",
+ "value": "foo",
+ },
+ Object {
+ "label": "name",
+ "value": "bar",
+ },
+ ]
+ }
+ value="foo"
+ />
+ </div>
+ </div>
+ </Radio>
+ </div>
+ <Alert
+ variant="warning"
+ >
+ project_quality_profile.requires_new_analysis
+ </Alert>
+ </div>
+ <div
+ className="modal-foot"
+ >
+ <SubmitButton
+ disabled={true}
+ >
+ save
+ </SubmitButton>
+ <ButtonLink
+ disabled={false}
+ onClick={[Function]}
+ >
+ cancel
+ </ButtonLink>
+ </div>
+ </form>,
+]
+`;
+
+exports[`should render select options correctly: default 1`] = `
+<span>
+ Profile 1
+</span>
+`;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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.
+ */
+export const USE_SYSTEM_DEFAULT = '-1';
const routes = [
{
- indexRoute: { component: lazyLoadComponent(() => import('./App')) }
+ indexRoute: {
+ component: lazyLoadComponent(() => import('./ProjectQualityProfilesApp'))
+ }
}
];
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { Profile } from '../../api/quality-profiles';
+
+export interface ProjectProfile {
+ profile: Profile;
+ selected: boolean;
+}
settings.page.description=Edit global settings for this {instance} instance.
system_info.page=System Info
project_quality_profiles.page=Quality Profiles
-project_quality_profiles.page.description=Choose which profile is associated with this project on a language-by-language basis. (Note that you will only need to select profiles for multiple languages for multi-language projects.)
+project_quality_profiles.page.description=Choose which profile is associated with this project on a language-by-language basis.
project_quality_gate.page=Quality Gate
project_quality_gate.page.description=Choose which quality gate is associated with this project.
update_key.page=Update Key
# PROJECT QUALITY PROFILE PAGE
#
#------------------------------------------------------------------------------
-project_quality_profile.default_profile=Default
+project_quality_profile.instance_default=Instance default
project_quality_profile.successfully_updated={0} Quality Profile has been successfully updated.
+project_quality_profile.subtitle=Manage project Quality Profiles
+project_quality_profile.always_use_default=Always use the instance default Quality Profile
+project_quality_profile.current=Current Quality Profile
+project_quality_profile.always_use_specific=Always use a specific Quality Profile
+project_quality_profile.change_lang_X_profile=Change {0} Quality Profile
+project_quality_profile.requires_new_analysis=Changes will be applied after the next analysis.
+project_quality_profile.add_language.title=Add a new language
+project_quality_profile.add_language.description=Manually configure a specific profile for a new language before the next analysis.
+project_quality_profile.add_language.action=Add language
+project_quality_profile.add_language_modal.title=Add a language
+project_quality_profile.add_language_modal.choose_language=Choose a language
+project_quality_profile.add_language_modal.choose_profile=Choose a profile
+project_quality_profile.change_profile=Change profile
#------------------------------------------------------------------------------
#
quality_profiles.intro1=Quality Profiles are collections of rules to apply during an analysis.
quality_profiles.intro2=For each language there is a default profile. All projects not explicitly assigned to some other profile will be analyzed with the default. Ideally, all projects will use the same profile for a language.
quality_profiles.list.projects=Projects
-quality_profiles.list.projects.help=Projects assigned to a profile will always be analyzed with it for that language, regardless of which profile is the default. Quality Profile administrators may assign projects to a profile. Project administrators may also choose a non-default profile for each language.
+quality_profiles.list.projects.help=Projects assigned to a profile will always be analyzed with it for that language, regardless of which profile is the default. Quality Profile administrators may assign projects to a non-default profile, or always make it follow the system default. Project administrators may choose any profile for each language.
quality_profiles.list.rules=Rules
quality_profiles.list.updated=Updated
quality_profiles.list.used=Used
coding_rules.filters.activation=Activation
coding_rules.filters.activation.active=Active
+coding_rules.filters.activation.active_rules=Active Rules
coding_rules.filters.activation.inactive=Inactive
coding_rules.filters.activation.help=Activation criterion is available when a Quality Profile is selected
coding_rules.filters.active_severity=Active Severity