]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-23191 Filter updates based on MQR or STANDARD modes
authorIsmail Cherri <ismail.cherri@sonarsource.com>
Thu, 17 Oct 2024 11:54:24 +0000 (13:54 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 18 Oct 2024 20:03:11 +0000 (20:03 +0000)
server/sonar-web/src/main/js/api/mocks/QualityProfilesServiceMock.ts
server/sonar-web/src/main/js/api/quality-profiles.ts
server/sonar-web/src/main/js/apps/quality-profiles/changelog/Changelog.tsx
server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.tsx
server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangesList.tsx
server/sonar-web/src/main/js/apps/quality-profiles/changelog/SeverityChange.tsx
server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ChangelogContainer-it.tsx
server/sonar-web/src/main/js/queries/quality-profiles.ts
server/sonar-web/src/main/js/types/quality-profiles.ts [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index e348ef7dd2ab00bc74084ceb56f93fc192c1250a..f1b3ef928c151d1008cd7a3e1f1fb43cc7f283e9 100644 (file)
@@ -37,20 +37,17 @@ import {
 } from '../../types/clean-code-taxonomy';
 import { SearchRulesResponse } from '../../types/coding-rules';
 import { IssueSeverity } from '../../types/issues';
+import { QualityProfileChangelogFilterMode } from '../../types/quality-profiles';
 import { SearchRulesQuery } from '../../types/rules';
 import { Dict, Paging, ProfileInheritanceDetails, RuleDetails } from '../../types/types';
 import {
-  CompareResponse,
-  Profile,
-  ProfileProject,
-  SearchQualityProfilesParameters,
-  SearchQualityProfilesResponse,
   activateRule,
   addGroup,
   addUser,
   associateProject,
   changeProfileParent,
   compareProfiles,
+  CompareResponse,
   copyProfile,
   createQualityProfile,
   deactivateRule,
@@ -63,12 +60,16 @@ import {
   getProfileProjects,
   getQualityProfile,
   getQualityProfileExporterUrl,
+  Profile,
+  ProfileProject,
   removeGroup,
   removeUser,
   renameProfile,
   restoreQualityProfile,
   searchGroups,
   searchQualityProfiles,
+  SearchQualityProfilesParameters,
+  SearchQualityProfilesResponse,
   searchUsers,
   setDefaultProfile,
 } from '../quality-profiles';
@@ -296,6 +297,37 @@ export default class QualityProfilesServiceMock {
           credentialWords: 'foo,bar',
         },
       }),
+      mockQualityProfileChangelogEvent({
+        date: '2019-02-23T03:12:32+0100',
+        action: 'UPDATED',
+        ruleKey: 'c:rule4',
+        ruleName: 'Rule 5',
+        params: {
+          newCleanCodeAttribute: CleanCodeAttribute.Complete,
+          newCleanCodeAttributeCategory: CleanCodeAttributeCategory.Intentional,
+          oldCleanCodeAttribute: CleanCodeAttribute.Lawful,
+          oldCleanCodeAttributeCategory: CleanCodeAttributeCategory.Responsible,
+          impactChanges: [
+            {
+              newSeverity: SoftwareImpactSeverity.Medium,
+              newSoftwareQuality: SoftwareQuality.Reliability,
+            },
+            {
+              oldSeverity: SoftwareImpactSeverity.High,
+              oldSoftwareQuality: SoftwareQuality.Maintainability,
+            },
+          ],
+        },
+      }),
+      mockQualityProfileChangelogEvent({
+        date: '2019-01-23T03:12:32+0100',
+        action: 'UPDATED',
+        ruleKey: 'c:rule6',
+        ruleName: 'Rule 6',
+        params: {
+          severity: IssueSeverity.Critical,
+        },
+      }),
     ];
   }
 
@@ -682,9 +714,16 @@ export default class QualityProfilesServiceMock {
     return this.reply({});
   };
 
