]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13190 Add tags to Applications information sidedrawer
authorJeremy Davis <jeremy.davis@sonarsource.com>
Mon, 6 Apr 2020 12:27:19 +0000 (14:27 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 15 Apr 2020 20:03:38 +0000 (20:03 +0000)
server/sonar-web/src/main/js/api/components.ts
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/ProjectInformationRenderer.tsx
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/ProjectInformationRenderer-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/__tests__/__snapshots__/ProjectInformationRenderer-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/MetaTags.tsx
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/MetaTags-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/projectInformation/meta/__tests__/__snapshots__/MetaTags-test.tsx.snap

index df296aa6181321a30e577bfab85f63af12c6bce4..2b7ddb744927006668c0a173b73cad397e7d9385 100644 (file)
@@ -86,6 +86,10 @@ export function searchProjectTags(data?: { ps?: number; q?: string }): Promise<a
   return getJSON('/api/project_tags/search', data).catch(throwGlobalError);
 }
 
+export function setApplicationTags(data: { application: string; tags: string }): Promise<void> {
+  return post('/api/applications/set_tags', data);
+}
+
 export function setProjectTags(data: { project: string; tags: string }): Promise<void> {
   return post('/api/project_tags/set', data);
 }
index 1aff0f9cf33d0cfce55bf90e8c131d356e9ab7b5..25bab1a9d05444bee9da8e4001c560e008558d58 100644 (file)
@@ -53,27 +53,23 @@ export function ProjectInformationRenderer(props: ProjectInformationRendererProp
       </div>
 
       <div className="overflow-y-auto">
-        {(component.description || !isApp) && (
-          <div className="big-padded bordered-bottom">
-            <div className="display-flex-center">
-              <h3 className="spacer-right">{translate('project.info.description')}</h3>
-              {component.visibility && (
-                <PrivacyBadgeContainer
-                  organization={undefined}
-                  qualifier={component.qualifier}
-                  tooltipProps={{ projectKey: component.key }}
-                  visibility={component.visibility}
-                />
-              )}
-            </div>
-
-            {component.description && <p className="spacer-bottom">{component.description}</p>}
-
-            {!isApp && (
-              <MetaTags component={component} onComponentChange={props.onComponentChange} />
+        <div className="big-padded bordered-bottom">
+          <div className="display-flex-center">
+            <h3 className="spacer-right">{translate('project.info.description')}</h3>
+            {component.visibility && (
+              <PrivacyBadgeContainer
+                organization={undefined}
+                qualifier={component.qualifier}
+                tooltipProps={{ projectKey: component.key }}
+                visibility={component.visibility}
+              />
             )}
           </div>
-        )}
+
+          {component.description && <p>{component.description}</p>}
+
+          <MetaTags component={component} onComponentChange={props.onComponentChange} />
+        </div>
 
         <div className="big-padded bordered-bottom it__project-loc-value">
           <MetaSize component={component} measures={measures} />
index 577b2e387d2108969ae9e8ea861cdc285e77e037..26f2ab9ff25ce514690f370067e0611fbcfbb055 100644 (file)
@@ -43,6 +43,11 @@ it('should render an app correctly', () => {
   expect(shallowRender({ component })).toMatchSnapshot('default');
 });
 
+it('should render without description', () => {
+  const component = mockComponent({ description: undefined });
+  expect(shallowRender({ component })).toMatchSnapshot();
+});
+
 it('should handle missing quality profiles and quality gates', () => {
   expect(
     shallowRender({
index d071690097a5984b430522ffb1f1df8383e7af4b..8819cf6af17d0464e3d002bb688205ac811af1c8 100644 (file)
@@ -273,6 +273,45 @@ exports[`should render an app correctly: default 1`] = `
   <div
     className="overflow-y-auto"
   >
+    <div
+      className="big-padded bordered-bottom"
+    >
+      <div
+        className="display-flex-center"
+      >
+        <h3
+          className="spacer-right"
+        >
+          project.info.description
+        </h3>
+      </div>
+      <MetaTags
+        component={
+          Object {
+            "breadcrumbs": Array [],
+            "key": "my-project",
+            "name": "MyProject",
+            "organization": "foo",
+            "qualifier": "APP",
+            "qualityGate": Object {
+              "isDefault": true,
+              "key": "30",
+              "name": "Sonar way",
+            },
+            "qualityProfiles": Array [
+              Object {
+                "deleted": false,
+                "key": "my-qp",
+                "language": "ts",
+                "name": "Sonar way",
+              },
+            ],
+            "tags": Array [],
+          }
+        }
+        onComponentChange={[MockFunction]}
+      />
+    </div>
     <div
       className="big-padded bordered-bottom it__project-loc-value"
     >
@@ -968,3 +1007,160 @@ exports[`should render correctly: with notifications 1`] = `
   </div>
 </Fragment>
 `;
+
+exports[`should render without description 1`] = `
+<Fragment>
+  <div>
+    <h2
+      className="big-padded bordered-bottom"
+    >
+      project.info.title
+    </h2>
+  </div>
+  <div
+    className="overflow-y-auto"
+  >
+    <div
+      className="big-padded bordered-bottom"
+    >
+      <div
+        className="display-flex-center"
+      >
+        <h3
+          className="spacer-right"
+        >
+          project.info.description
+        </h3>
+      </div>
+      <MetaTags
+        component={
+          Object {
+            "breadcrumbs": Array [],
+            "description": undefined,
+            "key": "my-project",
+            "name": "MyProject",
+            "organization": "foo",
+            "qualifier": "TRK",
+            "qualityGate": Object {
+              "isDefault": true,
+              "key": "30",
+              "name": "Sonar way",
+            },
+            "qualityProfiles": Array [
+              Object {
+                "deleted": false,
+                "key": "my-qp",
+                "language": "ts",
+                "name": "Sonar way",
+              },
+            ],
+            "tags": Array [],
+          }
+        }
+        onComponentChange={[MockFunction]}
+      />
+    </div>
+    <div
+      className="big-padded bordered-bottom it__project-loc-value"
+    >
+      <MetaSize
+        component={
+          Object {
+            "breadcrumbs": Array [],
+            "description": undefined,
+            "key": "my-project",
+            "name": "MyProject",
+            "organization": "foo",
+            "qualifier": "TRK",
+            "qualityGate": Object {
+              "isDefault": true,
+              "key": "30",
+              "name": "Sonar way",
+            },
+            "qualityProfiles": Array [
+              Object {
+                "deleted": false,
+                "key": "my-qp",
+                "language": "ts",
+                "name": "Sonar way",
+              },
+            ],
+            "tags": Array [],
+          }
+        }
+        measures={Array []}
+      />
+    </div>
+    <div
+      className="big-padded bordered-bottom"
+    >
+      <MetaQualityGate
+        qualityGate={
+          Object {
+            "isDefault": true,
+            "key": "30",
+            "name": "Sonar way",
+          }
+        }
+      />
+      <Connect(MetaQualityProfiles)
+        headerClassName="big-spacer-top"
+        profiles={
+          Array [
+            Object {
+              "deleted": false,
+              "key": "my-qp",
+              "language": "ts",
+              "name": "Sonar way",
+            },
+          ]
+        }
+      />
+    </div>
+    <MetaLinks
+      component={
+        Object {
+          "breadcrumbs": Array [],
+          "description": undefined,
+          "key": "my-project",
+          "name": "MyProject",
+          "organization": "foo",
+          "qualifier": "TRK",
+          "qualityGate": Object {
+            "isDefault": true,
+            "key": "30",
+            "name": "Sonar way",
+          },
+          "qualityProfiles": Array [
+            Object {
+              "deleted": false,
+              "key": "my-qp",
+              "language": "ts",
+              "name": "Sonar way",
+            },
+          ],
+          "tags": Array [],
+        }
+      }
+    />
+    <div
+      className="big-padded bordered-bottom"
+    >
+      <MetaKey
+        componentKey="my-project"
+        qualifier="TRK"
+      />
+    </div>
+    <Memo(DrawerLink)
+      label="overview.badges.get_badge.TRK"
+      onPageChange={[MockFunction]}
+      to={1}
+    />
+    <Memo(DrawerLink)
+      label="project.info.to_notifications"
+      onPageChange={[MockFunction]}
+      to={2}
+    />
+  </div>
+</Fragment>
+`;
index 1e7b349658634b2384bdf81cd3a27251addba4ad..03650b13f6737c5a46ac649d5ee40d6633ebd023 100644 (file)
@@ -22,8 +22,9 @@ import { ButtonLink } from 'sonar-ui-common/components/controls/buttons';
 import Dropdown from 'sonar-ui-common/components/controls/Dropdown';
 import { PopupPlacement } from 'sonar-ui-common/components/ui/popups';
 import { translate } from 'sonar-ui-common/helpers/l10n';
-import { setProjectTags } from '../../../../../../api/components';
+import { setApplicationTags, setProjectTags } from '../../../../../../api/components';
 import TagsList from '../../../../../../components/tags/TagsList';
+import { ComponentQualifier } from '../../../../../../types/component';
 import MetaTagsSelector from './MetaTagsSelector';
 
 interface Props {
@@ -46,11 +47,24 @@ export default class MetaTags extends React.PureComponent<Props> {
     right: containerPos.width - eltPos.width
   });
 
+  setTags = (values: string[]) => {
+    const { component } = this.props;
+
+    if (component.qualifier === ComponentQualifier.Application) {
+      return setApplicationTags({
+        application: component.key,
+        tags: values.join(',')
+      });
+    } else {
+      return setProjectTags({
+        project: component.key,
+        tags: values.join(',')
+      });
+    }
+  };
+
   handleSetProjectTags = (values: string[]) => {
-    setProjectTags({
-      project: this.props.component.key,
-      tags: values.join(',')
-    }).then(
+    this.setTags(values).then(
       () => this.props.onComponentChange({ tags: values }),
       () => {}
     );
@@ -62,7 +76,7 @@ export default class MetaTags extends React.PureComponent<Props> {
 
     if (this.canUpdateTags()) {
       return (
-        <div className="project-info-tags" ref={card => (this.card = card)}>
+        <div className="big-spacer-top project-info-tags" ref={card => (this.card = card)}>
           <Dropdown
             closeOnClick={false}
             closeOnClickOutside={true}
index f8464ab66c5940207c04c622069d8d4e53c331d7..d6df5ed4b7cc0d0fb3bf288eb957a4fd7f6747a0 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
+import { setApplicationTags, setProjectTags } from '../../../../../../../api/components';
 import { mockComponent } from '../../../../../../../helpers/testMocks';
+import { ComponentQualifier } from '../../../../../../../types/component';
 import MetaTags from '../MetaTags';
 
-const component = mockComponent({
-  configuration: {
-    showSettings: false
-  }
-});
+jest.mock('../../../../../../../api/components', () => ({
+  setApplicationTags: jest.fn().mockResolvedValue(true),
+  setProjectTags: jest.fn().mockResolvedValue(true)
+}));
 
-const componentWithTags = mockComponent({
-  key: 'my-second-project',
-  tags: ['foo', 'bar'],
-  configuration: {
-    showSettings: true
-  },
-  name: 'MySecondProject'
+beforeEach(() => {
+  jest.clearAllMocks();
 });
 
 it('should render without tags and admin rights', () => {
-  expect(
-    shallow(<MetaTags component={component} onComponentChange={jest.fn()} />, {
-      disableLifecycleMethods: true
-    })
-  ).toMatchSnapshot();
+  expect(shallowRender()).toMatchSnapshot();
 });
 
 it('should render with tags and admin rights', () => {
-  expect(
-    shallow(<MetaTags component={componentWithTags} onComponentChange={jest.fn()} />, {
-      disableLifecycleMethods: true
-    })
-  ).toMatchSnapshot();
+  const component = mockComponent({
+    key: 'my-second-project',
+    tags: ['foo', 'bar'],
+    configuration: {
+      showSettings: true
+    },
+    name: 'MySecondProject'
+  });
+
+  expect(shallowRender({ component })).toMatchSnapshot();
+});
+
+it('should set tags for a project', () => {
+  const wrapper = shallowRender();
+
+  wrapper.instance().handleSetProjectTags(['tag1', 'tag2']);
+
+  expect(setProjectTags).toHaveBeenCalled();
+  expect(setApplicationTags).not.toHaveBeenCalled();
 });
+
+it('should set tags for an app', () => {
+  const wrapper = shallowRender({
+    component: mockComponent({ qualifier: ComponentQualifier.Application })
+  });
+
+  wrapper.instance().handleSetProjectTags(['tag1', 'tag2']);
+
+  expect(setProjectTags).not.toHaveBeenCalled();
+  expect(setApplicationTags).toHaveBeenCalled();
+});
+
+function shallowRender(overrides: Partial<MetaTags['props']> = {}) {
+  const component = mockComponent({
+    configuration: {
+      showSettings: false
+    }
+  });
+
+  return shallow<MetaTags>(
+    <MetaTags component={component} onComponentChange={jest.fn()} {...overrides} />
+  );
+}
index c415bdfdab7449cf78acb61dae6e41466df043a8..5e9a5365588903dfc13bae85918ddfecbccb809d 100644 (file)
@@ -2,7 +2,7 @@
 
 exports[`should render with tags and admin rights 1`] = `
 <div
-  className="project-info-tags"
+  className="big-spacer-top project-info-tags"
 >
   <Dropdown
     closeOnClick={false}