ariaLabel?: string;
buttonSize?: 'small' | 'medium';
children: React.ReactNode;
+ toggleClassName?: string;
}
export function ActionsDropdown(props: ActionsDropdownProps) {
- const { children, buttonSize, ariaLabel, ...dropdownProps } = props;
+ const { children, buttonSize, ariaLabel, toggleClassName, ...dropdownProps } = props;
const intl = useIntl();
<InteractiveIcon
Icon={MenuIcon}
aria-label={ariaLabel ?? intl.formatMessage({ id: 'menu' })}
+ className={toggleClassName}
size={buttonSize}
stopPropagation={false}
/>
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { PeopleIcon } from '@primer/octicons-react';
+import { OcticonHoc } from './Icon';
+
+export const UserGroupIcon = OcticonHoc(PeopleIcon, 'UserGroupIcon');
export { UnfoldDownIcon } from './UnfoldDownIcon';
export { UnfoldIcon } from './UnfoldIcon';
export { UnfoldUpIcon } from './UnfoldUpIcon';
+export { UserGroupIcon } from './UserGroupIcon';
export { VulnerabilityIcon } from './VulnerabilityIcon';
getProfileInheritance,
getProfileProjects,
getQualityProfile,
+ getQualityProfileExporterUrl,
removeGroup,
removeUser,
renameProfile,
jest.mocked(deleteProfile).mockImplementation(this.handleDeleteProfile);
jest.mocked(renameProfile).mockImplementation(this.handleRenameProfile);
jest.mocked(setDefaultProfile).mockImplementation(this.handleSetDefaultProfile);
+ jest
+ .mocked(getQualityProfileExporterUrl)
+ .mockImplementation(() => '/api/qualityprofiles/export');
}
resetQualityProfile() {
'/project/issues',
'/project/activity',
'/code',
+ '/profiles/show',
'/project/extension/securityreport/securityreport',
'/projects',
'/project/information',
jest.mock('../../../api/quality-profiles');
jest.mock('../../../api/rules');
+beforeEach(() => {
+ serviceMock.reset();
+});
+
const serviceMock = new QualityProfilesServiceMock();
const ui = {
permissionSection: byRole('region', { name: 'permissions.page' }),
name: /quality_profiles.actions/,
}),
qualityProfilesHeader: byRole('heading', { name: 'quality_profiles.page' }),
- deleteQualityProfileButton: byRole('button', { name: 'delete' }),
+ deleteQualityProfileButton: byRole('menuitem', { name: 'delete' }),
activateMoreRulesButton: byRole('button', { name: 'quality_profiles.activate_more' }),
activateMoreLink: byRole('link', { name: 'quality_profiles.activate_more' }),
- activateMoreRulesLink: byRole('link', { name: 'quality_profiles.activate_more_rules' }),
- backUpLink: byRole('link', { name: 'backup_verb' }),
- compareLink: byRole('link', { name: 'compare' }),
- extendButton: byRole('button', { name: 'extend' }),
- copyButton: byRole('button', { name: 'copy' }),
- renameButton: byRole('button', { name: 'rename' }),
- setAsDefaultButton: byRole('button', { name: 'set_as_default' }),
+ activateMoreRulesLink: byRole('menuitem', { name: 'quality_profiles.activate_more_rules' }),
+ backUpLink: byRole('menuitem', { name: 'backup_verb' }),
+ compareLink: byRole('menuitem', { name: 'compare' }),
+ extendButton: byRole('menuitem', { name: 'extend' }),
+ copyButton: byRole('menuitem', { name: 'copy' }),
+ renameButton: byRole('menuitem', { name: 'rename' }),
+ setAsDefaultButton: byRole('menuitem', { name: 'set_as_default' }),
newNameInput: byRole('textbox', { name: /quality_profiles.new_name/ }),
qualityProfilePageLink: byRole('link', { name: 'quality_profiles.page' }),
rulesTotalRow: byRole('row', { name: /total/ }),
rulesDeprecatedLink: byRole('link', { name: '8' }),
};
-beforeEach(() => {
- serviceMock.reset();
-});
-
describe('Admin or user with permission', () => {
beforeEach(() => {
serviceMock.setAdmin();
renderQualityProfile('sonar');
expect(await ui.rulesSection.find()).toBeInTheDocument();
expect(ui.activateMoreRulesButton.get()).toBeInTheDocument();
- expect(ui.activateMoreRulesButton.get()).toHaveClass('disabled');
+ expect(ui.activateMoreRulesButton.get()).toBeDisabled();
});
});
expect(ui.dialog.query()).not.toBeInTheDocument();
expect(screen.getAllByText('Bad new PHP quality profile')).toHaveLength(2);
- expect(screen.getAllByText('Good old PHP quality profile')).toHaveLength(2);
+ expect(screen.getByText('Good old PHP quality profile')).toBeInTheDocument();
});
it('should be able to copy a quality profile', async () => {
it('should be able to see a warning when some rules are deprecated', async () => {
renderQualityProfile();
- expect(await ui.rulesDeprecatedWarning.findAll()).toHaveLength(2);
+ expect(await ui.rulesDeprecatedWarning.findAll()).toHaveLength(1);
expect(ui.rulesDeprecatedLink.get()).toBeInTheDocument();
expect(ui.rulesDeprecatedLink.get()).toHaveAttribute(
'href',
cQualityProfileName: 'c quality profile',
newCQualityProfileName: 'New c quality profile',
newCQualityProfileNameFromCreateButton: 'New c quality profile from create',
- profileActions: (name: string, language: string) =>
+ listProfileActions: (name: string, language: string) =>
byRole('button', {
name: `quality_profiles.actions.${name}.${language}`,
}),
- extendButton: byRole('button', {
+ profileActions: (name: string, language: string) =>
+ byRole('menuitem', {
+ name: `quality_profiles.actions.${name}.${language}`,
+ }),
+ modalExtendButton: byRole('button', {
name: 'extend',
}),
- copyButton: byRole('button', {
+ qualityProfileActions: byRole('button', {
+ name: /quality_profiles.actions/,
+ }),
+ extendButton: byRole('menuitem', {
+ name: 'extend',
+ }),
+ modalCopyButton: byRole('button', {
+ name: 'copy',
+ }),
+ copyButton: byRole('menuitem', {
name: 'copy',
}),
createButton: byRole('button', { name: 'create' }),
restoreButton: byRole('button', { name: 'restore' }),
- compareButton: byRole('link', { name: 'compare' }),
+ compareButton: byRole('menuitem', { name: 'compare' }),
cancelButton: byRole('button', { name: 'cancel' }),
compareDropdown: byRole('combobox', { name: 'quality_profiles.compare_with' }),
changelogLink: byRole('link', { name: 'changelog' }),
namePropupInput: byRole('textbox', { name: 'quality_profiles.new_name required' }),
filterByLang: byRole('combobox', { name: 'quality_profiles.filter_by:' }),
listLinkCQualityProfile: byRole('link', { name: 'c quality profile' }),
- listLinkNewCQualityProfile: byRole('link', { name: 'New c quality profile' }),
- listLinkNewCQualityProfileFromCreateButton: byRole('link', {
+ headingNewCQualityProfile: byRole('heading', { name: 'New c quality profile' }),
+ headingNewCQualityProfileFromCreateButton: byRole('heading', {
name: 'New c quality profile from create',
}),
listLinkJavaQualityProfile: byRole('link', { name: 'java quality profile' }),
serviceMock.setAdmin();
renderQualityProfiles();
- await user.click(await ui.profileActions('c quality profile', 'C').find());
+ await user.click(await ui.listProfileActions('c quality profile', 'C').find());
await user.click(ui.extendButton.get());
await user.clear(ui.namePropupInput.get());
await user.type(ui.namePropupInput.get(), ui.newCQualityProfileName);
await act(async () => {
- await user.click(ui.extendButton.get());
+ await user.click(ui.modalExtendButton.get());
});
- expect(await ui.listLinkNewCQualityProfile.find()).toBeInTheDocument();
+ expect(await ui.headingNewCQualityProfile.find()).toBeInTheDocument();
await user.click(ui.returnToList.get());
await user.click(ui.createButton.get());
await user.click(ui.createButton.get(ui.popup.get()));
});
- expect(await ui.listLinkNewCQualityProfileFromCreateButton.find()).toBeInTheDocument();
+ expect(await ui.headingNewCQualityProfileFromCreateButton.find()).toBeInTheDocument();
});
it('should be able to copy an existing Quality Profile', async () => {
serviceMock.setAdmin();
renderQualityProfiles();
- await user.click(await ui.profileActions('c quality profile', 'C').find());
+ await user.click(await ui.listProfileActions('c quality profile', 'C').find());
await user.click(ui.copyButton.get());
await user.clear(ui.namePropupInput.get());
await user.type(ui.namePropupInput.get(), ui.newCQualityProfileName);
await act(async () => {
- await user.click(ui.copyButton.get(ui.popup.get()));
+ await user.click(ui.modalCopyButton.get(ui.popup.get()));
});
- expect(await ui.listLinkNewCQualityProfile.find()).toBeInTheDocument();
+ expect(await ui.headingNewCQualityProfile.find()).toBeInTheDocument();
await user.click(ui.returnToList.get());
await user.click(ui.createButton.get());
await user.click(ui.createButton.get(ui.popup.get()));
});
- expect(await ui.listLinkNewCQualityProfileFromCreateButton.find()).toBeInTheDocument();
+ expect(await ui.headingNewCQualityProfileFromCreateButton.find()).toBeInTheDocument();
});
it('should be able to create blank Quality Profile', async () => {
await user.click(ui.createButton.get(ui.popup.get()));
});
- expect(await ui.listLinkNewCQualityProfile.find()).toBeInTheDocument();
+ expect(await ui.headingNewCQualityProfile.find()).toBeInTheDocument();
});
});
renderQualityProfiles();
// For language with 1 profle we should not see compare action
- await user.click(await ui.profileActions('c quality profile', 'C').find());
+ await user.click(await ui.listProfileActions('c quality profile', 'C').find());
expect(ui.compareButton.query()).not.toBeInTheDocument();
- await user.click(ui.profileActions('java quality profile', 'Java').get());
+ await user.click(ui.listProfileActions('java quality profile', 'Java').get());
expect(ui.compareButton.get()).toBeInTheDocument();
await user.click(ui.compareButton.get());
expect(ui.compareDropdown.get()).toBeInTheDocument();
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import classNames from 'classnames';
+import { Badge } from 'design-system';
import * as React from 'react';
import Tooltip from '../../../components/controls/Tooltip';
import { translate } from '../../../helpers/l10n';
export default function BuiltInQualityProfileBadge({ className, tooltip = true }: Props) {
const badge = (
- <div className={classNames('badge badge-info', className)}>
+ <Badge variant="default" className={className}>
{translate('quality_profiles.built_in')}
- </div>
+ </Badge>
);
if (tooltip) {
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import classNames from 'classnames';
+import {
+ ActionsDropdown,
+ ItemButton,
+ ItemDangerButton,
+ ItemDivider,
+ ItemDownload,
+ ItemLink,
+ PopupPlacement,
+ Tooltip,
+} from 'design-system';
import { some } from 'lodash';
import * as React from 'react';
import {
renameProfile,
setDefaultProfile,
} from '../../../api/quality-profiles';
-import ActionsDropdown, {
- ActionsDropdownDivider,
- ActionsDropdownItem,
-} from '../../../components/controls/ActionsDropdown';
-import Tooltip from '../../../components/controls/Tooltip';
import { Router, withRouter } from '../../../components/hoc/withRouter';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { getBaseUrl } from '../../../helpers/system';
import ProfileModalForm from './ProfileModalForm';
interface Props {
- className?: string;
profile: Profile;
router: Router;
isComparable: boolean;
const hasNoActiveRules = profile.activeRuleCount === 0;
const hasAnyAction = some([...Object.values(actions), !profile.isBuiltIn, isComparable]);
+ if (!hasAnyAction) {
+ return null;
+ }
+
return (
<>
<ActionsDropdown
- className={classNames(this.props.className, { invisible: !hasAnyAction })}
- label={translateWithParameters(
+ allowResizing
+ id={`quality-profile-actions-${profile.key}`}
+ className="it__quality-profiles__actions-dropdown"
+ toggleClassName="it__quality-profiles__actions-dropdown-toggle"
+ ariaLabel={translateWithParameters(
'quality_profiles.actions',
profile.name,
profile.languageName,
)}
- disabled={!hasAnyAction}
+ isPortal
>
{actions.edit && (
- <ActionsDropdownItem
- className="it__quality-profiles__activate-more-rules"
- to={activateMoreUrl}
- >
+ <ItemLink className="it__quality-profiles__activate-more-rules" to={activateMoreUrl}>
{translate('quality_profiles.activate_more_rules')}
- </ActionsDropdownItem>
+ </ItemLink>
)}
{!profile.isBuiltIn && (
- <ActionsDropdownItem
- className="it__quality-profiles__backup"
+ <ItemDownload
download={`${profile.key}.xml`}
- to={backupUrl}
+ href={backupUrl}
+ className="it__quality-profiles__backup"
>
{translate('backup_verb')}
- </ActionsDropdownItem>
+ </ItemDownload>
)}
{isComparable && (
- <ActionsDropdownItem
+ <ItemLink
className="it__quality-profiles__compare"
to={getProfileComparePath(profile.name, profile.language)}
>
{translate('compare')}
- </ActionsDropdownItem>
+ </ItemLink>
)}
{actions.copy && (
<>
- <ActionsDropdownItem
- tooltipPlacement="left"
- tooltipOverlay={translateWithParameters(
- 'quality_profiles.extend_help',
- profile.name,
- )}
- className="it__quality-profiles__extend"
- onClick={this.handleExtendClick}
+ <Tooltip
+ overlay={translateWithParameters('quality_profiles.extend_help', profile.name)}
+ placement={PopupPlacement.Left}
>
- {translate('extend')}
- </ActionsDropdownItem>
-
- <ActionsDropdownItem
- tooltipPlacement="left"
- tooltipOverlay={translateWithParameters('quality_profiles.copy_help', profile.name)}
- className="it__quality-profiles__copy"
- onClick={this.handleCopyClick}
+ <ItemButton
+ className="it__quality-profiles__extend"
+ onClick={this.handleExtendClick}
+ >
+ {translate('extend')}
+ </ItemButton>
+ </Tooltip>
+
+ <Tooltip
+ overlay={translateWithParameters('quality_profiles.copy_help', profile.name)}
+ placement={PopupPlacement.Left}
>
- {translate('copy')}
- </ActionsDropdownItem>
+ <ItemButton className="it__quality-profiles__copy" onClick={this.handleCopyClick}>
+ {translate('copy')}
+ </ItemButton>
+ </Tooltip>
</>
)}
{actions.edit && (
- <ActionsDropdownItem
- className="it__quality-profiles__rename"
- onClick={this.handleRenameClick}
- >
+ <ItemButton className="it__quality-profiles__rename" onClick={this.handleRenameClick}>
{translate('rename')}
- </ActionsDropdownItem>
+ </ItemButton>
)}
{actions.setAsDefault &&
(hasNoActiveRules ? (
<li>
<Tooltip
- placement="left"
+ placement={PopupPlacement.Left}
overlay={translate('quality_profiles.cannot_set_default_no_rules')}
>
- <span className="it__quality-profiles__set-as-default text-muted-2">
+ <span className="it__quality-profiles__set-as-default">
{translate('set_as_default')}
</span>
</Tooltip>
</li>
) : (
- <ActionsDropdownItem
+ <ItemButton
className="it__quality-profiles__set-as-default"
onClick={this.handleSetDefaultClick}
>
{translate('set_as_default')}
- </ActionsDropdownItem>
+ </ItemButton>
))}
- {actions.delete && <ActionsDropdownDivider />}
-
{actions.delete && (
- <ActionsDropdownItem
- className="it__quality-profiles__delete"
- destructive
- onClick={this.handleDeleteClick}
- >
- {translate('delete')}
- </ActionsDropdownItem>
+ <>
+ <ItemDivider />
+ <ItemDangerButton
+ className="it__quality-profiles__delete"
+ onClick={this.handleDeleteClick}
+ >
+ {translate('delete')}
+ </ItemDangerButton>
+ </>
)}
</ActionsDropdown>
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { StandoutLink } from 'design-system';
import * as React from 'react';
-import { NavLink } from 'react-router-dom';
import { getProfilePath } from '../utils';
interface Props {
export default function ProfileLink({ name, language, children, ...other }: Props) {
return (
- <NavLink
- className={({ isActive }) => (isActive ? 'link-no-underline' : '')}
- to={getProfilePath(name, language)}
- {...other}
- >
+ <StandoutLink to={getProfilePath(name, language)} {...other}>
{children}
- </NavLink>
+ </StandoutLink>
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { LargeCenteredLayout, Spinner } from 'design-system';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
import { Outlet } from 'react-router-dom';
const { actions, loading, profiles, exporters } = this.state;
if (loading) {
- return <i className="spinner" />;
+ return <Spinner />;
}
const finalLanguages = Object.values(this.props.languages);
const context: QualityProfilesContextProps = {
- actions: actions || {},
- profiles: profiles || [],
+ actions: actions ?? {},
+ profiles: profiles ?? [],
languages: finalLanguages,
- exporters: exporters || [],
+ exporters: exporters ?? [],
updateProfiles: this.updateProfiles,
};
render() {
return (
- <div className="page page-limited">
+ <LargeCenteredLayout className="sw-my-8">
<Suggestions suggestions="quality_profiles" />
<Helmet defer={false} title={translate('quality_profiles.page')} />
{this.renderChild()}
- </div>
+ </LargeCenteredLayout>
);
}
}
import { HelmetProvider } from 'react-helmet-async';
import { MemoryRouter, Outlet, Route, Routes } from 'react-router-dom';
import { mockQualityProfile } from '../../../../helpers/testMocks';
+import { IntlWrapper } from '../../../../helpers/testReactTestingUtils';
import {
QualityProfilesContextProps,
withQualityProfilesContext,
return render(
<HelmetProvider context={{}}>
<MemoryRouter initialEntries={[path]}>
- <Routes>
- <Route element={<ProfileOutlet {...overrides} />}>
- <Route element={<ProfileContainer />}>
- <Route path="*" element={<WrappedChild />} />
+ <IntlWrapper>
+ <Routes>
+ <Route element={<ProfileOutlet {...overrides} />}>
+ <Route element={<ProfileContainer />}>
+ <Route path="*" element={<WrappedChild />} />
+ </Route>
</Route>
- </Route>
- </Routes>
+ </Routes>
+ </IntlWrapper>
</MemoryRouter>
</HelmetProvider>,
);
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import styled from '@emotion/styled';
+import { FlagMessage, themeColor } from 'design-system';
import * as React from 'react';
-import { Alert } from '../../../components/ui/Alert';
import { translate } from '../../../helpers/l10n';
import { withQualityProfilesContext } from '../qualityProfilesContext';
import { Exporter, Profile } from '../types';
const { profile, profiles, exporters } = props;
return (
- <div>
- <div className="quality-profile-grid">
- <div className="quality-profile-grid-left">
- <ProfileRules profile={profile} />
- <ProfileExporters exporters={exporters} profile={profile} />
- {profile.actions?.edit && !profile.isBuiltIn && <ProfilePermissions profile={profile} />}
- </div>
- <div className="quality-profile-grid-right">
+ <ContentWrapper>
+ <div className="sw-grid sw-grid-cols-3 sw-gap-12">
+ <div className="sw-col-span-2 sw-flex sw-flex-col sw-gap-12">
{profile.activeRuleCount === 0 && (profile.projectCount || profile.isDefault) && (
- <Alert className="big-spacer-bottom" variant="warning">
+ <FlagMessage variant="warning">
{profile.projectCount !== undefined &&
profile.projectCount > 0 &&
translate('quality_profiles.warning.used_by_projects_no_rules')}
{!profile.projectCount &&
profile.isDefault &&
translate('quality_profiles.warning.is_default_no_rules')}
- </Alert>
+ </FlagMessage>
)}
<ProfileInheritance
updateProfiles={props.updateProfiles}
/>
<ProfileProjects profile={profile} />
+ {profile.actions?.edit && !profile.isBuiltIn && <ProfilePermissions profile={profile} />}
+ </div>
+ <div className="sw-flex sw-flex-col sw-gap-12">
+ <ProfileRules profile={profile} />
+ <ProfileExporters exporters={exporters} profile={profile} />
</div>
</div>
- </div>
+ </ContentWrapper>
);
}
+const ContentWrapper = styled.div`
+ color: ${themeColor('pageContent')};
+`;
+
export default withQualityProfilesContext(ProfileDetails);
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { Link, SubTitle } from 'design-system';
import * as React from 'react';
import { getQualityProfileExporterUrl } from '../../../api/quality-profiles';
-import Link from '../../../components/common/Link';
-import { Alert } from '../../../components/ui/Alert';
import { translate } from '../../../helpers/l10n';
import { Exporter, Profile } from '../types';
}
return (
- <section
- aria-label={translate('quality_profiles.exporters')}
- className="boxed-group quality-profile-exporters"
- >
- <h2>{translate('quality_profiles.exporters')}</h2>
- <div className="boxed-group-inner">
- <Alert className="big-spacer-bottom" variant="warning">
- {translate('quality_profiles.exporters.deprecated')}
- </Alert>
- <ul>
- {exportersForLanguage.map((exporter, index) => (
- <li
- className={index > 0 ? 'spacer-top' : undefined}
- data-key={exporter.key}
- key={exporter.key}
- >
- <Link to={getQualityProfileExporterUrl(exporter, profile)} target="_blank">
- {exporter.name}
- </Link>
- </li>
- ))}
- </ul>
+ <section aria-label={translate('quality_profiles.exporters')}>
+ <div>
+ <SubTitle>{translate('quality_profiles.exporters')}</SubTitle>
</div>
+ <ul className="sw-flex sw-flex-col sw-gap-2">
+ {exportersForLanguage.map((exporter) => (
+ <li data-key={exporter.key} key={exporter.key}>
+ <Link to={getQualityProfileExporterUrl(exporter, profile)}>{exporter.name}</Link>
+ </li>
+ ))}
+ </ul>
</section>
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { Badge, Breadcrumbs, HoverLink, Link } from 'design-system';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
-import { FormattedMessage } from 'react-intl';
-import { NavLink } from 'react-router-dom';
-import Link from '../../../components/common/Link';
-import HelpTooltip from '../../../components/controls/HelpTooltip';
-import Tooltip from '../../../components/controls/Tooltip';
import { useLocation } from '../../../components/hoc/withRouter';
import DateFromNow from '../../../components/intl/DateFromNow';
+import { AdminPageHeader } from '../../../components/ui/AdminPageHeader';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { getQualityProfileUrl } from '../../../helpers/urls';
import BuiltInQualityProfileBadge from '../components/BuiltInQualityProfileBadge';
import ProfileActions from '../components/ProfileActions';
-import ProfileLink from '../components/ProfileLink';
import { PROFILE_PATH } from '../constants';
import { QualityProfilePath } from '../routes';
import { Profile } from '../types';
-import {
- getProfileChangelogPath,
- getProfilesForLanguagePath,
- isProfileComparePath,
-} from '../utils';
+import { getProfileChangelogPath, isProfileComparePath } from '../utils';
interface Props {
profile: Profile;
const isChangeLogPage = location.pathname.endsWith(`/${QualityProfilePath.CHANGELOG}`);
return (
- <header className="page-header quality-profile-header">
+ <div className="it__quality-profiles__header">
{(isComparePage || isChangeLogPage) && (
<Helmet
defer={false}
)}
/>
)}
- <nav className="note spacer-bottom" aria-label={translate('breadcrumbs')}>
- <ul className="list-breadcrumbs">
- <li>
- <NavLink end to={PROFILE_PATH}>
- {translate('quality_profiles.page')}
- </NavLink>
- </li>
- <li>
- <Link to={getProfilesForLanguagePath(profile.language)}>{profile.languageName}</Link>
- </li>
- </ul>
- </nav>
- <h1 className="page-title">
- <ProfileLink language={profile.language} name={profile.name}>
- <span>{profile.name}</span>
- </ProfileLink>
- {profile.isDefault && (
- <Tooltip overlay={translate('quality_profiles.list.default.help')}>
- <span className=" spacer-left badge">{translate('default')}</span>
- </Tooltip>
- )}
- {profile.isBuiltIn && (
- <BuiltInQualityProfileBadge className="spacer-left" tooltip={false} />
- )}
- </h1>
- {!isProfileComparePath(location.pathname) && (
- <div className="pull-right">
- <ul className="list-inline" style={{ lineHeight: '24px' }}>
- <li className="small spacer-right">
- {translate('quality_profiles.updated_')} <DateFromNow date={profile.rulesUpdatedAt} />
- </li>
- <li className="small big-spacer-right">
- {translate('quality_profiles.used_')} <DateFromNow date={profile.lastUsed} />
- </li>
- <li>
- <Link className="button" to={getProfileChangelogPath(profile.name, profile.language)}>
- {translate('changelog')}
- </Link>
- </li>
+ <Breadcrumbs className="sw-mb-6">
+ <HoverLink to={PROFILE_PATH}>{translate('quality_profiles.page')}</HoverLink>
+ <HoverLink to={getQualityProfileUrl(profile.name, profile.language)}>
+ {profile.languageName}
+ </HoverLink>
+ </Breadcrumbs>
- <li>
- <ProfileActions
- className="pull-left"
- profile={profile}
- isComparable={isComparable}
- updateProfiles={updateProfiles}
- />
- </li>
- </ul>
- </div>
- )}
+ <AdminPageHeader
+ description={profile.isBuiltIn && translate('quality_profiles.built_in.description')}
+ title={
+ <span className="sw-inline-flex sw-items-center sw-gap-2">
+ {profile.name}
+ {profile.isBuiltIn && <BuiltInQualityProfileBadge tooltip={false} />}
+ {profile.isDefault && <Badge>{translate('default')}</Badge>}
+ </span>
+ }
+ >
+ <div className="sw-flex sw-items-center sw-gap-3 sw-self-start">
+ {!isProfileComparePath(location.pathname) && (
+ <>
+ <div>
+ <strong className="sw-body-sm-highlight">
+ {translate('quality_profiles.updated_')}
+ </strong>{' '}
+ <DateFromNow date={profile.rulesUpdatedAt} />
+ </div>
+ <div>
+ <strong className="sw-body-sm-highlight">
+ {translate('quality_profiles.used_')}
+ </strong>{' '}
+ <DateFromNow date={profile.lastUsed} />
+ </div>
- {profile.isBuiltIn && (
- <div className="page-description">{translate('quality_profiles.built_in.description')}</div>
- )}
+ <div>
+ <Link
+ className="it__quality-profiles__changelog"
+ to={getProfileChangelogPath(profile.name, profile.language)}
+ >
+ {translate('see_changelog')}
+ </Link>
+ </div>
+ </>
+ )}
- {profile.parentKey && profile.parentName && (
- <div className="page-description">
- <FormattedMessage
- defaultMessage={translate('quality_profiles.extend_description')}
- id="quality_profiles.extend_description"
- values={{
- link: (
- <>
- <Link to={getQualityProfileUrl(profile.parentName, profile.language)}>
- {profile.parentName}
- </Link>
- <HelpTooltip
- className="little-spacer-left"
- overlay={translateWithParameters(
- 'quality_profiles.extend_description_help',
- profile.parentName,
- )}
- />
- </>
- ),
- }}
+ <ProfileActions
+ profile={profile}
+ isComparable={isComparable}
+ updateProfiles={updateProfiles}
/>
</div>
- )}
- </header>
+ </AdminPageHeader>
+ </div>
);
}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import classNames from 'classnames';
+import { ButtonSecondary, Spinner, SubTitle, Table } from 'design-system';
import * as React from 'react';
import { getProfileInheritance } from '../../../api/quality-profiles';
-import { Button } from '../../../components/controls/buttons';
import { translate } from '../../../helpers/l10n';
import { ProfileInheritanceDetails } from '../../../types/types';
import { Profile } from '../types';
render() {
const { profile, profiles } = this.props;
- const { ancestors } = this.state;
+ const { ancestors, loading, formOpen, children } = this.state;
const highlightCurrent =
!this.state.loading &&
this.state.children != null &&
(ancestors.length > 0 || this.state.children.length > 0);
- const extendsBuiltIn = ancestors != null && ancestors.some((profile) => profile.isBuiltIn);
+ const extendsBuiltIn = ancestors?.some((profile) => profile.isBuiltIn);
return (
<section
aria-label={translate('quality_profiles.profile_inheritance')}
- className="boxed-group quality-profile-inheritance"
+ className="it__quality-profiles__inheritance"
>
- {profile.actions && profile.actions.edit && !profile.isBuiltIn && (
- <div className="boxed-group-actions">
- <Button className="pull-right js-change-parent" onClick={this.handleChangeParentClick}>
+ <div className="sw-flex sw-items-center sw-gap-3 sw-mb-6">
+ <SubTitle className="sw-mb-0">
+ {translate('quality_profiles.profile_inheritance')}
+ </SubTitle>
+ {profile.actions?.edit && !profile.isBuiltIn && (
+ <ButtonSecondary
+ className="it__quality-profiles__change-parent"
+ onClick={this.handleChangeParentClick}
+ >
{translate('quality_profiles.change_parent')}
- </Button>
- </div>
- )}
-
- <div className="boxed-group-header">
- <h2>{translate('quality_profiles.profile_inheritance')}</h2>
- </div>
-
- <div className="boxed-group-inner">
- {this.state.loading ? (
- <i className="spinner" />
- ) : (
- <table className="data zebra">
- <tbody>
- {ancestors != null &&
- ancestors.map((ancestor, index) => (
- <ProfileInheritanceBox
- depth={index}
- key={ancestor.key}
- language={profile.language}
- profile={ancestor}
- type="ancestor"
- />
- ))}
-
- {this.state.profile != null && (
- <ProfileInheritanceBox
- className={classNames({
- selected: highlightCurrent,
- })}
- depth={ancestors ? ancestors.length : 0}
- displayLink={false}
- extendsBuiltIn={extendsBuiltIn}
- language={profile.language}
- profile={this.state.profile}
- />
- )}
-
- {this.state.children != null &&
- this.state.children.map((child) => (
- <ProfileInheritanceBox
- depth={ancestors ? ancestors.length + 1 : 0}
- key={child.key}
- language={profile.language}
- profile={child}
- type="child"
- />
- ))}
- </tbody>
- </table>
+ </ButtonSecondary>
)}
</div>
- {this.state.formOpen && (
+ <Spinner loading={loading}>
+ <Table columnCount={3} noSidePadding>
+ {ancestors?.map((ancestor, index) => (
+ <ProfileInheritanceBox
+ depth={index}
+ key={ancestor.key}
+ language={profile.language}
+ profile={ancestor}
+ type="ancestor"
+ />
+ ))}
+
+ {this.state.profile && (
+ <ProfileInheritanceBox
+ className={classNames({
+ selected: highlightCurrent,
+ })}
+ depth={ancestors ? ancestors.length : 0}
+ displayLink={false}
+ extendsBuiltIn={extendsBuiltIn}
+ language={profile.language}
+ profile={this.state.profile}
+ />
+ )}
+
+ {children?.map((child) => (
+ <ProfileInheritanceBox
+ depth={ancestors ? ancestors.length + 1 : 0}
+ key={child.key}
+ language={profile.language}
+ profile={child}
+ type="child"
+ />
+ ))}
+ </Table>
+ </Spinner>
+
+ {formOpen && (
<ChangeParentForm
onChange={this.handleParentChange}
onClose={this.closeForm}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import classNames from 'classnames';
+import { ContentCell, HelperHintIcon, TableRow } from 'design-system';
import * as React from 'react';
import HelpTooltip from '../../../components/controls/HelpTooltip';
import { translate, translateWithParameters } from '../../../helpers/l10n';
const offset = 25 * depth;
return (
- <tr className={classNames(`it__quality-profiles__inheritance-${type}`, className)}>
- <td>
- <div style={{ paddingLeft: offset }}>
+ <TableRow className={classNames(`it__quality-profiles__inheritance-${type}`, className)}>
+ <ContentCell>
+ <div className="sw-flex sw-items-center sw-gap-2" style={{ paddingLeft: offset }}>
{displayLink ? (
- <ProfileLink className="text-middle" language={language} name={profile.name}>
+ <ProfileLink language={language} name={profile.name}>
{profile.name}
</ProfileLink>
) : (
- <span className="text-middle">{profile.name}</span>
+ <span>{profile.name}</span>
)}
- {profile.isBuiltIn && <BuiltInQualityProfileBadge className="spacer-left" />}
+ {profile.isBuiltIn && <BuiltInQualityProfileBadge />}
{extendsBuiltIn && (
- <HelpTooltip
- className="spacer-left"
- overlay={translate('quality_profiles.extends_built_in')}
- />
+ <HelpTooltip overlay={translate('quality_profiles.extends_built_in')}>
+ <HelperHintIcon aria-label="help-tooltip" />
+ </HelpTooltip>
)}
</div>
- </td>
+ </ContentCell>
- <td>{translateWithParameters('quality_profile.x_active_rules', profile.activeRuleCount)}</td>
+ <ContentCell>
+ {translateWithParameters('quality_profile.x_active_rules', profile.activeRuleCount)}
+ </ContentCell>
- <td>
+ <ContentCell>
{profile.overridingRuleCount != null && (
<p>
{translateWithParameters(
)}
</p>
)}
- </td>
- </tr>
+ </ContentCell>
+ </TableRow>
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { ButtonSecondary, Note, Spinner, SubTitle } from 'design-system';
import { sortBy, uniqBy } from 'lodash';
import * as React from 'react';
import {
searchGroups,
searchUsers,
} from '../../../api/quality-profiles';
-import { Button } from '../../../components/controls/buttons';
import { translate } from '../../../helpers/l10n';
import { UserSelected } from '../../../types/types';
import { Profile } from '../types';
};
render() {
+ const { loading } = this.state;
+
return (
- <section aria-label={translate('permissions.page')} className="boxed-group">
- <h2>{translate('permissions.page')}</h2>
- <div className="boxed-group-inner">
- <p className="note">{translate('quality_profiles.default_permissions')}</p>
-
- {this.state.loading ? (
- <div className="big-spacer-top">
- <i className="spinner" />
- </div>
- ) : (
- <div className="big-spacer-top">
- {this.state.users &&
- sortBy(this.state.users, 'name').map((user) => (
- <ProfilePermissionsUser
- key={user.login}
- onDelete={this.handleUserDelete}
- profile={this.props.profile}
- user={user}
- />
- ))}
- {this.state.groups &&
- sortBy(this.state.groups, 'name').map((group) => (
- <ProfilePermissionsGroup
- group={group}
- key={group.name}
- onDelete={this.handleGroupDelete}
- profile={this.props.profile}
- />
- ))}
- <div className="text-right">
- <Button onClick={this.handleAddUserButtonClick}>
- {translate('quality_profiles.grant_permissions_to_more_users')}
- </Button>
- </div>
- </div>
- )}
+ <section aria-label={translate('permissions.page')}>
+ <div className="sw-mb-6">
+ <SubTitle className="sw-mb-0">{translate('permissions.page')}</SubTitle>
+ <Note as="p">{translate('quality_profiles.default_permissions')}</Note>
</div>
+ <Spinner loading={loading}>
+ <ul className="sw-flex sw-flex-col sw-gap-4 sw-max-w-[238px]">
+ {this.state.users &&
+ sortBy(this.state.users, 'name').map((user) => (
+ <ProfilePermissionsUser
+ key={user.login}
+ onDelete={this.handleUserDelete}
+ profile={this.props.profile}
+ user={user}
+ />
+ ))}
+ {this.state.groups &&
+ sortBy(this.state.groups, 'name').map((group) => (
+ <ProfilePermissionsGroup
+ group={group}
+ key={group.name}
+ onDelete={this.handleGroupDelete}
+ profile={this.props.profile}
+ />
+ ))}
+ </ul>
+ <div className="sw-mt-6">
+ <ButtonSecondary onClick={this.handleAddUserButtonClick}>
+ {translate('quality_profiles.grant_permissions_to_more_users')}
+ </ButtonSecondary>
+ </div>
+ </Spinner>
+
{this.state.addUserForm && (
<ProfilePermissionsForm
onClose={this.handleAddUserFormClose}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { DestructiveIcon, GenericAvatar, TrashIcon, UserGroupIcon } from 'design-system';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { removeGroup } from '../../../api/quality-profiles';
import SimpleModal, { ChildrenProps } from '../../../components/controls/SimpleModal';
-import { Button, DeleteButton, ResetButtonLink } from '../../../components/controls/buttons';
-import GroupIcon from '../../../components/icons/GroupIcon';
+import { Button, ResetButtonLink } from '../../../components/controls/buttons';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Group } from './ProfilePermissions';
const { group } = this.props;
return (
- <div className="clearfix big-spacer-bottom">
- <DeleteButton
+ <li className="sw-flex sw-items-center sw-justify-between sw-mb-4">
+ <div className="sw-flex sw-items-center sw-truncate">
+ <GenericAvatar
+ Icon={UserGroupIcon}
+ className="sw-mr-3 sw-grow-0 sw-shrink-0"
+ name={group.name}
+ />
+ <strong className="sw-body-sm-highlight sw-truncate fs-mask">{group.name}</strong>
+ </div>
+ <DestructiveIcon
+ Icon={TrashIcon}
aria-label={translateWithParameters(
'quality_profiles.permissions.remove.group_x',
group.name,
)}
- className="pull-right spacer-top spacer-left spacer-right button-small"
onClick={this.handleDeleteClick}
/>
- <GroupIcon className="pull-left spacer-right" size={32} />
- <div className="overflow-hidden" style={{ lineHeight: '32px' }}>
- <strong>{group.name}</strong>
- </div>
{this.state.deleteModal && (
<SimpleModal
{this.renderDeleteModal}
</SimpleModal>
)}
- </div>
+ </li>
);
}
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { Avatar, DestructiveIcon, Note, TrashIcon } from 'design-system';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { removeUser } from '../../../api/quality-profiles';
import SimpleModal, { ChildrenProps } from '../../../components/controls/SimpleModal';
-import { DeleteButton, ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
-import LegacyAvatar from '../../../components/ui/LegacyAvatar';
+import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { UserSelected } from '../../../types/types';
const { user } = this.props;
return (
- <div className="clearfix big-spacer-bottom">
- <DeleteButton
+ <li className="sw-flex sw-items-center sw-justify-between sw-mb-4">
+ <div className="sw-flex sw-items-center sw-truncate">
+ <Avatar className="sw-mr-3 sw-grow-0 sw-shrink-0" hash={user.avatar} name={user.name} />
+ <div className="sw-truncate fs-mask">
+ <strong className="sw-body-sm-highlight">{user.name}</strong>
+ <Note className="sw-block">{user.login}</Note>
+ </div>
+ </div>
+ <DestructiveIcon
+ Icon={TrashIcon}
aria-label={translateWithParameters(
'quality_profiles.permissions.remove.user_x',
user.name,
)}
- className="pull-right spacer-top spacer-left spacer-right button-small"
onClick={this.handleDeleteClick}
/>
- <LegacyAvatar
- className="pull-left spacer-right"
- hash={user.avatar}
- name={user.name}
- size={32}
- />
- <div className="overflow-hidden">
- <strong>{user.name}</strong>
- <div className="note">{user.login}</div>
- </div>
{this.state.deleteModal && (
<SimpleModal
{this.renderDeleteModal}
</SimpleModal>
)}
- </div>
+ </li>
);
}
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import {
+ Badge,
+ ButtonSecondary,
+ ContentCell,
+ Link,
+ Spinner,
+ SubTitle,
+ Table,
+ TableRow,
+ Tooltip,
+} from 'design-system';
import * as React from 'react';
import { getProfileProjects } from '../../../api/quality-profiles';
-import Link from '../../../components/common/Link';
import ListFooter from '../../../components/controls/ListFooter';
-import Tooltip from '../../../components/controls/Tooltip';
-import { Button } from '../../../components/controls/buttons';
-import QualifierIcon from '../../../components/icons/QualifierIcon';
import { translate } from '../../../helpers/l10n';
import { getProjectUrl } from '../../../helpers/urls';
import { Profile } from '../types';
renderDefault() {
return (
- <div>
- <span className="badge spacer-right">{translate('default')}</span>
+ <>
+ <Badge className="sw-mr-2">{translate('default')}</Badge>
{translate('quality_profiles.projects_for_default')}
- </div>
+ </>
);
}
renderProjects() {
if (this.state.loading) {
- return <i className="spinner" />;
+ return <Spinner />;
}
const { projects } = this.state;
const { profile } = this.props;
if (profile.activeRuleCount === 0 && projects.length === 0) {
- return <div>{translate('quality_profiles.cannot_associate_projects_no_rules')}</div>;
+ return translate('quality_profiles.cannot_associate_projects_no_rules');
}
if (projects.length === 0) {
- return <div>{translate('quality_profiles.no_projects_associated_to_profile')}</div>;
+ return translate('quality_profiles.no_projects_associated_to_profile');
}
return (
<>
- <ul>
+ <Table columnCount={1} noSidePadding>
{projects.map((project) => (
- <li className="spacer-top js-profile-project" data-key={project.key} key={project.key}>
- <Link to={getProjectUrl(project.key)}>
- <QualifierIcon qualifier="TRK" /> <span>{project.name}</span>
- </Link>
- </li>
+ <TableRow key={project.key}>
+ <ContentCell>
+ <Link
+ className="it__quality-profiles__project fs-mask"
+ to={getProjectUrl(project.key)}
+ >
+ {project.name}
+ </Link>
+ </ContentCell>
+ </TableRow>
))}
- </ul>
- <ListFooter
- count={projects.length}
- loadMore={this.loadMore}
- ready={!this.state.loadingMore}
- total={this.state.total}
- />
+ </Table>
+ {projects.length > 0 && (
+ <ListFooter
+ useMIUIButtons
+ count={projects.length}
+ loadMore={this.loadMore}
+ loading={this.state.loadingMore}
+ total={this.state.total}
+ />
+ )}
</>
);
}
const { profile } = this.props;
const hasNoActiveRules = profile.activeRuleCount === 0;
return (
- <section className="boxed-group quality-profile-projects" aria-label={translate('projects')}>
- {profile.actions && profile.actions.associateProjects && (
- <div className="boxed-group-actions">
+ // eslint-disable-next-line local-rules/use-metrickey-enum
+ <section className="it__quality-profiles__projects" aria-label={translate('projects')}>
+ <div className="sw-flex sw-items-center sw-gap-3 sw-mb-6">
+ {
+ // eslint-disable-next-line local-rules/use-metrickey-enum
+ <SubTitle className="sw-mb-0">{translate('projects')}</SubTitle>
+ }
+ {profile.actions?.associateProjects && (
<Tooltip
overlay={
hasNoActiveRules
: null
}
>
- <Button
- className="js-change-projects"
+ <ButtonSecondary
+ className="it__quality-profiles__change-projects"
onClick={this.handleChangeClick}
disabled={hasNoActiveRules}
>
{translate('quality_profiles.change_projects')}
- </Button>
+ </ButtonSecondary>
</Tooltip>
- </div>
- )}
-
- <header className="boxed-group-header">
- <h2>{translate('projects')}</h2>
- </header>
-
- <div className="boxed-group-inner">
- {profile.isDefault ? this.renderDefault() : this.renderProjects()}
+ )}
</div>
+ {profile.isDefault ? this.renderDefault() : this.renderProjects()}
+
{this.state.formOpen && <ChangeProjectsForm onClose={this.closeForm} profile={profile} />}
</section>
);
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import {
+ ButtonPrimary,
+ ContentCell,
+ NumericalCell,
+ SubTitle,
+ Table,
+ TableRow,
+} from 'design-system/lib';
import { keyBy } from 'lodash';
import * as React from 'react';
import { getQualityProfile } from '../../../api/quality-profiles';
import { searchRules } from '../../../api/rules';
-import Link from '../../../components/common/Link';
-import Tooltip from '../../../components/controls/Tooltip';
-import { Button } from '../../../components/controls/buttons';
+import DocumentationTooltip from '../../../components/common/DocumentationTooltip';
import { translate } from '../../../helpers/l10n';
+import { isDefined } from '../../../helpers/types';
import { getRulesUrl } from '../../../helpers/urls';
import { SearchRulesResponse } from '../../../types/coding-rules';
import { Dict } from '../../../types/types';
import { Profile } from '../types';
import ProfileRulesDeprecatedWarning from './ProfileRulesDeprecatedWarning';
-import ProfileRulesRowOfType from './ProfileRulesRowOfType';
-import ProfileRulesRowTotal from './ProfileRulesRowTotal';
+import ProfileRulesRow from './ProfileRulesRow';
import ProfileRulesSonarWayComparison from './ProfileRulesSonarWayComparison';
const TYPES = ['BUG', 'VULNERABILITY', 'CODE_SMELL', 'SECURITY_HOTSPOT'];
const { actions = {} } = profile;
return (
- <section aria-label={translate('rules')} className="boxed-group quality-profile-rules">
- <div className="quality-profile-rules-distribution">
- <table className="data condensed">
- <thead>
- <tr>
- <th>
- <h2>{translate('rules')}</h2>
- </th>
- <th>{translate('active')}</th>
- <th>{translate('inactive')}</th>
- </tr>
- </thead>
- <tbody>
- <ProfileRulesRowTotal
- count={this.state.activatedTotal}
- qprofile={profile.key}
- total={this.state.total}
- />
- {TYPES.map((type) => (
- <ProfileRulesRowOfType
- count={this.getRulesCountForType(type)}
- key={type}
- qprofile={profile.key}
- total={this.getRulesTotalForType(type)}
- type={type}
- />
- ))}
- </tbody>
- </table>
+ <section aria-label={translate('rules')} className="it__quality-profiles__rules">
+ <Table
+ columnCount={3}
+ columnWidths={['50%', '25%', '25%']}
+ header={
+ <TableRow>
+ <ContentCell>
+ <SubTitle className="sw-mb-0">{translate('rules')}</SubTitle>
+ </ContentCell>
+ <NumericalCell>{translate('active')}</NumericalCell>
+ <NumericalCell>{translate('inactive')}</NumericalCell>
+ </TableRow>
+ }
+ noHeaderTopBorder
+ noSidePadding
+ >
+ <ProfileRulesRow
+ count={this.state.activatedTotal}
+ qprofile={profile.key}
+ total={this.state.total}
+ />
+ {TYPES.map((type) => (
+ <ProfileRulesRow
+ count={this.state.activatedByType[type]?.count}
+ key={type}
+ qprofile={profile.key}
+ total={this.state.allByType[type]?.count}
+ type={type}
+ />
+ ))}
+ </Table>
+
+ <div className="sw-mt-6 sw-flex sw-flex-col sw-gap-4 sw-items-start">
+ {profile.activeDeprecatedRuleCount > 0 && (
+ <ProfileRulesDeprecatedWarning
+ activeDeprecatedRules={profile.activeDeprecatedRuleCount}
+ profile={profile.key}
+ />
+ )}
+
+ {isDefined(compareToSonarWay) && compareToSonarWay.missingRuleCount > 0 && (
+ <ProfileRulesSonarWayComparison
+ language={profile.language}
+ profile={profile.key}
+ sonarWayMissingRules={compareToSonarWay.missingRuleCount}
+ sonarway={compareToSonarWay.profile}
+ />
+ )}
{actions.edit && !profile.isBuiltIn && (
- <div className="text-right big-spacer-top">
- <Link className="button js-activate-rules" to={activateMoreUrl}>
- {translate('quality_profiles.activate_more')}
- </Link>
- </div>
+ <ButtonPrimary className="it__quality-profiles__activate-rules" to={activateMoreUrl}>
+ {translate('quality_profiles.activate_more')}
+ </ButtonPrimary>
)}
{/* if a user is allowed to `copy` a profile if they are a global admin */}
- {/* this user could potentially active more rules if the profile was not built-in */}
+ {/* this user could potentially activate more rules if the profile was not built-in */}
{/* in such cases it's better to show the button but disable it with a tooltip */}
{actions.copy && profile.isBuiltIn && (
- <div className="text-right big-spacer-top">
- <Tooltip overlay={translate('quality_profiles.activate_more.help.built_in')}>
- <Button className="disabled js-activate-rules">
- {translate('quality_profiles.activate_more')}
- </Button>
- </Tooltip>
- </div>
+ <DocumentationTooltip
+ content={translate('quality_profiles.activate_more.help.built_in')}
+ >
+ <ButtonPrimary className="it__quality-profiles__activate-rules" disabled>
+ {translate('quality_profiles.activate_more')}
+ </ButtonPrimary>
+ </DocumentationTooltip>
)}
</div>
- {profile.activeDeprecatedRuleCount > 0 && (
- <ProfileRulesDeprecatedWarning
- activeDeprecatedRules={profile.activeDeprecatedRuleCount}
- profile={profile.key}
- />
- )}
- {compareToSonarWay != null && compareToSonarWay.missingRuleCount > 0 && (
- <ProfileRulesSonarWayComparison
- language={profile.language}
- profile={profile.key}
- sonarWayMissingRules={compareToSonarWay.missingRuleCount}
- sonarway={compareToSonarWay.profile}
- />
- )}
</section>
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { FlagMessage, HelperHintIcon, Link } from 'design-system';
import * as React from 'react';
-import Link from '../../../components/common/Link';
+import { FormattedMessage } from 'react-intl';
import HelpTooltip from '../../../components/controls/HelpTooltip';
import { translate } from '../../../helpers/l10n';
import { getDeprecatedActiveRulesUrl } from '../../../helpers/urls';
export default function ProfileRulesDeprecatedWarning(props: Props) {
return (
- <div className="quality-profile-rules-deprecated clearfix">
- <span className="pull-left">
- <span className="text-middle">{translate('quality_profiles.deprecated_rules')}</span>
- <HelpTooltip
- className="spacer-left"
- overlay={translate('quality_profiles.deprecated_rules_description')}
+ <FlagMessage variant="warning">
+ <div className="sw-flex sw-gap-1">
+ <FormattedMessage
+ defaultMessage={translate('quality_profiles.x_deprecated_rules')}
+ id="quality_profiles.x_deprecated_rules"
+ values={{
+ count: props.activeDeprecatedRules,
+ linkCount: (
+ <Link to={getDeprecatedActiveRulesUrl({ qprofile: props.profile })}>
+ {props.activeDeprecatedRules}
+ </Link>
+ ),
+ }}
/>
- </span>
- <Link className="pull-right" to={getDeprecatedActiveRulesUrl({ qprofile: props.profile })}>
- {props.activeDeprecatedRules}
- </Link>
- </div>
+ <HelpTooltip overlay={translate('quality_profiles.deprecated_rules_description')}>
+ <HelperHintIcon aria-label="help-tooltip" />
+ </HelpTooltip>
+ </div>
+ </FlagMessage>
);
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { ContentCell, Link, Note, NumericalCell, TableRow } from 'design-system';
+import * as React from 'react';
+import IssueTypeIcon from '../../../components/icons/IssueTypeIcon';
+import { translate } from '../../../helpers/l10n';
+import { formatMeasure } from '../../../helpers/measures';
+import { isDefined } from '../../../helpers/types';
+import { getRulesUrl } from '../../../helpers/urls';
+import { MetricType } from '../../../types/metrics';
+
+interface Props {
+ count: number | null;
+ qprofile: string;
+ total: number | null;
+ type?: string;
+}
+
+export default function ProfileRulesRowOfType(props: Props) {
+ const activeRulesUrl = getRulesUrl({
+ qprofile: props.qprofile,
+ activation: 'true',
+ types: props.type,
+ });
+ const inactiveRulesUrl = getRulesUrl({
+ qprofile: props.qprofile,
+ activation: 'false',
+ types: props.type,
+ });
+ let inactiveCount = null;
+ if (props.count != null && props.total != null) {
+ inactiveCount = props.total - props.count;
+ }
+
+ return (
+ <TableRow>
+ <ContentCell>
+ {props.type ? (
+ <>
+ <IssueTypeIcon className="sw-mr-1" query={props.type} />
+ {translate('issue.type', props.type, 'plural')}
+ </>
+ ) : (
+ translate('total')
+ )}
+ </ContentCell>
+ <NumericalCell>
+ {isDefined(props.count) && (
+ <Link to={activeRulesUrl}>{formatMeasure(props.count, MetricType.ShortInteger)}</Link>
+ )}
+ </NumericalCell>
+ <NumericalCell>
+ {isDefined(inactiveCount) &&
+ (inactiveCount > 0 ? (
+ <Link to={inactiveRulesUrl}>
+ {formatMeasure(inactiveCount, MetricType.ShortInteger)}
+ </Link>
+ ) : (
+ <Note>0</Note>
+ ))}
+ </NumericalCell>
+ </TableRow>
+ );
+}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 Link from '../../../components/common/Link';
-import IssueTypeIcon from '../../../components/icons/IssueTypeIcon';
-import { translate } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
-import { getRulesUrl } from '../../../helpers/urls';
-
-interface Props {
- count: number | null;
- qprofile: string;
- total: number | null;
- type: string;
-}
-
-export default function ProfileRulesRowOfType(props: Props) {
- const activeRulesUrl = getRulesUrl({
- qprofile: props.qprofile,
- activation: 'true',
- types: props.type,
- });
- const inactiveRulesUrl = getRulesUrl({
- qprofile: props.qprofile,
- activation: 'false',
- types: props.type,
- });
- let inactiveCount = null;
- if (props.count != null && props.total != null) {
- inactiveCount = props.total - props.count;
- }
-
- return (
- <tr>
- <td>
- <span>
- <IssueTypeIcon className="little-spacer-right" query={props.type} />
- {translate('issue.type', props.type, 'plural')}
- </span>
- </td>
- <td className="thin nowrap text-right">
- {props.count != null && (
- <Link to={activeRulesUrl}>{formatMeasure(props.count, 'SHORT_INT', null)}</Link>
- )}
- </td>
- <td className="thin nowrap text-right">
- {inactiveCount != null &&
- (inactiveCount > 0 ? (
- <Link className="small" to={inactiveRulesUrl}>
- {formatMeasure(inactiveCount, 'SHORT_INT', null)}
- </Link>
- ) : (
- <span className="note">0</span>
- ))}
- </td>
- </tr>
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 Link from '../../../components/common/Link';
-import { translate } from '../../../helpers/l10n';
-import { formatMeasure } from '../../../helpers/measures';
-import { getRulesUrl } from '../../../helpers/urls';
-
-interface Props {
- count: number | null;
- qprofile: string;
- total: number | null;
-}
-
-export default function ProfileRulesRowTotal(props: Props) {
- const activeRulesUrl = getRulesUrl({ qprofile: props.qprofile, activation: 'true' });
- const inactiveRulesUrl = getRulesUrl({ qprofile: props.qprofile, activation: 'false' });
- let inactiveCount = null;
- if (props.count != null && props.total != null) {
- inactiveCount = props.total - props.count;
- }
-
- return (
- <tr>
- <td>
- <strong>{translate('total')}</strong>
- </td>
- <td className="thin nowrap text-right">
- {props.count != null && (
- <Link to={activeRulesUrl}>
- <strong>{formatMeasure(props.count, 'SHORT_INT', null)}</strong>
- </Link>
- )}
- </td>
- <td className="thin nowrap text-right">
- {inactiveCount != null &&
- (inactiveCount > 0 ? (
- <Link className="small" to={inactiveRulesUrl}>
- <strong>{formatMeasure(inactiveCount, 'SHORT_INT', null)}</strong>
- </Link>
- ) : (
- <span className="note">0</span>
- ))}
- </td>
- </tr>
- );
-}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { FlagMessage, Link } from 'design-system';
import * as React from 'react';
-import Link from '../../../components/common/Link';
+import { FormattedMessage } from 'react-intl';
import HelpTooltip from '../../../components/controls/HelpTooltip';
import { translate } from '../../../helpers/l10n';
import { getRulesUrl } from '../../../helpers/urls';
});
return (
- <div className="quality-profile-rules-sonarway-missing clearfix">
- <span className="pull-left">
- <span className="text-middle">{translate('quality_profiles.sonarway_missing_rules')}</span>
+ <FlagMessage variant="warning">
+ <div className="sw-flex sw-items-center sw-gap-1">
+ <FormattedMessage
+ defaultMessage={translate('quality_profiles.x_sonarway_missing_rules')}
+ id="quality_profiles.x_sonarway_missing_rules"
+ values={{
+ count: props.sonarWayMissingRules,
+ linkCount: <Link to={url}>{props.sonarWayMissingRules}</Link>,
+ }}
+ />
<HelpTooltip
- className="spacer-left"
+ className="sw-ml-2"
overlay={translate('quality_profiles.sonarway_missing_rules_description')}
/>
- </span>
- <Link className="pull-right" data-test="rules" to={url}>
- {props.sonarWayMissingRules}
- </Link>
- </div>
+ </div>
+ </FlagMessage>
);
}
background-color: var(--alertBackgroundError);
}
-.quality-profile-rules-sonarway-missing {
- margin-top: 20px;
- padding: 15px 20px;
- background-color: var(--alertBackgroundWarning);
-}
-
.quality-profile-not-found {
padding-top: 100px;
text-align: center;
import DropdownIcon from '../icons/DropdownIcon';
import SettingsIcon from '../icons/SettingsIcon';
import { PopupPlacement } from '../ui/popups';
-import { Button, ButtonPlain } from './buttons';
import Dropdown from './Dropdown';
import Tooltip, { Placement } from './Tooltip';
+import { Button, ButtonPlain } from './buttons';
export interface ActionsDropdownProps {
className?: string;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { withTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+import classNames from 'classnames';
+import { themeColor } from 'design-system';
+import React, { ReactNode } from 'react';
+
+interface Props {
+ children?: ReactNode;
+ className?: string;
+ description?: ReactNode;
+ title: ReactNode;
+}
+
+export function AdminPageHeader({ children, className, description, title }: Props) {
+ return (
+ <div className={classNames('sw-flex sw-justify-between', className)}>
+ <header className="sw-flex-1">
+ <AdminPageTitle className="sw-heading-lg sw-pb-4">{title}</AdminPageTitle>
+ <AdminPageDescription className="sw-body-sm sw-pb-12 sw-max-w-9/12">
+ {description}
+ </AdminPageDescription>
+ </header>
+ {children && <div className="sw-flex sw-gap-2">{children}</div>}
+ </div>
+ );
+}
+export const AdminPageTitle = withTheme(styled.h1`
+ color: ${themeColor('pageTitle')};
+`);
+
+export const AdminPageDescription = withTheme(styled.div`
+ color: ${themeColor('pageContent')};
+`);
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { screen } from '@testing-library/react';
+import React from 'react';
+import { renderComponent } from '../../../helpers/testReactTestingUtils';
+import { AdminPageHeader } from '../AdminPageHeader';
+
+it('render correctly', () => {
+ renderAdminPageHeader();
+
+ expect(screen.getByRole('heading', { name: 'Page title' })).toBeInTheDocument();
+ expect(screen.getByText('Page description')).toBeInTheDocument();
+ expect(screen.getByText('Actions')).toBeInTheDocument();
+});
+
+function renderAdminPageHeader() {
+ return renderComponent(
+ <AdminPageHeader description="Page description" title="Page title">
+ Actions
+ </AdminPageHeader>,
+ );
+}
calendar=Calendar
cancel=Cancel
category=Category
+see_changelog=See Changelog
changelog=Changelog
change_verb=Change
check_all=Check all
quality_profiles.cannot_set_default_no_rules=You must activate at least 1 rule before you can make this profile the default profile.
quality_profiles.warning.used_by_projects_no_rules=The current profile is used on several projects, but it has no active rules. Please activate at least 1 rule for this profile.
quality_profiles.warning.is_default_no_rules=The current profile is the default profile, but it has no active rules. Please activate at least 1 rule for this profile.
+quality_profiles.x_sonarway_missing_rules={linkCount} Sonar way {count, plural, one {rule} other {rules}} not included
quality_profiles.parent=Parent:
quality_profiles.parameter_set_to=Parameter {0} set to {1}
quality_profiles.x_rules_only_in={0} rules only in
quality_profiles.latest_new_rules.activated={0}, activated on {1} profile(s)
quality_profiles.latest_new_rules.not_activated={0}, not yet activated
quality_profiles.deprecated_rules=Deprecated Rules
+quality_profiles.x_deprecated_rules={linkCount} deprecated {count, plural, one {rule} other {rules}}
quality_profiles.deprecated_rules_description=These deprecated rules will eventually disappear. You should proactively investigate replacing them.
quality_profiles.deprecated_rules_are_still_activated=Deprecated rules are still activated on {0} quality profile(s):
quality_profiles.sonarway_missing_rules=Sonar way rules not included
quality_profiles.sonarway_missing_rules_description=Recommended rules are missing from your profile
+quality_profiles.x_sonarway_missing_rules={linkCount} Sonar way {count, plural, one {rule} other {rules}} not included
quality_profiles.stagnant_profiles=Stagnant Profiles
quality_profiles.not_updated_more_than_year=The following profiles haven't been updated for more than 1 year:
quality_profiles.exporters=Exporters