]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-23300 Show filters from other mode if they are needed
authorViktor Vorona <viktor.vorona@sonarsource.com>
Thu, 24 Oct 2024 08:23:47 +0000 (10:23 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 28 Oct 2024 20:02:55 +0000 (20:02 +0000)
16 files changed:
server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-Filtering-it.tsx
server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx
server/sonar-web/src/main/js/apps/issues/__tests__/IssuesSourceViewer-it.tsx
server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/SimpleListStyleFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx
server/sonar-web/src/main/js/apps/issues/test-utils.tsx
server/sonar-web/src/main/js/components/facets/Facet.tsx
server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx
server/sonar-web/src/main/js/components/issue/components/IssueMetaBar.tsx
server/sonar-web/src/main/js/components/issue/components/IssueSeverity.tsx
server/sonar-web/src/main/js/components/issue/components/IssueType.tsx
server/sonar-web/src/main/js/design-system/components/FacetBox.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index ed965bf54015d6c732eb88ddb03b0e4d210a4a5b..0e99d15c437676cfb4d61d10ed28bcdad6db7343 100644 (file)
@@ -391,6 +391,7 @@ describe('issues app filtering', () => {
   it('should support OWASP Top 10 version 2021', async () => {
     const user = userEvent.setup();
     renderIssueApp();
+    await waitOnDataLoaded();
     await user.click(screen.getByRole('button', { name: 'issues.facet.standards' }));
     const owaspTop102021 = screen.getByRole('button', { name: 'issues.facet.owaspTop10_2021' });
     expect(owaspTop102021).toBeInTheDocument();
@@ -405,6 +406,26 @@ describe('issues app filtering', () => {
       }),
     );
   });
+
+  it('should close all filters if there is a filter from other mode', async () => {
+    let component = renderIssueApp();
+    await waitOnDataLoaded();
+    expect(screen.getAllByRole('button', { expanded: true })).toHaveLength(3);
+
+    component.unmount();
+
+    component = renderIssueApp(undefined, undefined, 'issues?types=CODE_SMELL');
+    await waitOnDataLoaded();
+    expect(screen.queryByRole('button', { expanded: true })).not.toBeInTheDocument();
+
+    component.unmount();
+
+    settingsHandler.set(SettingsKey.MQRMode, 'false');
+
+    renderIssueApp(undefined, undefined, 'issues?impactSeverities=BLOCKER');
+    await waitOnDataLoaded();
+    expect(screen.queryByRole('button', { expanded: true })).not.toBeInTheDocument();
+  });
 });
 
 describe('issues app when reindexing', () => {
index e2817522cac4ca4634238ec52687bc75a416198a..051bdfb50ea7104fb4817aff60223b2636262dad 100644 (file)
@@ -74,6 +74,18 @@ describe('issues app', () => {
 
       expect(await ui.fixedIssuesHeading.find()).toBeInTheDocument();
     });
+
+    it('should show issue type if old filter exists', async () => {
+      const component = renderProjectIssuesApp('project/issues?id=my-project');
+
+      expect(await ui.issueItem1.find()).not.toHaveTextContent('issue.type.VULNERABILITY');
+
+      component.unmount();
+
+      renderProjectIssuesApp('project/issues?id=my-project&types=VULNERABILITY');
+
+      expect(await ui.issueItem1.find()).toHaveTextContent('issue.type.VULNERABILITY');
+    });
   });
 
   describe('navigation', () => {
@@ -229,7 +241,7 @@ describe('issues app', () => {
       renderIssueApp(currentUser);
 
       // Check that the bulk button has correct behavior
-      expect(screen.getByRole('button', { name: 'bulk_change' })).toBeDisabled();
+      expect(await screen.findByRole('button', { name: 'bulk_change' })).toBeDisabled();
 
       // Select all issues
       await user.click(await screen.findByRole('checkbox', { name: 'issues.select_all_issues' }));
index 5f0747d8eea3a7970de485f2f2319c3478d6cf5a..744dc7bfee74ed781c039e3063c0d7a57f8a244c 100644 (file)
@@ -18,7 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
-import { screen, waitForElementToBeRemoved } from '@testing-library/react';
+import { screen } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { keyBy } from 'lodash';
 import { byLabelText, byRole } from '~sonar-aligned/helpers/testSelector';
@@ -103,7 +103,6 @@ describe('issues source viewer', () => {
   it('should show source across components', async () => {
     const user = userEvent.setup();
     renderProjectIssuesApp('project/issues?issues=issue101&open=issue101&id=myproject');
-    await waitOnDataLoaded();
 
     expect(await screen.findByLabelText('test1.js')).toBeInTheDocument();
     expect(screen.getByLabelText('test2.js')).toBeInTheDocument();
@@ -153,12 +152,12 @@ describe('issues source viewer', () => {
     renderProjectIssuesApp('project/issues?issues=issue1&open=issue1&id=myproject');
     await waitOnDataLoaded();
 
-    // Line 44 is between both snippets, it should not be shown
-    expect(ui.line44.query()).not.toBeInTheDocument();
-
     // There currently are two snippet shown
     expect(await screen.findAllByRole('table')).toHaveLength(2);
 
+    // Line 44 is between both snippets, it should not be shown
+    expect(ui.line44.query()).not.toBeInTheDocument();
+
     // Expand lines above second snippet
     await user.click(ui.expandLinesAbove.get());
 
@@ -175,10 +174,9 @@ describe('issues source viewer', () => {
       issuesHandler.setIssueList([JUPYTER_ISSUE]);
       sourcesHandler.setSource('{not a JSON file}');
       renderProjectIssuesApp('project/issues?issues=some-issue&open=some-issue&id=myproject');
-      await waitOnDataLoaded();
 
       // Preview tab should be shown
-      expect(ui.preview.get()).toBeChecked();
+      expect(await ui.preview.find()).toBeChecked();
       expect(ui.code.get()).toBeInTheDocument();
 
       expect(
@@ -204,10 +202,9 @@ describe('issues source viewer', () => {
         },
       ]);
       renderProjectIssuesApp('project/issues?issues=some-issue&open=some-issue&id=myproject');
-      await waitOnDataLoaded();
 
       // Preview tab should be shown
-      expect(ui.preview.get()).toBeChecked();
+      expect(await ui.preview.find()).toBeChecked();
       expect(ui.code.get()).toBeInTheDocument();
 
       expect(
@@ -220,17 +217,16 @@ describe('issues source viewer', () => {
     it('should show preview tab when jupyter notebook issue', async () => {
       issuesHandler.setIssueList([JUPYTER_ISSUE]);
       renderProjectIssuesApp('project/issues?issues=some-issue&open=some-issue&id=myproject');
-      await waitOnDataLoaded();
 
       // Preview tab should be shown
-      expect(ui.preview.get()).toBeChecked();
+      expect(await ui.preview.find()).toBeChecked();
       expect(ui.code.get()).toBeInTheDocument();
 
       expect(
         await screen.findByRole('button', { name: 'Issue on Jupyter Notebook' }),
       ).toBeInTheDocument();
 
-      await waitForElementToBeRemoved(screen.queryByText('issue.preview.jupyter_notebook.error'));
+      expect(screen.queryByText('issue.preview.jupyter_notebook.error')).not.toBeInTheDocument();
       expect(screen.getByTestId('hljs-sonar-underline')).toHaveTextContent('matplotlib');
       expect(screen.getByText(/pylab/, { exact: false })).toBeInTheDocument();
     });
@@ -249,9 +245,8 @@ describe('issues source viewer', () => {
       ]);
       const user = userEvent.setup();
       renderProjectIssuesApp('project/issues?issues=some-issue&open=some-issue&id=myproject');
-      await waitOnDataLoaded();
 
-      await user.click(ui.code.get());
+      await user.click(await ui.code.find());
 
       expect(screen.getAllByRole('button', { name: 'Issue on Jupyter Notebook' })).toHaveLength(2);
       expect(screen.queryByText('Another unrelated issue')).not.toBeInTheDocument();
@@ -273,17 +268,16 @@ describe('issues source viewer', () => {
         },
       ]);
       renderProjectIssuesApp('project/issues?issues=some-issue&open=some-issue&id=myproject');
-      await waitOnDataLoaded();
 
       // Preview tab should be shown
-      expect(ui.preview.get()).toBeChecked();
+      expect(await ui.preview.find()).toBeChecked();
       expect(ui.code.get()).toBeInTheDocument();
 
       expect(
         await screen.findByRole('button', { name: 'Issue on Jupyter Notebook' }),
       ).toBeInTheDocument();
 
-      await waitForElementToBeRemoved(screen.queryByText('issue.preview.jupyter_notebook.error'));
+      expect(screen.queryByText('issue.preview.jupyter_notebook.error')).not.toBeInTheDocument();
 
       const underlined = screen.getAllByTestId('hljs-sonar-underline');
       expect(underlined).toHaveLength(4);
index 3f5cdc1685e246149d7449826aba2593f3598489..4e2ca45bcd6aee51af06dc0bd8193893e220170a 100644 (file)
@@ -60,6 +60,7 @@ import { KeyboardKeys } from '../../../helpers/keycodes';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { serializeDate } from '../../../helpers/query';
 import { withBranchLikes } from '../../../queries/branch';
+import { useStandardExperienceMode } from '../../../queries/settings';
 import { BranchLike } from '../../../types/branch-like';
 import { isProject } from '../../../types/component';
 import {
@@ -109,6 +110,7 @@ interface Props extends WithIndexationContextProps {
   component?: Component;
   currentUser: CurrentUser;
   isFetchingBranch?: boolean;
+  isStandard?: boolean;
   location: Location;
   router: Router;
 }
@@ -154,6 +156,9 @@ export class App extends React.PureComponent<Props, State> {
     super(props);
     const query = parseQuery(props.location.query, props.component?.needIssueSync);
     this.bulkButtonRef = React.createRef();
+    const hasFilterFromOtherMode = props.isStandard
+      ? query.impactSoftwareQualities.length !== 0 || query.impactSeverities.length !== 0
+      : query.types.length !== 0 || query.severities.length !== 0;
 
     this.state = {
       bulkChangeModal: false,
@@ -165,21 +170,23 @@ export class App extends React.PureComponent<Props, State> {
       loadingMore: false,
       locationsNavigator: false,
       myIssues: areMyIssuesSelected(props.location.query),
-      openFacets: {
-        owaspTop10: shouldOpenStandardsChildFacet({}, query, SecurityStandard.OWASP_TOP10),
-        'owaspTop10-2021': shouldOpenStandardsChildFacet(
-          {},
-          query,
-          SecurityStandard.OWASP_TOP10_2021,
-        ),
-        cleanCodeAttributeCategories: true,
-        impactSoftwareQualities: true,
-        severities: true,
-        types: true,
-        impactSeverities: true,
-        sonarsourceSecurity: shouldOpenSonarSourceSecurityFacet({}, query),
-        standards: shouldOpenStandardsFacet({}, query),
-      },
+      openFacets: hasFilterFromOtherMode
+        ? {}
+        : {
+            owaspTop10: shouldOpenStandardsChildFacet({}, query, SecurityStandard.OWASP_TOP10),
+            'owaspTop10-2021': shouldOpenStandardsChildFacet(
+              {},
+              query,
+              SecurityStandard.OWASP_TOP10_2021,
+            ),
+            cleanCodeAttributeCategories: true,
+            impactSoftwareQualities: true,
+            severities: true,
+            types: true,
+            impactSeverities: true,
+            sonarsourceSecurity: shouldOpenSonarSourceSecurityFacet({}, query),
+            standards: shouldOpenStandardsFacet({}, query),
+          },
       query,
       referencedComponentsById: {},
       referencedComponentsByKey: {},
@@ -1228,13 +1235,23 @@ export class App extends React.PureComponent<Props, State> {
   }
 }
 
+function WrappedApp(props: Readonly<Omit<Props, 'isStandard'>>) {
+  const { data: isStandard, isLoading } = useStandardExperienceMode();
+
+  return (
+    <Spinner ariaLabel={translate('issues.loading_issues')} isLoading={isLoading}>
+      <App {...props} isStandard={isStandard} />
+    </Spinner>
+  );
+}
+
 export default withRouter(
   withComponentContext(
     withCurrentUserContext(
       withBranchLikes(
         withIndexationContext(
           withIndexationGuard<Props & WithIndexationContextProps>({
-            Component: App,
+            Component: WrappedApp,
             showIndexationMessage: ({
               component,
               indexationContext: {
index d6d2620533e33af945d728274cb4e04e25fe6e49..2efb1a1199f098cc3345ecc7dba38523b6eaf509 100644 (file)
@@ -170,6 +170,10 @@ export function Sidebar(props: Readonly<Props>) {
 
   const needIssueSync = component?.needIssueSync;
 
+  const secondLine = translate(
+    `issues.facet.second_line.mode.${isStandardMode ? 'mqr' : 'standard'}`,
+  );
+
   return (
     <>
       {displayPeriodFilter && (
@@ -201,6 +205,39 @@ export function Sidebar(props: Readonly<Props>) {
 
           <BasicSeparator className="sw-my-4" />
 
+          {query.types.length > 0 && (
+            <>
+              <TypeFacet
+                fetching={props.loadingFacets.types === true}
+                needIssueSync={needIssueSync}
+                onChange={props.onFilterChange}
+                onToggle={props.onFacetToggle}
+                open={!!openFacets.types}
+                stats={facets.types}
+                types={query.types}
+                secondLine={secondLine}
+              />
+              <BasicSeparator className="sw-my-4" />
+            </>
+          )}
+
+          {query.severities.length > 0 && (
+            <>
+              <StandardSeverityFacet
+                fetching={props.loadingFacets.severities === true}
+                onChange={props.onFilterChange}
+                onToggle={props.onFacetToggle}
+                open={!!openFacets.severities}
+                stats={facets.severities}
+                values={query.severities}
+                headerName={translate('issues.facet.severities')}
+                secondLine={secondLine}
+              />
+
+              <BasicSeparator className="sw-my-4" />
+            </>
+          )}
+
           <AttributeCategoryFacet
             fetching={props.loadingFacets.cleanCodeAttributeCategories === true}
             needIssueSync={needIssueSync}
@@ -241,6 +278,39 @@ export function Sidebar(props: Readonly<Props>) {
               />
 
               <BasicSeparator className="sw-my-4" />
+
+              {query.impactSoftwareQualities.length > 0 && (
+                <>
+                  <SoftwareQualityFacet
+                    fetching={props.loadingFacets.impactSoftwareQualities === true}
+                    needIssueSync={needIssueSync}
+                    onChange={props.onFilterChange}
+                    onToggle={props.onFacetToggle}
+                    open={!!openFacets.impactSoftwareQualities}
+                    stats={facets.impactSoftwareQualities}
+                    qualities={query.impactSoftwareQualities}
+                    secondLine={secondLine}
+                  />
+
+                  <BasicSeparator className="sw-my-4" />
+                </>
+              )}
+
+              {query.impactSeverities.length > 0 && (
+                <>
+                  <SeverityFacet
+                    fetching={props.loadingFacets.impactSeverities === true}
+                    onChange={props.onFilterChange}
+                    onToggle={props.onFacetToggle}
+                    open={!!openFacets.impactSeverities}
+                    stats={facets.impactSeverities}
+                    values={query.impactSeverities}
+                    secondLine={secondLine}
+                  />
+
+                  <BasicSeparator className="sw-my-4" />
+                </>
+              )}
             </>
           )}
         </>
index edf2f5aa2392991ac9b268ca681362be45a72594..d92b019c58ac4b56660da1ea37efe62f575baa2c 100644 (file)
@@ -34,6 +34,7 @@ export interface CommonProps {
   onChange: (changes: Partial<Query>) => void;
   onToggle: (property: string) => void;
   open: boolean;
+  secondLine?: string;
   stats: Dict<number> | undefined;
 }
 
@@ -50,6 +51,7 @@ export function SimpleListStyleFacet(props: Props) {
     fetching,
     open,
     selectedItems = [],
+    secondLine,
     stats = {},
     needIssueSync,
     property,
@@ -77,6 +79,7 @@ export function SimpleListStyleFacet(props: Props) {
       onClick={() => props.onToggle(property)}
       open={open}
       help={help}
+      secondLine={secondLine}
     >
       <FacetItemsList labelledby={headerId}>
         {listItems.map((item) => {
index cc7d661267c98598344d6b34f1c0d08f95546a25..a96858bd4bc7898667c0cbce185e268d5571ba8f 100644 (file)
@@ -35,6 +35,7 @@ interface Props {
   onChange: (changes: Partial<Query>) => void;
   onToggle: (property: string) => void;
   open: boolean;
+  secondLine?: string;
   stats: Dict<number> | undefined;
   types: string[];
 }
@@ -108,7 +109,7 @@ export class TypeFacet extends React.PureComponent<Props> {
   };
 
   render() {
-    const { fetching, open, types } = this.props;
+    const { fetching, open, types, secondLine } = this.props;
 
     const nbSelectableItems = AVAILABLE_TYPES.filter(this.getStat.bind(this)).length;
     const nbSelectedItems = types.length;
@@ -127,6 +128,7 @@ export class TypeFacet extends React.PureComponent<Props> {
         onClear={this.handleClear}
         onClick={this.handleHeaderClick}
         open={open}
+        secondLine={secondLine}
       >
         <FacetItemsList labelledby={typeFacetHeaderId}>
           {AVAILABLE_TYPES.map(this.renderItem)}
index f2b29ef55f46660d3f0bbc8c79040f7f88431202..57d9bd85aa2969e50d0e0db4d31fcb39982c6349 100644 (file)
@@ -25,7 +25,10 @@ import { mockComponent } from '../../../../helpers/mocks/component';
 import { mockQuery } from '../../../../helpers/mocks/issues';
 import { mockAppState } from '../../../../helpers/testMocks';
 import { renderApp } from '../../../../helpers/testReactTestingUtils';
+import { byRole } from '../../../../sonar-aligned/helpers/testSelector';
+import { SoftwareImpactSeverity, SoftwareQuality } from '../../../../types/clean-code-taxonomy';
 import { Feature } from '../../../../types/features';
+import { IssueSeverity, IssueType } from '../../../../types/issues';
 import { GlobalSettingKeys, SettingsKey } from '../../../../types/settings';
 import { Sidebar } from '../Sidebar';
 
@@ -75,6 +78,60 @@ describe('MQR mode', () => {
     ]);
   });
 
+  it('should show show mqr filters if they exist in query', async () => {
+    let component = renderSidebar({
+      query: mockQuery({ types: [IssueType.CodeSmell] }),
+    });
+
+    expect(
+      await screen.findByRole('button', { name: 'issues.facet.impactSoftwareQualities' }),
+    ).toBeInTheDocument();
+
+    expect(screen.getByRole('button', { name: 'issues.facet.types' })).toBeInTheDocument();
+    expect(
+      byRole('button', { name: 'issues.facet.types' })
+        .byText('issues.facet.second_line.mode.standard')
+        .get(),
+    ).toBeInTheDocument();
+    expect(
+      screen.queryByRole('button', { name: 'issues.facet.severities' }),
+    ).not.toBeInTheDocument();
+
+    component.unmount();
+
+    component = renderSidebar({
+      query: mockQuery({ severities: [IssueSeverity.Blocker] }),
+    });
+
+    expect(
+      await screen.findByRole('button', { name: 'issues.facet.impactSoftwareQualities' }),
+    ).toBeInTheDocument();
+
+    expect(screen.getByRole('button', { name: 'issues.facet.severities' })).toBeInTheDocument();
+    expect(
+      byRole('button', { name: 'issues.facet.severities' })
+        .byText('issues.facet.second_line.mode.standard')
+        .get(),
+    ).toBeInTheDocument();
+    expect(screen.queryByRole('button', { name: 'issues.facet.types' })).not.toBeInTheDocument();
+
+    component.unmount();
+
+    renderSidebar({
+      query: mockQuery({
+        types: [IssueType.CodeSmell],
+        severities: [IssueSeverity.Blocker],
+      }),
+    });
+
+    expect(
+      await screen.findByRole('button', { name: 'issues.facet.impactSoftwareQualities' }),
+    ).toBeInTheDocument();
+
+    expect(screen.getByRole('button', { name: 'issues.facet.types' })).toBeInTheDocument();
+    expect(screen.getByRole('button', { name: 'issues.facet.severities' })).toBeInTheDocument();
+  });
+
   it('should render correct facets for Application', () => {
     renderSidebar({ component: mockComponent({ qualifier: ComponentQualifier.Application }) });
 
@@ -179,6 +236,64 @@ describe('Standard mode', () => {
     ]);
   });
 
+  it('should show show mqr filters if they exist in query', async () => {
+    let component = renderSidebar({
+      query: mockQuery({ impactSeverities: [SoftwareImpactSeverity.Blocker] }),
+    });
+
+    expect(await screen.findByRole('button', { name: 'issues.facet.types' })).toBeInTheDocument();
+
+    expect(
+      screen.getByRole('button', { name: 'coding_rules.facet.impactSeverities' }),
+    ).toBeInTheDocument();
+    expect(
+      byRole('button', { name: 'coding_rules.facet.impactSeverities' })
+        .byText('issues.facet.second_line.mode.mqr')
+        .get(),
+    ).toBeInTheDocument();
+    expect(
+      screen.queryByRole('button', { name: 'issues.facet.impactSoftwareQualities' }),
+    ).not.toBeInTheDocument();
+
+    component.unmount();
+
+    component = renderSidebar({
+      query: mockQuery({ impactSoftwareQualities: [SoftwareQuality.Maintainability] }),
+    });
+
+    expect(await screen.findByRole('button', { name: 'issues.facet.types' })).toBeInTheDocument();
+
+    expect(
+      screen.getByRole('button', { name: 'issues.facet.impactSoftwareQualities' }),
+    ).toBeInTheDocument();
+    expect(
+      byRole('button', { name: 'issues.facet.impactSoftwareQualities' })
+        .byText('issues.facet.second_line.mode.mqr')
+        .get(),
+    ).toBeInTheDocument();
+    expect(
+      screen.queryByRole('button', { name: 'coding_rules.facet.impactSeverities' }),
+    ).not.toBeInTheDocument();
+
+    component.unmount();
+
+    renderSidebar({
+      query: mockQuery({
+        impactSoftwareQualities: [SoftwareQuality.Maintainability],
+        impactSeverities: [SoftwareImpactSeverity.Blocker],
+      }),
+    });
+
+    expect(await screen.findByRole('button', { name: 'issues.facet.types' })).toBeInTheDocument();
+
+    expect(
+      screen.getByRole('button', { name: 'issues.facet.impactSoftwareQualities' }),
+    ).toBeInTheDocument();
+    expect(
+      screen.getByRole('button', { name: 'coding_rules.facet.impactSeverities' }),
+    ).toBeInTheDocument();
+  });
+
   it('should render correct facets for Application', async () => {
     renderSidebar({ component: mockComponent({ qualifier: ComponentQualifier.Application }) });
 
index dccf87e7503d5087833386869e2d0f7b0bcad438..dc6200c22c5765cc6212db60a18c8a3efa5b0ae6 100644 (file)
@@ -195,8 +195,13 @@ export function renderIssueApp(
     },
   }),
   featureList: Feature[] = [],
+  navigateTo?: string,
 ) {
-  renderApp('issues', <IssuesApp />, { currentUser, featureList });
+  return renderApp('issues', <IssuesApp />, {
+    currentUser,
+    featureList,
+    navigateTo,
+  });
 }
 
 export function renderProjectIssuesApp(
@@ -210,7 +215,7 @@ export function renderProjectIssuesApp(
   }),
   featureList = [Feature.BranchSupport],
 ) {
-  renderAppWithComponentContext(
+  return renderAppWithComponentContext(
     'project/issues',
     () => (
       <Route
index c4b03fd76e86d0e3c03c378ea6162d94d0fe91c4..365af7843f0448cc3eeefe401ee47c31ad7bf2d2 100644 (file)
@@ -37,6 +37,7 @@ export interface BasicProps {
   onChange: (changes: Dict<string | string[] | undefined>) => void;
   onToggle: (facet: FacetKey) => void;
   open: boolean;
+  secondLine?: string;
   stats?: Dict<number>;
   values: string[];
 }
@@ -103,6 +104,7 @@ export default class Facet extends React.PureComponent<Props> {
       open,
       property,
       renderTextName = defaultRenderName,
+      secondLine,
       stats,
       help,
       values,
@@ -140,6 +142,7 @@ export default class Facet extends React.PureComponent<Props> {
         disabledHelper={disabledHelper}
         tooltipComponent={Tooltip}
         help={help}
+        secondLine={secondLine}
       >
         {open && items !== undefined && (
           <FacetItemsList labelledby={headerId}>{items.map(this.renderItem)}</FacetItemsList>
index 8269688ec2605f73715c0e480d21ae5edcbe7eda..1556548d858317691ee5dc71f27e9238cafee557 100644 (file)
@@ -67,7 +67,10 @@ describe('rendering', () => {
     const { ui } = getPageObject();
     const issue = mockIssue(true, { effort: '2 days', message: 'This is an issue' });
     const onClick = jest.fn();
-    renderIssue({ issue, onSelect: onClick });
+    renderIssue(
+      { issue, onSelect: onClick },
+      'scopes=MAIN&impactSeverities=LOW&types=VULNERABILITY',
+    );
 
     expect(ui.effort('2 days').get()).toBeInTheDocument();
     expect(ui.issueMessageLink.get()).toHaveAttribute(
@@ -383,6 +386,7 @@ function getPageObject() {
 
 function renderIssue(
   props: Partial<Omit<ComponentPropsType<typeof Issue>, 'onChange' | 'onPopupToggle'>> = {},
+  query?: string,
 ) {
   function Wrapper(
     wrapperProps: Omit<ComponentPropsType<typeof Issue>, 'onChange' | 'onPopupToggle'>,
@@ -405,7 +409,7 @@ function renderIssue(
   }
 
   return renderAppRoutes(
-    'issues?scopes=MAIN&impactSeverities=LOW&types=VULNERABILITY',
+    `issues${query ? `?${query}` : ''}`,
     () => (
       <Route
         path="issues"
index a56759bedc556272ce7003c9ad69c00429514289..74e86a08cf986e11a7f401258b3ef2a46620e5e0 100644 (file)
@@ -24,11 +24,15 @@ import * as React from 'react';
 import { Badge, CommentIcon, SeparatorCircleIcon } from '~design-system';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { isDefined } from '../../../helpers/types';
+import { useStandardExperienceMode } from '../../../queries/settings';
+import { useLocation } from '../../../sonar-aligned/components/hoc/withRouter';
 import { Issue } from '../../../types/types';
 import Tooltip from '../../controls/Tooltip';
 import DateFromNow from '../../intl/DateFromNow';
 import { WorkspaceContext } from '../../workspace/context';
 import IssuePrioritized from './IssuePrioritized';
+import IssueSeverity from './IssueSeverity';
+import IssueType from './IssueType';
 import SonarLintBadge from './SonarLintBadge';
 
 interface Props {
@@ -38,8 +42,10 @@ interface Props {
 
 export default function IssueMetaBar(props: Readonly<Props>) {
   const { issue, showLine } = props;
+  const location = useLocation();
 
   const { externalRulesRepoNames } = React.useContext(WorkspaceContext);
+  const { data: isStandardMode } = useStandardExperienceMode();
 
   const ruleEngine =
     (issue.externalRuleEngine && externalRulesRepoNames[issue.externalRuleEngine]) ||
@@ -139,6 +145,17 @@ export default function IssueMetaBar(props: Readonly<Props>) {
       <IssueMetaListItem className={issueMetaListItemClassNames}>
         <DateFromNow date={issue.creationDate} />
       </IssueMetaListItem>
+      {!isStandardMode && (location.query.types || location.query.severities) && (
+        <>
+          <SeparatorCircleIcon aria-hidden as="li" />
+
+          <IssueType issue={issue} height={12} width={12} />
+
+          <SeparatorCircleIcon data-guiding-id="issue-4" aria-hidden as="li" />
+
+          <IssueSeverity issue={issue} height={12} width={12} />
+        </>
+      )}
 
       {issue.prioritizedRule && (
         <>
index e5eb59c552aa0486fa7a8cc826cc32527a720528..447718f0f1a51677fe05562aff6b746eb6e50816 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
-import { IconProps, TextSubdued } from '~design-system';
-import DocHelpTooltip from '~sonar-aligned/components/controls/DocHelpTooltip';
-import { DocLink } from '../../../helpers/doc-links';
+import { Text } from '@sonarsource/echoes-react';
+import { IconProps } from '~design-system';
 import { translate } from '../../../helpers/l10n';
 import { IssueSeverity as IssueSeverityType } from '../../../types/issues';
 import { Issue } from '../../../types/types';
-import IssueSeverityIcon from '../../icon-mappers/IssueSeverityIcon';
-import { DeprecatedFieldTooltip } from './DeprecatedFieldTooltip';
+import SoftwareImpactSeverityIcon from '../../icon-mappers/SoftwareImpactSeverityIcon';
 
 interface Props extends IconProps {
   issue: Pick<Issue, 'severity'>;
@@ -33,24 +31,14 @@ interface Props extends IconProps {
 
 export default function IssueSeverity({ issue, ...iconProps }: Readonly<Props>) {
   return (
-    <DocHelpTooltip
-      content={<DeprecatedFieldTooltip field="severity" />}
-      links={[
-        {
-          href: DocLink.Issues,
-          label: translate('learn_more'),
-        },
-      ]}
-    >
-      <TextSubdued className="sw-flex sw-items-center sw-gap-1/2">
-        <IssueSeverityIcon
-          aria-hidden
-          fill="var(--echoes-color-icon-disabled)"
-          severity={issue.severity as IssueSeverityType}
-          {...iconProps}
-        />
-        {translate('severity', issue.severity)}
-      </TextSubdued>
-    </DocHelpTooltip>
+    <Text isSubdued className="sw-flex sw-items-center sw-gap-1/2">
+      <SoftwareImpactSeverityIcon
+        aria-hidden
+        disabled
+        severity={issue.severity as IssueSeverityType}
+        {...iconProps}
+      />
+      {translate('severity', issue.severity)}
+    </Text>
   );
 }
index e20acdd9991fda01a57ccb6190266d0ad6643d38..a5b4d54ca54fd125567ff4cf4fbfa6d1eed883aa 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
-import { IconProps, TextSubdued } from '~design-system';
-import DocHelpTooltip from '~sonar-aligned/components/controls/DocHelpTooltip';
-import { DocLink } from '../../../helpers/doc-links';
+import { Text } from '@sonarsource/echoes-react';
+import { IconProps } from '~design-system';
 import { translate } from '../../../helpers/l10n';
 import { Issue } from '../../../types/types';
 import IssueTypeIcon from '../../icon-mappers/IssueTypeIcon';
-import { DeprecatedFieldTooltip } from './DeprecatedFieldTooltip';
 
 interface Props extends IconProps {
   issue: Pick<Issue, 'type'>;
@@ -32,24 +30,14 @@ interface Props extends IconProps {
 
 export default function IssueType({ issue, ...iconProps }: Readonly<Props>) {
   return (
-    <DocHelpTooltip
-      content={<DeprecatedFieldTooltip field="type" />}
-      links={[
-        {
-          href: DocLink.Issues,
-          label: translate('learn_more'),
-        },
-      ]}
-    >
-      <TextSubdued className="sw-flex sw-items-center sw-gap-1/2">
-        <IssueTypeIcon
-          aria-hidden
-          fill="var(--echoes-color-icon-disabled)"
-          type={issue.type}
-          {...iconProps}
-        />
-        {translate('issue.type', issue.type)}
-      </TextSubdued>
-    </DocHelpTooltip>
+    <Text isSubdued className="sw-flex sw-items-center sw-gap-1/2">
+      <IssueTypeIcon
+        aria-hidden
+        fill="var(--echoes-color-icon-disabled)"
+        type={issue.type}
+        {...iconProps}
+      />
+      {translate('issue.type', issue.type)}
+    </Text>
   );
 }
index 4b1a4adee2a33cef23c5daf93fa675d6a6ba2599..bac0c8b20db4fdc0b2f95140f2361965e419a416 100644 (file)
@@ -19,6 +19,7 @@
  */
 
 import styled from '@emotion/styled';
+import { Text } from '@sonarsource/echoes-react';
 import classNames from 'classnames';
 import { uniqueId } from 'lodash';
 import * as React from 'react';
@@ -51,6 +52,7 @@ export interface FacetBoxProps {
   onClear?: () => void;
   onClick?: (isOpen: boolean) => void;
   open?: boolean;
+  secondLine?: string;
   tooltipComponent?: React.ComponentType<React.PropsWithChildren<{ content: React.ReactNode }>>;
 }
 
@@ -65,6 +67,7 @@ export function FacetBox(props: FacetBoxProps) {
     'data-property': dataProperty,
     disabled = false,
     disabledHelper,
+    secondLine,
     hasEmbeddedFacets = false,
     help,
     id: idProp,
@@ -82,6 +85,7 @@ export function FacetBox(props: FacetBoxProps) {
   const expandable = !disabled && Boolean(onClick);
   const id = React.useMemo(() => idProp ?? uniqueId('filter-facet-'), [idProp]);
   const Tooltip = tooltipComponent ?? SCTooltip;
+
   return (
     <Accordion
       className={classNames(className, { open })}
@@ -117,7 +121,14 @@ export function FacetBox(props: FacetBoxProps) {
                 </HeaderTitle>
               </Tooltip>
             ) : (
-              <HeaderTitle>{name}</HeaderTitle>
+              <div>
+                <HeaderTitle>{name}</HeaderTitle>
+                {secondLine !== undefined && (
+                  <Text as="div" isSubdued>
+                    {secondLine}
+                  </Text>
+                )}
+              </div>
             )}
           </ChevronAndTitle>
           {help && <span className="sw-ml-1">{help}</span>}
index 9e9d817c5dc1f8864603fcad065327f595269152..5355fdf72ea4bac7f4a4487897dfe7eb924d38b9 100644 (file)
@@ -1263,6 +1263,8 @@ issues.facet.cwe=CWE
 issues.facet.sonarsource.show_more=Show more SonarSource categories
 issues.facet.prioritized_rule.category=Prioritized rules
 issues.facet.prioritized_rule=Issues from prioritized rules
+issues.facet.second_line.mode.standard=Standard Experience
+issues.facet.second_line.mode.mqr=MQR mode
 
 #------------------------------------------------------------------------------
 #