menuIsOpen,
onChange,
onInputChange,
+ isClearable,
zLevel = PopupZLevel.Global,
+ placeholder = '',
...rest
} = props;
const [open, setOpen] = React.useState(false);
const ref = React.useRef<Select<Option, IsMulti, Group>>(null);
+ const computedControlLabel = controlLabel ?? (value as Option | undefined)?.label ?? null;
+
const toggleDropdown = React.useCallback(
(value?: boolean) => {
- setOpen(value === undefined ? !open : value);
+ setOpen(value ?? !open);
},
[open],
);
[onInputChange],
);
+ const handleClear = () => {
+ onChange?.(null as OnChangeValue<Option, IsMulti>, {
+ action: 'clear',
+ removedValues: [],
+ });
+ };
+
React.useEffect(() => {
if (open) {
ref.current?.inputRef?.select();
minLength={minLength}
onChange={handleChange}
onInputChange={handleInputChange}
+ placeholder={placeholder}
selectRef={ref}
+ value={value}
/>
</StyledSearchSelectWrapper>
</SearchHighlighterContext.Provider>
ariaLabel={controlAriaLabel}
className={className}
disabled={isDisabled}
+ isClearable={isClearable && Boolean(value)}
isDiscreet={isDiscreet}
- label={controlLabel}
+ label={computedControlLabel}
+ onClear={handleClear}
onClick={() => {
toggleDropdown(true);
}}
import styled from '@emotion/styled';
import classNames from 'classnames';
+import { useIntl } from 'react-intl';
import tw from 'twin.macro';
import { INPUT_SIZES, themeBorder, themeColor, themeContrast } from '../../helpers';
import { Key } from '../../helpers/keyboard';
import { InputSizeKeys } from '../../types/theme';
-import { ChevronDownIcon } from '../icons';
+import { InteractiveIcon } from '../InteractiveIcon';
+import { ChevronDownIcon, CloseIcon } from '../icons';
interface SearchSelectDropdownControlProps {
ariaLabel?: string;
className?: string;
disabled?: boolean;
+ isClearable?: boolean;
isDiscreet?: boolean;
label?: React.ReactNode | string;
+ onClear: VoidFunction;
onClick: VoidFunction;
placeholder?: string;
size?: InputSizeKeys;
disabled,
placeholder,
label,
+ isClearable,
isDiscreet,
+ onClear,
onClick,
size = 'full',
ariaLabel = '',
} = props;
+
+ const intl = useIntl();
+
return (
<StyledControl
aria-label={ariaLabel}
},
)}
>
- <span className="sw-truncate">{label ?? placeholder}</span>
- <ChevronDownIcon className="sw-ml-1" />
+ <span className="sw-flex-1 sw-truncate">{label ?? placeholder}</span>
+ <div className="sw-flex sw-items-center">
+ {isClearable && (
+ <InteractiveIcon
+ Icon={CloseIcon}
+ aria-label={intl.formatMessage({ id: 'clear' })}
+ currentColor
+ onClick={() => {
+ onClear();
+ }}
+ size="small"
+ />
+ )}
+ <ChevronDownIcon />
+ </div>
</InputValue>
</StyledControl>
);
${tw`sw-flex sw-justify-between sw-items-center`};
${tw`sw-rounded-2`};
${tw`sw-box-border`};
- ${tw`sw-px-3 sw-py-2`};
+ ${tw`sw-px-3`};
${tw`sw-body-sm`};
${tw`sw-h-control`};
${tw`sw-leading-4`};
`;
const InputValue = styled.span`
+ height: 100%;
width: 100%;
color: ${themeContrast('inputBackground')};
'/project/issues',
'/project/activity',
'/code',
- '/profiles/show',
- '/profiles/compare',
+ '/profiles',
'/project/extension/securityreport/securityreport',
'/projects',
'/project/information',
size="full"
controlSize="full"
inputId={inputId}
- isClearable
defaultOptions={defaultOptions}
loadOptions={handleAssigneeSearch}
onChange={props.onAssigneeSelect}
size="large"
controlSize="full"
inputId="condition-metric"
- isClearable
defaultOptions={optionsWithDomains}
loadOptions={handleAssigneeSearch}
onChange={handleChange}
controlAriaLabel={translate('quality_gates.permissions.search')}
inputId={USER_SELECT_INPUT_ID}
autoFocus
- isClearable={false}
- placeholder=""
defaultOptions
noOptionsMessage={() => translate('no_results')}
onChange={props.onSelection}
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' }),
+ qualityProfilePageLink: byRole('link', { name: 'quality_profiles.back_to_list' }),
rulesTotalRow: byRole('row', { name: /total/ }),
rulesBugsRow: byRole('row', { name: /issue.type.BUG.plural/ }),
rulesVulnerabilitiesRow: byRole('row', { name: /issue.type.VULNERABILITY/ }),
renderQualityProfile('i-dont-exist');
await ui.waitForDataLoaded();
- expect(await screen.findByText('quality_profiles.not_found')).toBeInTheDocument();
+ expect(
+ await screen.findByRole('heading', { name: 'quality_profiles.not_found' }),
+ ).toBeInTheDocument();
expect(ui.qualityProfilePageLink.get()).toBeInTheDocument();
});
}),
activateConfirmButton: byRole('button', { name: 'coding_rules.activate' }),
namePropupInput: byRole('textbox', { name: 'quality_profiles.new_name required' }),
- filterByLang: byRole('combobox', { name: 'quality_profiles.filter_by:' }),
+ filterByLang: byRole('combobox', { name: 'quality_profiles.select_lang' }),
listLinkCQualityProfile: byRole('link', { name: 'c quality profile' }),
headingNewCQualityProfile: byRole('heading', { name: 'New c quality profile' }),
headingNewCQualityProfileFromCreateButton: byRole('heading', {
stagnantProfilesRegion: byRole('region', { name: 'quality_profiles.stagnant_profiles' }),
recentlyAddedRulesRegion: byRole('region', { name: 'quality_profiles.latest_new_rules' }),
newRuleLink: byRole('link', { name: 'Recently Added Rule' }),
- seeAllNewRulesLink: byRole('link', { name: 'see_all 20 quality_profiles.latest_new_rules' }),
+ seeAllNewRulesLink: byRole('link', { name: 'quality_profiles.latest_new_rules.see_all_x.20' }),
};
it('should list Quality Profiles and filter by language', async () => {
.filter((p) => p.language === profile.language && p !== profile)
.map((p) => ({ value: p.key, label: p.name, isDefault: p.isDefault }));
- const value = options.find((o) => o.value === withKey);
-
const handleProfilesSearch = React.useCallback(
(query: string, cb: (options: Options<Option>) => void) => {
cb(options.filter((option) => option.label.toLowerCase().includes(query.toLowerCase())));
<>
<span className="sw-mr-2">{intl.formatMessage({ id: 'quality_profiles.compare_with' })}</span>
<SearchSelectDropdown
- placeholder=""
controlPlaceholder={intl.formatMessage({ id: 'select_verb' })}
- controlLabel={value?.label}
controlAriaLabel={intl.formatMessage({ id: 'quality_profiles.compare_with' })}
options={options}
onChange={(option: Option) => props.onCompare(option.value)}
* 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 } from 'design-system';
import * as React from 'react';
-import { NavLink } from 'react-router-dom';
-import { translate } from '../../../helpers/l10n';
+import { useIntl } from 'react-intl';
import { PROFILE_PATH } from '../constants';
export default function ProfileNotFound() {
- return (
- <div className="quality-profile-not-found">
- <div className="note spacer-bottom">
- <NavLink end to={PROFILE_PATH}>
- {translate('quality_profiles.page')}
- </NavLink>
- </div>
+ const intl = useIntl();
- <div>{translate('quality_profiles.not_found')}</div>
+ return (
+ <div className="sw-text-center sw-mt-4">
+ <h1 className="sw-body-md-highlight sw-mb-4">
+ {intl.formatMessage({ id: 'quality_profiles.not_found' })}
+ </h1>
+ <Link className="sw-body-sm-highlight" to={PROFILE_PATH}>
+ {intl.formatMessage({ id: 'quality_profiles.back_to_list' })}
+ </Link>
</div>
);
}
export default function Evolution({ profiles }: EvolutionProps) {
return (
- <div className="quality-profiles-evolution">
+ <div className="sw-flex sw-flex-col sw-gap-12">
<EvolutionDeprecated profiles={profiles} />
<EvolutionStagnant profiles={profiles} />
<EvolutionRules />
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { DiscreetLink, FlagMessage, Note } from 'design-system';
import { sortBy } from 'lodash';
import * as React from 'react';
-import Link from '../../../components/common/Link';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { useIntl } from 'react-intl';
import { getDeprecatedActiveRulesUrl } from '../../../helpers/urls';
-import ProfileLink from '../components/ProfileLink';
import { Profile } from '../types';
+import { getProfilePath } from '../utils';
interface Props {
profiles: Profile[];
from: Profile;
}
-export default class EvolutionDeprecated extends React.PureComponent<Props> {
- getDeprecatedRulesInheritanceChain = (profile: Profile, profilesWithDeprecations: Profile[]) => {
- let rules: InheritedRulesInfo[] = [];
- let count = profile.activeDeprecatedRuleCount;
+export default function EvolutionDeprecated({ profiles }: Readonly<Props>) {
+ const intl = useIntl();
- if (count === 0) {
- return rules;
- }
+ const profilesWithDeprecations = profiles.filter(
+ (profile) => profile.activeDeprecatedRuleCount > 0,
+ );
- if (profile.parentKey) {
- const parentProfile = profilesWithDeprecations.find((p) => p.key === profile.parentKey);
- if (parentProfile) {
- const parentRules = this.getDeprecatedRulesInheritanceChain(
- parentProfile,
- profilesWithDeprecations,
- );
- if (parentRules.length) {
- count -= parentRules.reduce((n, rule) => n + rule.count, 0);
- rules = rules.concat(parentRules);
- }
- }
- }
+ if (profilesWithDeprecations.length === 0) {
+ return null;
+ }
- if (count > 0) {
- rules.push({
- count,
- from: profile,
- });
- }
+ const sortedProfiles = sortBy(profilesWithDeprecations, (p) => -p.activeDeprecatedRuleCount);
- return rules;
- };
-
- renderInheritedInfo = (profile: Profile, profilesWithDeprecations: Profile[]) => {
- const rules = this.getDeprecatedRulesInheritanceChain(profile, profilesWithDeprecations);
- if (rules.length) {
- return (
- <>
- {rules.map((rule) => {
- if (rule.from.key === profile.key) {
- return null;
- }
-
- return (
- <div key={rule.from.key}>
- {' '}
- {translateWithParameters(
- 'coding_rules.filters.inheritance.x_inherited_from_y',
- rule.count,
- rule.from.name,
+ return (
+ <section aria-label={intl.formatMessage({ id: 'quality_profiles.deprecated_rules' })}>
+ <h2 className="sw-heading-md sw-mb-6">
+ {intl.formatMessage({ id: 'quality_profiles.deprecated_rules' })}
+ </h2>
+ <FlagMessage variant="error" className="sw-mb-3">
+ {intl.formatMessage(
+ { id: 'quality_profiles.deprecated_rules_are_still_activated' },
+ { count: profilesWithDeprecations.length },
+ )}
+ </FlagMessage>
+
+ <ul className="sw-flex sw-flex-col sw-gap-4 sw-body-sm">
+ {sortedProfiles.map((profile) => (
+ <li className="sw-flex sw-flex-col sw-gap-1" key={profile.key}>
+ <div className="sw-truncate">
+ <DiscreetLink to={getProfilePath(profile.name, profile.language)}>
+ {profile.name}
+ </DiscreetLink>
+ </div>
+
+ <Note>
+ {profile.languageName}
+ {', '}
+ <DiscreetLink
+ className="link-no-underline"
+ to={getDeprecatedActiveRulesUrl({ qprofile: profile.key })}
+ aria-label={intl.formatMessage(
+ { id: 'quality_profile.lang_deprecated_x_rules' },
+ { count: profile.activeDeprecatedRuleCount, name: profile.languageName },
)}
- </div>
- );
- })}
- </>
- );
- }
- return null;
- };
+ >
+ {intl.formatMessage(
+ { id: 'quality_profile.x_rules' },
+ { count: profile.activeDeprecatedRuleCount },
+ )}
+ </DiscreetLink>
+ </Note>
+ <EvolutionDeprecatedInherited
+ profile={profile}
+ profilesWithDeprecations={profilesWithDeprecations}
+ />
+ </li>
+ ))}
+ </ul>
+ </section>
+ );
+}
- render() {
- const profilesWithDeprecations = this.props.profiles.filter(
- (profile) => profile.activeDeprecatedRuleCount > 0,
+function EvolutionDeprecatedInherited(
+ props: Readonly<{
+ profile: Profile;
+ profilesWithDeprecations: Profile[];
+ }>,
+) {
+ const { profile, profilesWithDeprecations } = props;
+ const intl = useIntl();
+ const rules = React.useMemo(
+ () => getDeprecatedRulesInheritanceChain(profile, profilesWithDeprecations),
+ [profile, profilesWithDeprecations],
+ );
+ if (rules.length) {
+ return (
+ <>
+ {rules.map((rule) => {
+ if (rule.from.key === profile.key) {
+ return null;
+ }
+
+ return (
+ <Note key={rule.from.key}>
+ {intl.formatMessage(
+ { id: 'coding_rules.filters.inheritance.x_inherited_from_y' },
+ { count: rule.count, name: rule.from.name },
+ )}
+ </Note>
+ );
+ })}
+ </>
);
+ }
+ return null;
+}
- if (profilesWithDeprecations.length === 0) {
- return null;
- }
+function getDeprecatedRulesInheritanceChain(profile: Profile, profilesWithDeprecations: Profile[]) {
+ let rules: InheritedRulesInfo[] = [];
+ let count = profile.activeDeprecatedRuleCount;
- const sortedProfiles = sortBy(profilesWithDeprecations, (p) => -p.activeDeprecatedRuleCount);
+ if (count === 0) {
+ return rules;
+ }
- return (
- <section
- className="boxed-group boxed-group-inner quality-profiles-evolution-deprecated"
- aria-label={translate('quality_profiles.deprecated_rules')}
- >
- <h2 className="h4 spacer-bottom">{translate('quality_profiles.deprecated_rules')}</h2>
- <div className="spacer-bottom">
- {translateWithParameters(
- 'quality_profiles.deprecated_rules_are_still_activated',
- profilesWithDeprecations.length,
- )}
- </div>
- <ul>
- {sortedProfiles.map((profile) => (
- <li className="spacer-top" key={profile.key}>
- <div className="text-ellipsis little-spacer-bottom">
- <ProfileLink language={profile.language} name={profile.name}>
- {profile.name}
- </ProfileLink>
- </div>
- <div className="note">
- {profile.languageName}
- {', '}
- <Link
- className="link-no-underline"
- to={getDeprecatedActiveRulesUrl({ qprofile: profile.key })}
- aria-label={translateWithParameters(
- 'quality_profile.lang_deprecated_x_rules',
- profile.languageName,
- profile.activeDeprecatedRuleCount,
- )}
- >
- {translateWithParameters(
- 'quality_profile.x_rules',
- profile.activeDeprecatedRuleCount,
- )}
- </Link>
- {this.renderInheritedInfo(profile, profilesWithDeprecations)}
- </div>
- </li>
- ))}
- </ul>
- </section>
- );
+ if (profile.parentKey) {
+ const parentProfile = profilesWithDeprecations.find((p) => p.key === profile.parentKey);
+ if (parentProfile) {
+ const parentRules = getDeprecatedRulesInheritanceChain(
+ parentProfile,
+ profilesWithDeprecations,
+ );
+ if (parentRules.length) {
+ count -= parentRules.reduce((n, rule) => n + rule.count, 0);
+ rules = rules.concat(parentRules);
+ }
+ }
}
+
+ if (count > 0) {
+ rules.push({
+ count,
+ from: profile,
+ });
+ }
+
+ return rules;
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { sortBy } from 'lodash';
+import { DiscreetLink, Link, Note } from 'design-system';
+import { noop, sortBy } from 'lodash';
import * as React from 'react';
+import { useIntl } from 'react-intl';
import { searchRules } from '../../../api/rules';
-import Link from '../../../components/common/Link';
import { toShortISO8601String } from '../../../helpers/dates';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { translateWithParameters } from '../../../helpers/l10n';
import { formatMeasure } from '../../../helpers/measures';
import { getRulesUrl } from '../../../helpers/urls';
import { MetricType } from '../../../types/metrics';
-import { Dict, Rule, RuleActivation } from '../../../types/types';
+import { Rule, RuleActivation } from '../../../types/types';
const RULES_LIMIT = 10;
-function parseRules(rules: Rule[], actives?: Dict<RuleActivation[]>): ExtendedRule[] {
- return rules.map((rule) => {
- const activations = actives?.[rule.key];
- return { ...rule, activations: activations ? activations.length : 0 };
- });
-}
-
interface ExtendedRule extends Rule {
activations: number;
}
-interface State {
- latestRules?: ExtendedRule[];
- latestRulesTotal?: number;
-}
-
-export default class EvolutionRules extends React.PureComponent<{}, State> {
- periodStartDate: string;
- mounted = false;
-
- constructor(props: {}) {
- super(props);
- this.state = {};
+export default function EvolutionRules() {
+ const intl = useIntl();
+ const [latestRules, setLatestRules] = React.useState<ExtendedRule[]>();
+ const [latestRulesTotal, setLatestRulesTotal] = React.useState<number>();
+ const periodStartDate = React.useMemo(() => {
const startDate = new Date();
startDate.setFullYear(startDate.getFullYear() - 1);
- this.periodStartDate = toShortISO8601String(startDate);
- }
-
- componentDidMount() {
- this.mounted = true;
- this.loadLatestRules();
- }
+ return toShortISO8601String(startDate);
+ }, []);
- componentWillUnmount() {
- this.mounted = false;
- }
-
- loadLatestRules() {
+ React.useEffect(() => {
const data = {
asc: false,
- available_since: this.periodStartDate,
+ available_since: periodStartDate,
f: 'name,langName,actives',
ps: RULES_LIMIT,
s: 'createdAt',
};
- searchRules(data).then(
- ({ actives, rules, paging: { total } }) => {
- if (this.mounted) {
- this.setState({
- latestRules: sortBy(parseRules(rules, actives), 'langName'),
- latestRulesTotal: total,
- });
- }
- },
- () => {
- /*noop*/
- },
- );
- }
+ searchRules(data).then(({ actives, rules, paging: { total } }) => {
+ setLatestRules(sortBy(parseRules(rules, actives), 'langName'));
+ setLatestRulesTotal(total);
+ }, noop);
+ }, [periodStartDate]);
- render() {
- const { latestRulesTotal, latestRules } = this.state;
-
- if (!latestRulesTotal || !latestRules) {
- return null;
- }
+ if (!latestRulesTotal || !latestRules) {
+ return null;
+ }
- const newRulesTitle = translate('quality_profiles.latest_new_rules');
- const newRulesUrl = getRulesUrl({ available_since: this.periodStartDate });
- const seeAllRulesText = `${translate('see_all')} ${formatMeasure(
- latestRulesTotal,
- MetricType.ShortInteger,
- )}`;
+ return (
+ <section aria-label={intl.formatMessage({ id: 'quality_profiles.latest_new_rules' })}>
+ <h2 className="sw-heading-md sw-mb-6">
+ {intl.formatMessage({ id: 'quality_profiles.latest_new_rules' })}
+ </h2>
+ <ul className="sw-flex sw-flex-col sw-gap-4 sw-body-sm">
+ {latestRules.map((rule) => (
+ <li className="sw-flex sw-flex-col sw-gap-1" key={rule.key}>
+ <div className="sw-truncate">
+ <DiscreetLink to={getRulesUrl({ rule_key: rule.key })}>{rule.name}</DiscreetLink>
+ </div>
+ <Note className="sw-truncate">
+ {rule.activations
+ ? translateWithParameters(
+ 'quality_profiles.latest_new_rules.activated',
+ rule.langName!,
+ rule.activations,
+ )
+ : translateWithParameters(
+ 'quality_profiles.latest_new_rules.not_activated',
+ rule.langName!,
+ )}
+ </Note>
+ </li>
+ ))}
+ </ul>
+ {latestRulesTotal > RULES_LIMIT && (
+ <div className="sw-mt-6 sw-body-sm-highlight">
+ <Link to={getRulesUrl({ available_since: periodStartDate })}>
+ {intl.formatMessage(
+ { id: 'quality_profiles.latest_new_rules.see_all_x' },
+ { count: formatMeasure(latestRulesTotal, MetricType.ShortInteger) },
+ )}
+ </Link>
+ </div>
+ )}
+ </section>
+ );
+}
- return (
- <section
- className="boxed-group boxed-group-inner quality-profiles-evolution-rules"
- aria-label={newRulesTitle}
- >
- <h2 className="h4 spacer-bottom">{newRulesTitle}</h2>
- <ul>
- {latestRules.map((rule) => (
- <li className="spacer-top" key={rule.key}>
- <div className="text-ellipsis">
- <Link className="link-no-underline" to={getRulesUrl({ rule_key: rule.key })}>
- {' '}
- {rule.name}
- </Link>
- <div className="note">
- {rule.activations
- ? translateWithParameters(
- 'quality_profiles.latest_new_rules.activated',
- rule.langName!,
- rule.activations,
- )
- : translateWithParameters(
- 'quality_profiles.latest_new_rules.not_activated',
- rule.langName!,
- )}
- </div>
- </div>
- </li>
- ))}
- </ul>
- {latestRulesTotal > RULES_LIMIT && (
- <div className="spacer-top">
- <Link
- className="small"
- to={newRulesUrl}
- aria-label={`${seeAllRulesText} ${newRulesTitle}`}
- >
- {seeAllRulesText}
- </Link>
- </div>
- )}
- </section>
- );
- }
+function parseRules(rules: Rule[], actives?: Record<string, RuleActivation[]>): ExtendedRule[] {
+ return rules.map((rule) => {
+ const activations = actives?.[rule.key]?.length ?? 0;
+ return { ...rule, activations };
+ });
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { DiscreetLink, FlagMessage, Note } from 'design-system';
import * as React from 'react';
+import { useIntl } from 'react-intl';
import DateFormatter from '../../../components/intl/DateFormatter';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import ProfileLink from '../components/ProfileLink';
import { Profile } from '../types';
-import { isStagnant } from '../utils';
+import { getProfilePath, isStagnant } from '../utils';
interface Props {
profiles: Profile[];
}
export default function EvolutionStagnant(props: Props) {
+ const intl = useIntl();
const outdated = props.profiles.filter((profile) => !profile.isBuiltIn && isStagnant(profile));
if (outdated.length === 0) {
}
return (
- <section
- className="boxed-group boxed-group-inner quality-profiles-evolution-stagnant"
- aria-label={translate('quality_profiles.stagnant_profiles')}
- >
- <h2 className="h4 spacer-bottom">{translate('quality_profiles.stagnant_profiles')}</h2>
- <div className="spacer-bottom">
- {translate('quality_profiles.not_updated_more_than_year')}
- </div>
- <ul>
+ <section aria-label={intl.formatMessage({ id: 'quality_profiles.stagnant_profiles' })}>
+ <h2 className="sw-heading-md sw-mb-6">
+ {intl.formatMessage({ id: 'quality_profiles.stagnant_profiles' })}
+ </h2>
+
+ <FlagMessage variant="warning" className="sw-mb-3">
+ {intl.formatMessage({ id: 'quality_profiles.not_updated_more_than_year' })}
+ </FlagMessage>
+ <ul className="sw-flex sw-flex-col sw-gap-4 sw-body-sm">
{outdated.map((profile) => (
- <li className="spacer-top" key={profile.key}>
- <div className="text-ellipsis">
- <ProfileLink
- className="link-no-underline"
- language={profile.language}
- name={profile.name}
- >
+ <li className="sw-flex sw-flex-col sw-gap-1" key={profile.key}>
+ <div className="sw-truncate">
+ <DiscreetLink to={getProfilePath(profile.name, profile.language)}>
{profile.name}
- </ProfileLink>
+ </DiscreetLink>
</div>
{profile.rulesUpdatedAt && (
- <DateFormatter date={profile.rulesUpdatedAt} long>
- {(formattedDate) => (
- <div className="note">
- {translateWithParameters(
- 'quality_profiles.x_updated_on_y',
- profile.languageName,
- formattedDate,
- )}
- </div>
- )}
- </DateFormatter>
+ <Note>
+ <DateFormatter date={profile.rulesUpdatedAt} long>
+ {(formattedDate) =>
+ intl.formatMessage(
+ { id: 'quality_profiles.x_updated_on_y' },
+ { name: profile.languageName, date: formattedDate },
+ )
+ }
+ </DateFormatter>
+ </Note>
)}
</li>
))}
import { useOutletContext, useSearchParams } from 'react-router-dom';
import { QualityProfilesContextProps } from '../qualityProfilesContext';
import Evolution from './Evolution';
+import LanguageSelect from './LanguageSelect';
import PageHeader from './PageHeader';
import ProfilesList from './ProfilesList';
<div>
<PageHeader {...context} />
- <div className="page-with-sidebar">
- <main className="page-main">
+ <div className="sw-grid sw-grid-cols-3 sw-gap-12 sw-mt-12">
+ <main className="sw-col-span-2">
+ <LanguageSelect currentFilter={selectedLanguage} languages={context.languages} />
<ProfilesList {...context} language={selectedLanguage} />
</main>
- <aside className="page-sidebar">
+ <aside>
<Evolution {...context} />
</aside>
</div>
--- /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 { LabelValueSelectOption, SearchSelectDropdown } from 'design-system';
+import * as React from 'react';
+import { useIntl } from 'react-intl';
+import { useRouter } from '../../../components/hoc/withRouter';
+import { PROFILE_PATH } from '../constants';
+import { getProfilesForLanguagePath } from '../utils';
+
+const MIN_LANGUAGES = 2;
+
+interface Props {
+ currentFilter?: string;
+ languages: Array<{ key: string; name: string }>;
+}
+
+export default function LanguageSelect(props: Readonly<Props>) {
+ const { currentFilter, languages } = props;
+ const intl = useIntl();
+ const router = useRouter();
+
+ const options = languages.map((language) => ({
+ label: language.name,
+ value: language.key,
+ }));
+
+ const handleLanguagesSearch = React.useCallback(
+ (query: string, cb: (options: LabelValueSelectOption<string>[]) => void) => {
+ cb(options.filter((option) => option.label.toLowerCase().includes(query.toLowerCase())));
+ },
+ [options],
+ );
+
+ if (languages.length < MIN_LANGUAGES) {
+ return null;
+ }
+
+ return (
+ <div className="sw-mb-4">
+ <span className="sw-mr-2 sw-body-sm-highlight">
+ {intl.formatMessage({ id: 'quality_profiles.filter_by' })}
+ </span>
+ <SearchSelectDropdown
+ className="sw-inline-block"
+ controlPlaceholder={intl.formatMessage({ id: 'quality_profiles.select_lang' })}
+ controlAriaLabel={intl.formatMessage({ id: 'quality_profiles.select_lang' })}
+ options={options}
+ onChange={(option: LabelValueSelectOption<string>) =>
+ router.replace(!option ? PROFILE_PATH : getProfilesForLanguagePath(option.value))
+ }
+ defaultOptions={options}
+ loadOptions={handleLanguagesSearch}
+ autoFocus
+ isClearable
+ controlSize="medium"
+ value={options.find((o) => o.value === currentFilter)}
+ />
+ </div>
+ );
+}
* 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, ButtonSecondary, FlagMessage, Link } from 'design-system';
import * as React from 'react';
+import { useIntl } from 'react-intl';
import { Actions } from '../../../api/quality-profiles';
-import DocLink from '../../../components/common/DocLink';
-import { Button } from '../../../components/controls/buttons';
-import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
-import { Alert } from '../../../components/ui/Alert';
+import { useLocation, useRouter } from '../../../components/hoc/withRouter';
+import { useDocUrl } from '../../../helpers/docs';
import { translate } from '../../../helpers/l10n';
import { Profile } from '../types';
import { getProfilePath } from '../utils';
interface Props {
actions: Actions;
languages: Array<{ key: string; name: string }>;
- location: Location;
profiles: Profile[];
- router: Router;
updateProfiles: () => Promise<void>;
}
-interface State {
- createFormOpen: boolean;
- restoreFormOpen: boolean;
-}
-
-export class PageHeader extends React.PureComponent<Props, State> {
- state: State = {
- createFormOpen: false,
- restoreFormOpen: false,
- };
+export default function PageHeader(props: Readonly<Props>) {
+ const { actions, languages, profiles } = props;
+ const intl = useIntl();
+ const location = useLocation();
+ const router = useRouter();
+ const docUrl = useDocUrl();
- handleCreateClick = () => {
- this.setState({ createFormOpen: true });
- };
+ const [modal, setModal] = React.useState<'' | 'createProfile' | 'restoreProfile'>('');
- handleCreate = (profile: Profile) => {
- this.props.updateProfiles().then(
+ const handleCreate = (profile: Profile) => {
+ props.updateProfiles().then(
() => {
- this.props.router.push(getProfilePath(profile.name, profile.language));
+ router.push(getProfilePath(profile.name, profile.language));
},
() => {},
);
};
- closeCreateForm = () => {
- this.setState({ createFormOpen: false });
- };
+ const closeModal = () => setModal('');
- handleRestoreClick = () => {
- this.setState({ restoreFormOpen: true });
- };
+ return (
+ <header className="sw-grid sw-grid-cols-3 sw-gap-12">
+ <div className="sw-col-span-2">
+ <h1 className="sw-heading-lg sw-mb-4">{translate('quality_profiles.page')}</h1>
+ <div className="sw-body-sm">
+ {intl.formatMessage({ id: 'quality_profiles.intro' })}
- closeRestoreForm = () => {
- this.setState({ restoreFormOpen: false });
- };
-
- render() {
- const { actions, languages, location, profiles } = this.props;
- return (
- <header className="page-header">
- <h1 className="page-title">{translate('quality_profiles.page')}</h1>
-
- {actions.create && (
- <div className="page-actions">
- <Button
+ <Link className="sw-ml-2" to={docUrl('/instance-administration/quality-profiles/')}>
+ {intl.formatMessage({ id: 'learn_more' })}
+ </Link>
+ </div>
+ </div>
+ {actions.create && (
+ <div className="sw-flex sw-flex-col sw-items-end">
+ <div>
+ <ButtonPrimary
disabled={languages.length === 0}
id="quality-profiles-create"
- onClick={this.handleCreateClick}
+ onClick={() => setModal('createProfile')}
>
- {translate('create')}
- </Button>
- <Button
- className="little-spacer-left"
+ {intl.formatMessage({ id: 'create' })}
+ </ButtonPrimary>
+ <ButtonSecondary
+ className="sw-ml-2"
id="quality-profiles-restore"
- onClick={this.handleRestoreClick}
+ onClick={() => setModal('restoreProfile')}
>
- {translate('restore')}
- </Button>
- {languages.length === 0 && (
- <Alert className="spacer-top" variant="warning">
- {translate('quality_profiles.no_languages_available')}
- </Alert>
- )}
+ {intl.formatMessage({ id: 'restore' })}
+ </ButtonSecondary>
</div>
- )}
-
- <div className="page-description markdown">
- {translate('quality_profiles.intro1')}
- <br />
- {translate('quality_profiles.intro2')}
- <DocLink className="spacer-left" to="/instance-administration/quality-profiles/">
- {translate('learn_more')}
- </DocLink>
+ {languages.length === 0 && (
+ <FlagMessage className="sw-mt-2" variant="warning">
+ {intl.formatMessage({ id: 'quality_profiles.no_languages_available' })}
+ </FlagMessage>
+ )}
</div>
+ )}
- {this.state.restoreFormOpen && (
- <RestoreProfileForm
- onClose={this.closeRestoreForm}
- onRestore={this.props.updateProfiles}
- />
- )}
+ {modal === 'restoreProfile' && (
+ <RestoreProfileForm onClose={closeModal} onRestore={props.updateProfiles} />
+ )}
- {this.state.createFormOpen && (
- <CreateProfileForm
- languages={languages}
- location={location}
- onClose={this.closeCreateForm}
- onCreate={this.handleCreate}
- profiles={profiles}
- />
- )}
- </header>
- );
- }
+ {modal === 'createProfile' && (
+ <CreateProfileForm
+ languages={languages}
+ location={location}
+ onClose={closeModal}
+ onCreate={handleCreate}
+ profiles={profiles}
+ />
+ )}
+ </header>
+ );
}
-
-export default withRouter(PageHeader);
* 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, FlagMessage, HelperHintIcon, Table, TableRow } from 'design-system';
import { groupBy, pick, sortBy } from 'lodash';
import * as React from 'react';
+import { useIntl } from 'react-intl';
import HelpTooltip from '../../../components/controls/HelpTooltip';
-import { Alert } from '../../../components/ui/Alert';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Language } from '../../../types/languages';
import { Dict } from '../../../types/types';
import { Profile } from '../types';
-import ProfilesListHeader from './ProfilesListHeader';
import ProfilesListRow from './ProfilesListRow';
interface Props {
updateProfiles: () => Promise<void>;
}
-export default class ProfilesList extends React.PureComponent<Props> {
- renderProfiles(profiles: Profile[]) {
- return profiles.map((profile) => (
- <ProfilesListRow
- key={profile.key}
- profile={profile}
- updateProfiles={this.props.updateProfiles}
- isComparable={profiles.length > 1}
- />
- ));
- }
+export default function ProfilesList(props: Readonly<Props>) {
+ const { profiles, languages, language } = props;
+ const intl = useIntl();
- renderHeader(languageKey: string, profilesCount: number) {
- const language = this.props.languages.find((l) => l.key === languageKey);
+ const profilesIndex: Dict<Profile[]> = groupBy<Profile>(profiles, (profile) => profile.language);
+ const profilesToShow = language ? pick(profilesIndex, language) : profilesIndex;
- if (!language) {
- return null;
- }
+ let languagesToShow = sortBy(languages, ({ name }) => name).map(({ key }) => key);
- return (
- <thead>
- <tr>
- <th>
- {language.name}
- {', '}
- {translateWithParameters('quality_profiles.x_profiles', profilesCount)}
- </th>
- <th className="text-right nowrap">
- {translate('quality_profiles.list.projects')}
- <HelpTooltip
- className="table-cell-doc"
- overlay={
- <div className="big-padded-top big-padded-bottom">
- {translate('quality_profiles.list.projects.help')}
- </div>
- }
- />
- </th>
- <th className="text-right nowrap">{translate('quality_profiles.list.rules')}</th>
- <th className="text-right nowrap">{translate('quality_profiles.list.updated')}</th>
- <th className="text-right nowrap">{translate('quality_profiles.list.used')}</th>
- <th> </th>
- </tr>
- </thead>
- );
+ if (language) {
+ languagesToShow = languagesToShow.find((key) => key === language) ? [language] : [];
}
- renderLanguage = (languageKey: string, profiles: Profile[] | undefined) => {
- return (
- <div className="boxed-group boxed-group-inner quality-profiles-table" key={languageKey}>
- <table className="data zebra zebra-hover" data-language={languageKey}>
- {profiles !== undefined && this.renderHeader(languageKey, profiles.length)}
- <tbody>{profiles !== undefined && this.renderProfiles(profiles)}</tbody>
- </table>
- </div>
- );
- };
-
- render() {
- const { profiles, languages, language } = this.props;
-
- const profilesIndex: Dict<Profile[]> = groupBy<Profile>(
- profiles,
- (profile) => profile.language,
- );
+ const renderHeader = React.useCallback(
+ (languageKey: string, count: number) => {
+ const language = languages.find((l) => l.key === languageKey);
- const profilesToShow = language ? pick(profilesIndex, language) : profilesIndex;
-
- let languagesToShow: string[];
- if (language) {
- languagesToShow = languages.find(({ key }) => key === language) ? [language] : [];
- } else {
- languagesToShow = sortBy(languages, ({ name }) => name).map(({ key }) => key);
- }
-
- return (
- <div>
- <ProfilesListHeader currentFilter={language} languages={languages} />
+ return (
+ <TableRow>
+ <ContentCell>
+ {intl.formatMessage(
+ { id: 'quality_profiles.x_profiles' },
+ { count, name: language?.name },
+ )}
+ </ContentCell>
+ <ContentCell>
+ {intl.formatMessage({ id: 'quality_profiles.list.projects' })}
+ <HelpTooltip
+ className="sw-ml-1"
+ overlay={intl.formatMessage({ id: 'quality_profiles.list.projects.help' })}
+ >
+ <HelperHintIcon />
+ </HelpTooltip>
+ </ContentCell>
+ <ContentCell>{intl.formatMessage({ id: 'quality_profiles.list.rules' })}</ContentCell>
+ <ContentCell>{intl.formatMessage({ id: 'quality_profiles.list.updated' })}</ContentCell>
+ <ContentCell>{intl.formatMessage({ id: 'quality_profiles.list.used' })}</ContentCell>
+ <ContentCell> </ContentCell>
+ </TableRow>
+ );
+ },
+ [languages, intl],
+ );
- {Object.keys(profilesToShow).length === 0 && (
- <Alert className="spacer-top" variant="warning">
- {translate('no_results')}
- </Alert>
- )}
+ return (
+ <div>
+ {Object.keys(profilesToShow).length === 0 && (
+ <FlagMessage className="sw-mt-4 sw-w-full" variant="warning">
+ {intl.formatMessage({ id: 'no_results' })}
+ </FlagMessage>
+ )}
- {languagesToShow.map((languageKey) =>
- this.renderLanguage(languageKey, profilesToShow[languageKey]),
- )}
- </div>
- );
- }
+ {languagesToShow.map((languageKey) => (
+ <Table
+ className="sw-mb-12"
+ noSidePadding
+ noHeaderTopBorder
+ key={languageKey}
+ columnCount={6}
+ columnWidths={['43%', '14%', '14%', '14%', '14%', '1%']}
+ header={renderHeader(languageKey, profilesToShow[languageKey].length)}
+ data-language={languageKey}
+ >
+ {profilesToShow[languageKey].map((profile) => (
+ <ProfilesListRow
+ key={profile.key}
+ profile={profile}
+ updateProfiles={props.updateProfiles}
+ isComparable={profilesToShow[languageKey].length > 1}
+ />
+ ))}
+ </Table>
+ ))}
+ </div>
+ );
}
+++ /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 Select from '../../../components/controls/Select';
-import { Router, withRouter } from '../../../components/hoc/withRouter';
-import { translate } from '../../../helpers/l10n';
-import { PROFILE_PATH } from '../constants';
-import { getProfilesForLanguagePath } from '../utils';
-
-interface Props {
- currentFilter?: string;
- languages: Array<{ key: string; name: string }>;
- router: Router;
-}
-
-export class ProfilesListHeader extends React.PureComponent<Props> {
- handleChange = (option: { value: string } | null) => {
- const { router } = this.props;
-
- router.replace(!option ? PROFILE_PATH : getProfilesForLanguagePath(option.value));
- };
-
- render() {
- const { currentFilter, languages } = this.props;
- if (languages.length < 2) {
- return null;
- }
-
- const options = languages.map((language) => ({
- label: language.name,
- value: language.key,
- }));
-
- return (
- <div className="quality-profiles-list-header clearfix">
- <label htmlFor="quality-profiles-filter-input" className="spacer-right">
- {translate('quality_profiles.filter_by')}:
- </label>
- <Select
- className="input-medium"
- autoFocus
- id="quality-profiles-filter"
- inputId="quality-profiles-filter-input"
- isClearable
- onChange={this.handleChange}
- options={options}
- isSearchable
- value={options.filter((o) => o.value === currentFilter)}
- />
- </div>
- );
- }
-}
-
-export default withRouter(ProfilesListHeader);
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { ActionCell, Badge, BaseLink, ContentCell, Link, Note, TableRow } from 'design-system';
import * as React from 'react';
-import Link from '../../../components/common/Link';
+import { useIntl } from 'react-intl';
import Tooltip from '../../../components/controls/Tooltip';
import DateFromNow from '../../../components/intl/DateFromNow';
-import { translate } from '../../../helpers/l10n';
import { getRulesUrl } from '../../../helpers/urls';
import BuiltInQualityProfileBadge from '../components/BuiltInQualityProfileBadge';
import ProfileActions from '../components/ProfileActions';
isComparable: boolean;
}
-export function ProfilesListRow(props: ProfilesListRowProps) {
+export function ProfilesListRow(props: Readonly<ProfilesListRowProps>) {
const { profile, isComparable } = props;
+ const intl = useIntl();
- const offset = 25 * (profile.depth - 1);
+ const offset = 24 * (profile.depth - 1);
const activeRulesUrl = getRulesUrl({
qprofile: profile.key,
activation: 'true',
});
return (
- <tr
- className="quality-profiles-table-row text-middle"
+ <TableRow
+ className="quality-profiles-table-row"
data-key={profile.key}
data-name={profile.name}
>
- <td className="quality-profiles-table-name text-middle">
- <div className="display-flex-center" style={{ paddingLeft: offset }}>
- <div>
- <ProfileLink language={profile.language} name={profile.name}>
- {profile.name}
- </ProfileLink>
- </div>
- {profile.isBuiltIn && <BuiltInQualityProfileBadge className="spacer-left" />}
+ <ContentCell>
+ <div className="sw-flex sw-items-center" style={{ paddingLeft: offset }}>
+ <ProfileLink language={profile.language} name={profile.name}>
+ {profile.name}
+ </ProfileLink>
+ {profile.isBuiltIn && <BuiltInQualityProfileBadge className="sw-ml-2" />}
</div>
- </td>
+ </ContentCell>
- <td className="quality-profiles-table-projects thin nowrap text-middle text-right">
+ <ContentCell>
{profile.isDefault ? (
- <Tooltip overlay={translate('quality_profiles.list.default.help')}>
- <span className="badge">{translate('default')}</span>
+ <Tooltip overlay={intl.formatMessage({ id: 'quality_profiles.list.default.help' })}>
+ <Badge>{intl.formatMessage({ id: 'default' })}</Badge>
</Tooltip>
) : (
- <span>{profile.projectCount}</span>
+ <Note>{profile.projectCount}</Note>
)}
- </td>
+ </ContentCell>
- <td className="quality-profiles-table-rules thin nowrap text-middle text-right">
+ <ContentCell>
<div>
+ <Link to={activeRulesUrl}>{profile.activeRuleCount}</Link>
+
{profile.activeDeprecatedRuleCount > 0 && (
- <span className="spacer-right">
- <Tooltip overlay={translate('quality_profiles.deprecated_rules')}>
- <Link className="badge badge-error" to={deprecatedRulesUrl}>
- {profile.activeDeprecatedRuleCount}
- </Link>
+ <span className="sw-ml-2">
+ <Tooltip overlay={intl.formatMessage({ id: 'quality_profiles.deprecated_rules' })}>
+ <BaseLink to={deprecatedRulesUrl} className="sw-border-0">
+ <Badge variant="deleted">{profile.activeDeprecatedRuleCount}</Badge>
+ </BaseLink>
</Tooltip>
</span>
)}
-
- <Link to={activeRulesUrl}>{profile.activeRuleCount}</Link>
</div>
- </td>
+ </ContentCell>
- <td className="quality-profiles-table-date thin nowrap text-middle text-right">
- <DateFromNow date={profile.rulesUpdatedAt} />
- </td>
+ <ContentCell>
+ <Note>
+ <DateFromNow date={profile.rulesUpdatedAt} />
+ </Note>
+ </ContentCell>
- <td className="quality-profiles-table-date thin nowrap text-middle text-right">
- <DateFromNow date={profile.lastUsed} />
- </td>
+ <ContentCell>
+ <Note>
+ <DateFromNow date={profile.lastUsed} />
+ </Note>
+ </ContentCell>
- <td className="quality-profiles-table-actions thin nowrap text-middle text-right">
+ <ActionCell>
<ProfileActions
isComparable={isComparable}
profile={profile}
updateProfiles={props.updateProfiles}
/>
- </td>
- </tr>
+ </ActionCell>
+ </TableRow>
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-.quality-profiles-table {
- padding-top: 7px;
-}
-
-.quality-profiles-table-inheritance {
- width: 280px;
-}
-
-.quality-profiles-table-projects,
-.quality-profiles-table-rules,
-.quality-profiles-table-date {
- min-width: 80px;
-}
-
-.quality-profiles-list-header {
- line-height: var(--controlHeight);
- margin-bottom: 20px;
- padding: 5px 10px;
- border-bottom: 1px solid var(--barBorderColor);
-}
-
-.quality-profile-grid {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
-}
-
-.quality-profile-grid-left {
- width: 340px;
- flex-shrink: 0;
-}
-
-.quality-profile-grid-right {
- flex-grow: 1;
- margin-left: 20px;
-}
-
-.quality-profile-rules-distribution {
- margin-bottom: 15px;
- padding: 7px 20px 0;
-}
-
-.quality-profile-rules-deprecated {
- margin-top: 20px;
- padding: 15px 20px;
- background-color: var(--alertBackgroundError);
-}
-
-.quality-profile-not-found {
- padding-top: 100px;
- text-align: center;
-}
-
-.quality-profiles-evolution {
- padding-top: 55px;
-}
-
-.quality-profiles-evolution-deprecated {
- border-color: var(--alertBorderError);
- background-color: var(--alertBackgroundError);
-}
-
-.quality-profiles-evolution-stagnant {
- border-color: var(--alertBorderWarning);
- background-color: var(--alertBackgroundWarning);
-}
-
-.quality-profiles-evolution-deprecated h2,
-.quality-profiles-evolution-stagnant h2,
-.quality-profiles-evolution-rules h2 {
- padding: 0;
-}
.quality-profile-changelog-rule-cell {
line-height: 1.5;
word-break: break-word;
}
-.quality-profile-compare-right-table:not(.has-first-column) td,
-.quality-profile-compare-right-table:not(.has-first-column) th {
- /* Aligns the first column with the second one (50%) and add usual cell padding */
- padding-left: calc(50% + 10px);
-}
-
#create-profile-form .radio-card {
width: 244px;
background-color: var(--neutral50);
useLocation as useLocationRouter,
useNavigate,
useParams,
- useSearchParams,
} from 'react-router-dom';
import { queryToSearch, searchParamsToQuery } from '../../helpers/urls';
import { RawQuery } from '../../types/types';
WrappedComponent: React.ComponentType<P>,
): React.ComponentType<Omit<P, keyof WithRouterProps>> {
function ComponentWithRouterProp(props: P) {
- const locationRouter = useLocationRouter();
- const navigate = useNavigate();
+ const router = useRouter();
const params = useParams();
- const [searchParams] = useSearchParams();
-
- const router = React.useMemo(
- () => ({
- replace: (path: string | Partial<Location>) => {
- if ((path as Location).query) {
- path.search = queryToSearch((path as Location).query);
- }
- navigate(path, { replace: true });
- },
- push: (path: string | Partial<Location>) => {
- if ((path as Location).query) {
- path.search = queryToSearch((path as Location).query);
- }
- navigate(path);
- },
- }),
- [navigate],
- );
-
- const location = {
- ...locationRouter,
- query: searchParamsToQuery(searchParams),
- };
+ const location = useLocation();
return <WrappedComponent {...props} location={location} params={params} router={router} />;
}
return ComponentWithRouterProp;
}
+export function useRouter() {
+ const navigate = useNavigate();
+
+ const router = React.useMemo(
+ () => ({
+ replace: (path: string | Partial<Location>) => {
+ if ((path as Location).query) {
+ path.search = queryToSearch((path as Location).query);
+ }
+ navigate(path, { replace: true });
+ },
+ push: (path: string | Partial<Location>) => {
+ if ((path as Location).query) {
+ path.search = queryToSearch((path as Location).query);
+ }
+ navigate(path);
+ },
+ }),
+ [navigate],
+ );
+
+ return router;
+}
+
export function useLocation() {
const location = useLocationRouter();
quality_profiles.page_title_compare_x={0} Comparison
quality_profiles.new_profile=New Quality Profile
quality_profiles.compare_with=Compare with
-quality_profiles.filter_by=Filter profiles by
+quality_profiles.filter_by=Filter by
+quality_profiles.select_lang=Select language
quality_profiles.restore_profile=Restore Profile
quality_profiles.restore_profile.success={1} rule(s) restored in profile "{0}"
quality_profiles.restore_profile.warning={1} rule(s) restored, {2} rule(s) ignored in profile "{0}"
quality_profiles.changelog.parameter_reset_to_default_value=Parameter {0} reset to default value
quality_profiles.deleted_profile=The profile {0} doesn't exist anymore
quality_profiles.projects_for_default=Every project not specifically associated with a quality profile will be associated to this one by default.
-quality_profile.x_rules={0} rule(s)
-quality_profile.lang_deprecated_x_rules={0}, {1} deprecated rule(s)
+quality_profile.x_rules={count} rule(s)
+quality_profile.lang_deprecated_x_rules={name}, {count} deprecated rule(s)
quality_profile.x_active_rules={0} active rules
quality_profiles.x_overridden_rules={0} overridden rules
quality_profiles.change_parent=Change Parent
quality_profiles.change_parent_warning=By changing the parent of this profile, any information on inherited rules that were manually disabled will be lost. This means some previously disabled rules might be re-enabled.
quality_profiles.all_profiles=All Profiles
-quality_profiles.x_profiles={0} profile(s)
+quality_profiles.x_profiles={name}, {count} profile(s)
quality_profiles.projects.select_hint=Click to associate this project with the quality profile
quality_profiles.projects.deselect_hint=Click to remove association between this project and the quality profile
quality_profile.empty_comparison=The quality profiles are equal.
quality_profiles.activate_more.help.built_in=This quality profile is built in, and cannot be updated manually. If you want to activate more rules, create a new profile that inherits from this one and add rules there.
quality_profiles.activate_more_rules=Activate More Rules
quality_profiles.comparison.activate_rule=Activate rule for profile "{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.intro=Quality profiles are collections of rules to apply during an analysis. 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 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
quality_profiles.list.default.help=For each language there is a default profile. All projects not explicitly assigned to some other profile will be analyzed with the default.
-quality_profiles.x_updated_on_y={0}, updated on {1}
+quality_profiles.x_updated_on_y={name}, updated on {date}
quality_profiles.change_projects=Change Projects
quality_profiles.not_found=The requested quality profile was not found.
+quality_profiles.back_to_list=Go back to the list of Quality Profiles
quality_profiles.latest_new_rules=Recently Added Rules
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.latest_new_rules.see_all_x=See all {count} recently added rules
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.deprecated_rules_are_still_activated=Deprecated rules are still activated on {count} 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
coding_rules.filters.inheritance.inactive=Inheritance criterion is available when an inherited Quality Profile is selected
coding_rules.filters.inheritance.none=Not Inherited
coding_rules.filters.inheritance.inherited=Inherited
-coding_rules.filters.inheritance.x_inherited_from_y={0} inherited from "{1}"
+coding_rules.filters.inheritance.x_inherited_from_y={count} inherited from "{name}"
coding_rules.filters.inheritance.overrides=Overridden
coding_rules.filters.key=Key
coding_rules.filters.language=Language