-  handleGetProfileChangelog: typeof getProfileChangelog = (since, to, { language }, page) => {
+  handleGetProfileChangelog: typeof getProfileChangelog = (data) => {
+    const {
+      profile: { language },
+      since,
+      to,
+      page,
+      filterMode = QualityProfileChangelogFilterMode.MQR,
+    } = data;
     const PAGE_SIZE = 50;
-    const p = page || 1;
+    const p = page ?? 1;
     const events = this.changelogEvents.filter((event) => {
       if (event.ruleKey.split(':')[0] !== language) {
         return false;
@@ -695,6 +734,23 @@ export default class QualityProfilesServiceMock {
       if (to && new Date(to) <= new Date(event.date)) {
         return false;
       }
+      if (
+        filterMode === QualityProfileChangelogFilterMode.MQR &&
+        event.action === 'UPDATED' &&
+        event.params &&
+        !Object.keys(event.params).includes('impactChanges')
+      ) {
+        return false;
+      }
+
+      if (
+        filterMode === QualityProfileChangelogFilterMode.STANDARD &&
+        event.action === 'UPDATED' &&
+        !event.params?.severity
+      ) {
+        return false;
+      }
+
       return true;
     });
 
index 21a61af4e4f6f6362c1b0e6c6cdf5703e9769037..2c293b4bb840b6bdfe1f13b73606f20cf4a495f3 100644 (file)
@@ -29,6 +29,7 @@ import {
   SoftwareImpactSeverity,
   SoftwareQuality,
 } from '../types/clean-code-taxonomy';
+import { QualityProfileChangelogFilterMode } from '../types/quality-profiles';
 import { Dict, Paging, ProfileInheritanceDetails, UserSelected } from '../types/types';
 
 export interface ProfileActions {
@@ -119,7 +120,7 @@ export function getProfileInheritance({
 }: Pick<Profile, 'language' | 'name'>): Promise<{
   ancestors: ProfileInheritanceDetails[];
   children: ProfileInheritanceDetails[];
-  profile: ProfileInheritanceDetails;
+  profile: ProfileInheritanceDetails | null;
 }> {
   return getJSON('/api/qualityprofiles/inheritance', {
     language,
@@ -182,17 +183,28 @@ export interface ChangelogResponse {
   paging: Paging;
 }
 
-export function getProfileChangelog(
-  since: any,
-  to: any,
-  { language, name: qualityProfile }: Profile,
-  page?: number,
-): Promise<ChangelogResponse> {
+interface ChangelogData {
+  filterMode: QualityProfileChangelogFilterMode;
+  page?: number;
+  profile: Profile;
+  since: string;
+  to: string;
+}
+
+export function getProfileChangelog(data: ChangelogData): Promise<ChangelogResponse> {
+  const {
+    filterMode,
+    page,
+    profile: { language, name: qualityProfile },
+    since,
+    to,
+  } = data;
   return getJSON('/api/qualityprofiles/changelog', {
     since,
     to,
     language,
     qualityProfile,
+    filterMode,
     p: page,
   });
 }
index 65257c141d90dfb61e4521bd25fc230a280d28a7..f8d8c88382218849494d28f61ba1aaab289ebfe6 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { LinkStandalone } from '@sonarsource/echoes-react';
 import classNames from 'classnames';
 import { isSameMinute } from 'date-fns';
 import {
   CellComponent,
   ContentCell,
   FlagMessage,
-  Link,
   Note,
   Table,
   TableRow,
@@ -34,6 +34,7 @@ import * as React from 'react';
 import { FormattedMessage, useIntl } from 'react-intl';
 import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
 import { parseDate } from '../../../helpers/dates';
+import { isDefined } from '../../../helpers/types';
 import { getRulesUrl } from '../../../helpers/urls';
 import { ProfileChangelogEvent } from '../types';
 import ChangesList from './ChangesList';
@@ -42,7 +43,7 @@ interface Props {
   events: ProfileChangelogEvent[];
 }
 
-export default function Changelog(props: Props) {
+export default function Changelog(props: Readonly<Props>) {
   const intl = useIntl();
 
   const sortedRows = sortBy(
@@ -126,8 +127,10 @@ export default function Changelog(props: Props) {
         <CellComponent
           className={classNames('sw-align-top', { 'sw-border-transparent': !shouldDisplayDate })}
         >
-          {event.ruleName && (
-            <Link to={getRulesUrl({ rule_key: event.ruleKey })}>{event.ruleName}</Link>
+          {isDefined(event.ruleName) && (
+            <LinkStandalone to={getRulesUrl({ rule_key: event.ruleKey })}>
+              {event.ruleName}
+            </LinkStandalone>
           )}
         </CellComponent>
 
index 0f37e03c4ac597dd5be441a087940ab0b81c1bd5..e795ab941e2e7654547122a019ba281aeafb6f40 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { Button } from '@sonarsource/echoes-react';
-import { Spinner } from 'design-system';
+import { Button, Spinner } from '@sonarsource/echoes-react';
 import * as React from 'react';
-import { withRouter } from '~sonar-aligned/components/hoc/withRouter';
-import { Location, Router } from '~sonar-aligned/types/router';
-import { ChangelogResponse, getProfileChangelog } from '../../../api/quality-profiles';
+import { useLocation, useRouter } from '~sonar-aligned/components/hoc/withRouter';
 import { parseDate, toISO8601WithOffsetString } from '../../../helpers/dates';
 import { translate } from '../../../helpers/l10n';
+import { isDefined } from '../../../helpers/types';
+import { useGetQualityProfileChangelog } from '../../../queries/quality-profiles';
+import { useStandardExperienceMode } from '../../../queries/settings';
+import { QualityProfileChangelogFilterMode } from '../../../types/quality-profiles';
 import { withQualityProfilesContext } from '../qualityProfilesContext';
-import { Profile, ProfileChangelogEvent } from '../types';
+import { Profile } from '../types';
 import { getProfileChangelogPath } from '../utils';
 import Changelog from './Changelog';
 import ChangelogEmpty from './ChangelogEmpty';
 import ChangelogSearch from './ChangelogSearch';
 
 interface Props {
-  location: Location;
   profile: Profile;
-  router: Router;
 }
 
-interface State {
-  events?: ProfileChangelogEvent[];
-  loading: boolean;
-  page?: number;
-  total?: number;
-}
-
-class ChangelogContainer extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = { loading: true };
-
-  componentDidMount() {
-    this.mounted = true;
-    this.loadChangelog();
-  }
-
-  componentDidUpdate(prevProps: Props) {
-    if (prevProps.location !== this.props.location) {
-      this.loadChangelog();
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  stopLoading() {
-    if (this.mounted) {
-      this.setState({ loading: false });
-    }
-  }
-
-  loadChangelog() {
-    this.setState({ loading: true });
-    const {
-      location: { query },
-      profile,
-    } = this.props;
-
-    getProfileChangelog(query.since, query.to, profile)
-      .then((r: ChangelogResponse) => {
-        if (this.mounted) {
-          this.setState({
-            events: r.events,
-            total: r.paging.total,
-            page: r.paging.pageIndex,
-            loading: false,
-          });
-        }
-      })
-      .catch(this.stopLoading);
-  }
-
-  loadMore(event: React.SyntheticEvent<HTMLElement>) {
-    event.preventDefault();
-    event.currentTarget.blur();
-
-    if (this.state.page != null) {
-      this.setState({ loading: true });
-      const {
-        location: { query },
-        profile,
-      } = this.props;
-
-      getProfileChangelog(query.since, query.to, profile, this.state.page + 1)
-        .then((r: ChangelogResponse) => {
-          if (this.mounted && this.state.events) {
-            this.setState(({ events = [] }) => ({
-              events: [...events, ...r.events],
-              total: r.paging.total,
-              page: r.paging.pageIndex,
-              loading: false,
-            }));
-          }
-        })
-        .catch(this.stopLoading);
-    }
-  }
-
-  handleDateRangeChange = ({ from, to }: { from?: Date; to?: Date }) => {
-    const path = getProfileChangelogPath(this.props.profile.name, this.props.profile.language, {
+function ChangelogContainer(props: Readonly<Props>) {
+  const { profile } = props;
+  const { data: isStandardMode } = useStandardExperienceMode();
+  const router = useRouter();
+  const {
+    query: { since, to },
+  } = useLocation();
+
+  const filterMode = isStandardMode
+    ? QualityProfileChangelogFilterMode.STANDARD
+    : QualityProfileChangelogFilterMode.MQR;
+
+  const {
+    data: changeLogResponse,
+    isLoading,
+    fetchNextPage,
+  } = useGetQualityProfileChangelog({
+    since,
+    to,
+    profile,
+    filterMode,
+  });
+
+  const events = changeLogResponse?.pages.flatMap((page) => page.events) ?? [];
+  const total = changeLogResponse?.pages[0].paging.total;
+
+  const handleDateRangeChange = ({ from, to }: { from?: Date; to?: Date }) => {
+    const path = getProfileChangelogPath(profile.name, profile.language, {
       since: from && toISO8601WithOffsetString(from),
       to: to && toISO8601WithOffsetString(to),
     });
-    this.props.router.push(path);
+    router.push(path);
   };
 
-  handleReset = () => {
-    const path = getProfileChangelogPath(this.props.profile.name, this.props.profile.language);
-    this.props.router.push(path);
+  const handleReset = () => {
+    const path = getProfileChangelogPath(profile.name, profile.language);
+    router.replace(path);
   };
 
-  render() {
-    const { query } = this.props.location;
-
-    const shouldDisplayFooter =
-      this.state.events != null &&
-      this.state.total != null &&
-      this.state.events.length < this.state.total;
-
-    return (
-      <div className="sw-mt-4">
-        <div className="sw-mb-2 sw-flex sw-gap-4 sw-items-center">
-          <ChangelogSearch
-            dateRange={{
-              from: query.since ? parseDate(query.since) : undefined,
-              to: query.to ? parseDate(query.to) : undefined,
-            }}
-            onDateRangeChange={this.handleDateRangeChange}
-            onReset={this.handleReset}
-          />
-          <Spinner loading={this.state.loading} />
-        </div>
+  const shouldDisplayFooter = isDefined(events) && isDefined(total) && events.length < total;
+
+  return (
+    <div className="sw-mt-4">
+      <div className="sw-mb-2 sw-flex sw-gap-4 sw-items-center">
+        <ChangelogSearch
+          dateRange={{
+            from: since ? parseDate(since) : undefined,
+            to: to ? parseDate(to) : undefined,
+          }}
+          onDateRangeChange={handleDateRangeChange}
+          onReset={handleReset}
+        />
+        <Spinner isLoading={isLoading} />
+      </div>
 
-        {this.state.events != null && this.state.events.length === 0 && <ChangelogEmpty />}
+      {isDefined(events) && events.length === 0 && <ChangelogEmpty />}
 
-        {this.state.events != null && this.state.events.length > 0 && (
-          <Changelog events={this.state.events} />
-        )}
+      {isDefined(events) && events.length > 0 && <Changelog events={events} />}
 
-        {shouldDisplayFooter && (
-          <footer className="sw-text-center sw-mt-2">
-            <Button onClick={this.loadMore.bind(this)}>{translate('show_more')}</Button>
-          </footer>
-        )}
-      </div>
-    );
-  }
+      {shouldDisplayFooter && (
+        <footer className="sw-text-center sw-mt-2">
+          <Button onClick={() => fetchNextPage()}>{translate('show_more')}</Button>
+        </footer>
+      )}
+    </div>
+  );
 }
 
-export default withQualityProfilesContext(withRouter(ChangelogContainer));
+export default withQualityProfilesContext(ChangelogContainer);
index dc01693ab87541526c8e713396d5c86977ddf423..1cf60b39fb712c9b67b58965abcf09afa739230c 100644 (file)
@@ -18,6 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
+import { useStandardExperienceMode } from '../../../queries/settings';
 import { ProfileChangelogEvent } from '../types';
 import CleanCodeAttributeChange from './CleanCodeAttributeChange';
 import ParameterChange from './ParameterChange';
@@ -28,7 +29,7 @@ interface Props {
   changes: ProfileChangelogEvent['params'];
 }
 
-export default function ChangesList({ changes }: Props) {
+export default function ChangesList({ changes }: Readonly<Props>) {
   const {
     severity,
     oldCleanCodeAttribute,
@@ -39,15 +40,18 @@ export default function ChangesList({ changes }: Props) {
     ...rest
   } = changes ?? {};
 
+  const { data: isStandardMode } = useStandardExperienceMode();
+
   return (
     <ul className="sw-w-full sw-flex sw-flex-col sw-gap-1">
-      {severity && (
+      {severity && isStandardMode && (
         <li>
           <SeverityChange severity={severity} />
         </li>
       )}
 
-      {oldCleanCodeAttribute &&
+      {!isStandardMode &&
+        oldCleanCodeAttribute &&
         oldCleanCodeAttributeCategory &&
         newCleanCodeAttribute &&
         newCleanCodeAttributeCategory && (
@@ -61,11 +65,12 @@ export default function ChangesList({ changes }: Props) {
           </li>
         )}
 
-      {impactChanges?.map((impactChange, index) => (
-        <li key={index}>
-          <SoftwareImpactChange impactChange={impactChange} />
-        </li>
-      ))}
+      {!isStandardMode &&
+        impactChanges?.map((impactChange, index) => (
+          <li key={index}>
+            <SoftwareImpactChange impactChange={impactChange} />
+          </li>
+        ))}
 
       {Object.keys(rest).map((key) => (
         <li key={key}>
index 7912c9a159a7c9f6ee80ad63628833d9da7d11cb..95cd310a8df82c322430e07558d0f3cb60162203 100644 (file)
@@ -25,11 +25,10 @@ interface Props {
   severity: string;
 }
 
-export default function SeverityChange({ severity }: Props) {
+export default function SeverityChange({ severity }: Readonly<Props>) {
   return (
     <div className="sw-whitespace-nowrap">
-      {translate('quality_profiles.deprecated_severity_set_to')}{' '}
-      <SeverityHelper severity={severity} />
+      {translate('quality_profiles.severity_set_to')} <SeverityHelper severity={severity} />
     </div>
   );
 }
index 66b303fb58fa3c98a25e822c02e6819dded57e87..8f2c7d2f09a789c44de6fae6c374e1206e7c3230 100644 (file)
@@ -24,6 +24,7 @@ import QualityProfilesServiceMock from '../../../../api/mocks/QualityProfilesSer
 import SettingsServiceMock from '../../../../api/mocks/SettingsServiceMock';
 import { mockQualityProfileChangelogEvent } from '../../../../helpers/testMocks';
 import { renderAppRoutes } from '../../../../helpers/testReactTestingUtils';
+import { SettingsKey } from '../../../../types/settings';
 import routes from '../../routes';
 
 jest.mock('../../../../api/quality-profiles');
@@ -80,20 +81,24 @@ afterEach(() => {
   settingsMock.reset();
 });
 
-it('should see the changelog', async () => {
+it('should see the changelog in MQR', async () => {
   const user = userEvent.setup();
 
   renderChangeLog();
 
   const rows = await ui.row.findAll();
-  expect(rows).toHaveLength(6);
+  expect(rows).toHaveLength(7);
   expect(ui.emptyPage.query()).not.toBeInTheDocument();
   ui.checkRow(1, 'May 23, 2019', 'System', 'quality_profiles.changelog.ACTIVATED', 'Rule 0');
   ui.checkRow(2, 'April 23, 2019', 'System', 'quality_profiles.changelog.DEACTIVATED', 'Rule 0', [
-    /quality_profiles.deprecated_severity_set_to severity.MAJOR/,
+    /^$/, // Should be empty
   ]);
   ui.checkRow(3, '', '', '', 'Rule 1', [
-    /quality_profiles.deprecated_severity_set_to severity.CRITICAL/,
+    /quality_profiles.changelog.cca_and_category_changed.*COMPLETE.*INTENTIONAL.*LAWFUL.*RESPONSIBLE/,
+    /quality_profiles.changelog.impact_added.severity_impact.*MEDIUM.*RELIABILITY/,
+    /quality_profiles.changelog.impact_removed.severity_impact.HIGH.*MAINTAINABILITY/,
+  ]);
+  ui.checkRow(6, 'February 23, 2019', 'System', 'quality_profiles.changelog.UPDATED', 'Rule 5', [
     /quality_profiles.changelog.cca_and_category_changed.*COMPLETE.*INTENTIONAL.*LAWFUL.*RESPONSIBLE/,
     /quality_profiles.changelog.impact_added.severity_impact.*MEDIUM.*RELIABILITY/,
     /quality_profiles.changelog.impact_removed.severity_impact.HIGH.*MAINTAINABILITY/,
@@ -102,11 +107,28 @@ it('should see the changelog', async () => {
   expect(screen.getByText('/coding_rules?rule_key=c%3Arule0')).toBeInTheDocument();
 });
 
+it('should return standard mode changelogs only', async () => {
+  settingsMock.set(SettingsKey.MQRMode, 'false');
+  renderChangeLog();
+
+  const rows = await ui.row.findAll();
+  expect(rows).toHaveLength(7);
+  ui.checkRow(1, 'May 23, 2019', 'System', 'quality_profiles.changelog.ACTIVATED', 'Rule 0', [
+    /^$/,
+  ]);
+  ui.checkRow(2, 'April 23, 2019', 'System', 'quality_profiles.changelog.DEACTIVATED', 'Rule 0', [
+    /quality_profiles.severity_set_to severity.MAJOR/,
+  ]);
+  ui.checkRow(6, 'January 23, 2019', 'System', 'quality_profiles.changelog.UPDATED', 'Rule 6', [
+    /quality_profiles.severity_set_to severity.CRITICAL/,
+  ]);
+});
+
 it('should filter the changelog', async () => {
   const user = userEvent.setup();
   renderChangeLog();
 
-  expect(await ui.row.findAll()).toHaveLength(6);
+  expect(await ui.row.findAll()).toHaveLength(7);
   await user.click(ui.startDate.get());
   await user.click(screen.getByRole('gridcell', { name: '20' }));
   await user.click(document.body);
@@ -116,7 +138,7 @@ it('should filter the changelog', async () => {
   await user.click(document.body);
   expect(await ui.row.findAll()).toHaveLength(4);
   await user.click(ui.reset.get());
-  expect(await ui.row.findAll()).toHaveLength(6);
+  expect(await ui.row.findAll()).toHaveLength(7);
 });
 
 it('should load more', async () => {
@@ -134,7 +156,7 @@ it('should load more', async () => {
   await user.click(ui.showMore.get());
   expect(await ui.row.findAll()).toHaveLength(101);
   await user.click(ui.reset.get());
-  expect(await ui.row.findAll()).toHaveLength(51);
+  expect(await ui.row.findAll()).toHaveLength(101); // Reset should not reset the page
 });
 
 it('should see short changelog for php', async () => {
@@ -143,7 +165,7 @@ it('should see short changelog for php', async () => {
   const rows = await ui.row.findAll();
   expect(rows).toHaveLength(2);
   ui.checkRow(1, 'May 23, 2019', 'System', 'quality_profiles.changelog.DEACTIVATED', 'PHP Rule', [
-    /quality_profiles.deprecated_severity_set_to severity.CRITICAL/,
+    /^((?!severity.CRITICAL).)*$/, // Should not contain severity.CRITICAL
     /quality_profiles.changelog.cca_and_category_changed.*COMPLETE.*INTENTIONAL.*CLEAR.*RESPONSIBLE/,
   ]);
   expect(ui.showMore.query()).not.toBeInTheDocument();
index 572a54a375853c1250a6a7e9f78b59aa2ed91fbf..246b11378949565803a9f8b50fd193f05dd4e6c8 100644 (file)
@@ -18,7 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import {
-  UseQueryResult,
+  infiniteQueryOptions,
   queryOptions,
   useMutation,
   useQuery,
@@ -35,37 +35,64 @@ import {
   addUser,
   compareProfiles,
   deactivateRule,
+  getProfileChangelog,
   getProfileInheritance,
   getQualityProfile,
 } from '../api/quality-profiles';
-import { ProfileInheritanceDetails } from '../types/types';
-import { createQueryHook } from './common';
+import { getNextPageParam, getPreviousPageParam } from '../helpers/react-query';
+import { isDefined } from '../helpers/types';
+import { QualityProfileChangelogFilterMode } from '../types/quality-profiles';
+import { createInfiniteQueryHook, createQueryHook } from './common';
 
-export function useProfileInheritanceQuery(
-  profile?: Pick<Profile, 'language' | 'name' | 'parentKey'>,
-): UseQueryResult<{
-  ancestors: ProfileInheritanceDetails[];
-  children: ProfileInheritanceDetails[];
-  profile: ProfileInheritanceDetails | null;
-}> {
-  const { language, name, parentKey } = profile ?? {};
-  return useQuery({
-    queryKey: ['quality-profiles', 'inheritance', language, name, parentKey],
-    queryFn: async ({ queryKey: [, , language, name] }) => {
-      if (!language || !name) {
-        return { ancestors: [], children: [], profile: null };
-      }
-      const response = await getProfileInheritance({ language, name });
-      response.ancestors.reverse();
-      return response;
-    },
-  });
-}
+const qualityProfileQueryKeys = {
+  all: () => ['quality-profiles'],
+  inheritance: (language?: string, name?: string, parentKey?: string) => [
+    ...qualityProfileQueryKeys.all(),
+    'inheritance',
+    language,
+    name,
+    parentKey,
+  ],
+  profile: (profile: Profile, compareToSonarWay = false) => [
+    ...qualityProfileQueryKeys.all(),
+    'details',
+    profile,
+    compareToSonarWay,
+  ],
+  changelog: (
+    language: string,
+    name: string,
+    since: string,
+    to: string,
+    filterMode: QualityProfileChangelogFilterMode,
+  ) => [...qualityProfileQueryKeys.all(), 'changelog', language, name, since, to, filterMode],
+  compare: (leftKey: string, rightKey: string) => [
+    ...qualityProfileQueryKeys.all(),
+    'compare',
+    leftKey,
+    rightKey,
+  ],
+};
+
+export const useProfileInheritanceQuery = createQueryHook(
+  (profile?: Pick<Profile, 'language' | 'name' | 'parentKey'>) => {
+    const { language, name, parentKey } = profile ?? {};
+    return queryOptions({
+      queryKey: qualityProfileQueryKeys.inheritance(language, name, parentKey),
+      queryFn: () => {
+        if (!isDefined(language) || !isDefined(name)) {
+          return { ancestors: [], children: [], profile: null };
+        }
+        return getProfileInheritance({ language, name });
+      },
+    });
+  },
+);
 
 export const useGetQualityProfile = createQueryHook(
   (data: Parameters<typeof getQualityProfile>[0]) => {
     return queryOptions({
-      queryKey: ['quality-profile', 'details', data.profile, data.compareToSonarWay],
+      queryKey: qualityProfileQueryKeys.profile(data.profile, data.compareToSonarWay),
       queryFn: () => {
         return getQualityProfile(data);
       },
@@ -73,11 +100,31 @@ export const useGetQualityProfile = createQueryHook(
   },
 );
 
+export const useGetQualityProfileChangelog = createInfiniteQueryHook(
+  (data: Parameters<typeof getProfileChangelog>[0]) => {
+    return infiniteQueryOptions({
+      queryKey: qualityProfileQueryKeys.changelog(
+        data.profile.language,
+        data.profile.name,
+        data.since,
+        data.to,
+        data.filterMode,
+      ),
+      queryFn: ({ pageParam }) => {
+        return getProfileChangelog({ ...data, page: pageParam });
+      },
+      getNextPageParam: (data) => getNextPageParam({ page: data.paging }),
+      getPreviousPageParam: (data) => getPreviousPageParam({ page: data.paging }),
+      initialPageParam: 1,
+    });
+  },
+);
+
 export function useProfilesCompareQuery(leftKey: string, rightKey: string) {
   return useQuery({
-    queryKey: ['quality-profiles', 'compare', leftKey, rightKey],
-    queryFn: ({ queryKey: [, leftKey, rightKey] }) => {
-      if (!leftKey || !rightKey) {
+    queryKey: qualityProfileQueryKeys.compare(leftKey, rightKey),
+    queryFn: ({ queryKey: [_1, _2, leftKey, rightKey] }) => {
+      if (!isDefined(leftKey) || !isDefined(rightKey)) {
         return null;
       }
 
diff --git a/server/sonar-web/src/main/js/types/quality-profiles.ts b/server/sonar-web/src/main/js/types/quality-profiles.ts
new file mode 100644 (file)
index 0000000..ec74022
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+export enum QualityProfileChangelogFilterMode {
+  MQR = 'MQR',
+  STANDARD = 'STANDARD',
+}
index 69a8d79f47eb1b494ab4ef3b90731fd67df964ff..0d8dc819ad4184bed29dcd8ee65c470c39f9fa5d 100644 (file)
@@ -2261,7 +2261,7 @@ quality_profiles.copy_x_title=Copy Profile "{0}" - {1}
 quality_profiles.extend_x_title=Extend Profile "{0}" - {1}
 quality_profiles.rename_x_title=Rename Profile {0} - {1}
 quality_profiles.deprecated=deprecated
-quality_profiles.deprecated_severity_set_to=Old severity set to
+quality_profiles.severity_set_to=Severity set to
 quality_profiles.changelog.ACTIVATED=Activated
 quality_profiles.changelog.DEACTIVATED=Deactivated
 quality_profiles.changelog.UPDATED=Updated