From daafaa37d838a2f87aa5f35c2776788c833358e4 Mon Sep 17 00:00:00 2001
From: Daniele Rossetti <drossetti@gitlab.com>
Date: Mon, 26 Aug 2024 11:50:24 +0000
Subject: [PATCH] Extract reusable logic from metrics related issue

---
 app/assets/javascripts/observability/utils.js | 22 ++++++++++
 .../fragments/related_issue.fragment.graphql  | 28 ++++++++++++
 .../get_metrics_related_issues.query.graphql  | 27 +-----------
 .../related_issues_provider.vue               | 26 +----------
 .../metrics/details/metrics_details_spec.js   |  5 +--
 .../details/related_issues/mock_data.js       |  2 +-
 .../related_issues_provider_spec.js           | 43 ++++++++-----------
 spec/frontend/observability/mock_data.js      | 39 +++++++++++++++++
 spec/frontend/observability/utils_spec.js     |  9 ++++
 9 files changed, 122 insertions(+), 79 deletions(-)
 create mode 100644 ee/app/assets/javascripts/graphql_shared/fragments/related_issue.fragment.graphql

diff --git a/app/assets/javascripts/observability/utils.js b/app/assets/javascripts/observability/utils.js
index f742c5feae8f0..8b3b867eeb847 100644
--- a/app/assets/javascripts/observability/utils.js
+++ b/app/assets/javascripts/observability/utils.js
@@ -1,6 +1,8 @@
 import { padWithZeros } from '~/lib/utils/datetime/date_format_utility';
 import { isValidDate, differenceInMinutes } from '~/lib/utils/datetime_utility';
 import { mergeUrlParams } from '~/lib/utils/url_utility';
+import { TYPE_ISSUE } from '~/issues/constants';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
 
 import {
   CUSTOM_DATE_RANGE_OPTION,
@@ -216,3 +218,23 @@ export function createIssueUrlWithDetails(createIssueUrl, detailsPayload, paramN
     },
   );
 }
+
+function parseGraphQLObject(obj) {
+  if (!obj) return null;
+
+  return {
+    ...obj,
+    id: getIdFromGraphQLId(obj.id),
+  };
+}
+
+export function parseGraphQLIssueLinksToRelatedIssues(issueLinks) {
+  return issueLinks.map(({ issue }) => ({
+    ...issue,
+    id: getIdFromGraphQLId(issue.id),
+    path: issue.webUrl,
+    type: TYPE_ISSUE,
+    milestone: parseGraphQLObject(issue.milestone),
+    assignees: issue.assignees.nodes.map(parseGraphQLObject),
+  }));
+}
diff --git a/ee/app/assets/javascripts/graphql_shared/fragments/related_issue.fragment.graphql b/ee/app/assets/javascripts/graphql_shared/fragments/related_issue.fragment.graphql
new file mode 100644
index 0000000000000..52bfb0cdbe5a2
--- /dev/null
+++ b/ee/app/assets/javascripts/graphql_shared/fragments/related_issue.fragment.graphql
@@ -0,0 +1,28 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+fragment RelatedIssue on Issue {
+  id
+  title
+  state
+  description
+  confidential
+  createdAt
+  closedAt
+  webUrl
+  dueDate
+  reference
+  weight
+  assignees {
+    nodes {
+      ...User
+    }
+  }
+  milestone {
+    expired
+    id
+    title
+    state
+    startDate
+    dueDate
+  }
+}
diff --git a/ee/app/assets/javascripts/metrics/details/related_issues/graphql/get_metrics_related_issues.query.graphql b/ee/app/assets/javascripts/metrics/details/related_issues/graphql/get_metrics_related_issues.query.graphql
index 8035d14715fa9..2ded3605ee11a 100644
--- a/ee/app/assets/javascripts/metrics/details/related_issues/graphql/get_metrics_related_issues.query.graphql
+++ b/ee/app/assets/javascripts/metrics/details/related_issues/graphql/get_metrics_related_issues.query.graphql
@@ -1,4 +1,4 @@
-#import "~/graphql_shared/fragments/user.fragment.graphql"
+#import "ee/graphql_shared/fragments/related_issue.fragment.graphql"
 
 query getMetricsRelatedIssues(
   $projectFullPath: ID!
@@ -10,30 +10,7 @@ query getMetricsRelatedIssues(
     observabilityMetricsLinks(name: $metricName, type: $metricType) {
       nodes {
         issue {
-          id
-          title
-          state
-          description
-          confidential
-          createdAt
-          closedAt
-          webUrl
-          dueDate
-          reference
-          weight
-          assignees {
-            nodes {
-              ...User
-            }
-          }
-          milestone {
-            expired
-            id
-            title
-            state
-            startDate
-            dueDate
-          }
+          ...RelatedIssue
         }
       }
     }
diff --git a/ee/app/assets/javascripts/metrics/details/related_issues/related_issues_provider.vue b/ee/app/assets/javascripts/metrics/details/related_issues/related_issues_provider.vue
index 3c417f5a0450f..ff46b7d1528ff 100644
--- a/ee/app/assets/javascripts/metrics/details/related_issues/related_issues_provider.vue
+++ b/ee/app/assets/javascripts/metrics/details/related_issues/related_issues_provider.vue
@@ -1,6 +1,5 @@
 <script>
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { TYPE_ISSUE } from '~/issues/constants';
+import { parseGraphQLIssueLinksToRelatedIssues } from '~/observability/utils';
 import { GRAPHQL_METRIC_TYPE } from '../../constants';
 import getMetricsRelatedIssues from './graphql/get_metrics_related_issues.query.graphql';
 
@@ -26,19 +25,6 @@ export default {
       isLoading: false,
     };
   },
-  methods: {
-    parseGraphQLObject(obj) {
-      if (!obj) return null;
-
-      return {
-        ...obj,
-        id: getIdFromGraphQLId(obj.id),
-      };
-    },
-    parseGraphQLNodes(nodes) {
-      return nodes.map((node) => this.parseGraphQLObject(node));
-    },
-  },
   apollo: {
     relatedIssues: {
       query: getMetricsRelatedIssues,
@@ -51,15 +37,7 @@ export default {
       },
       update(data) {
         const links = data.project?.observabilityMetricsLinks?.nodes || [];
-
-        return links.map(({ issue }) => ({
-          ...issue,
-          id: getIdFromGraphQLId(issue.id),
-          path: issue.webUrl,
-          type: TYPE_ISSUE,
-          milestone: this.parseGraphQLObject(issue.milestone),
-          assignees: this.parseGraphQLNodes(issue.assignees.nodes),
-        }));
+        return parseGraphQLIssueLinksToRelatedIssues(links);
       },
       error(error) {
         this.error = error;
diff --git a/ee/spec/frontend/metrics/details/metrics_details_spec.js b/ee/spec/frontend/metrics/details/metrics_details_spec.js
index 98eb6051a7c59..00b58ef7e1ebd 100644
--- a/ee/spec/frontend/metrics/details/metrics_details_spec.js
+++ b/ee/spec/frontend/metrics/details/metrics_details_spec.js
@@ -43,6 +43,7 @@ describe('MetricsDetails', () => {
   const findEmptyState = () => findMetricDetails().findComponent(GlEmptyState);
   const findFilteredSearch = () => findMetricDetails().findComponent(FilteredSearch);
   const findRelatedIssues = () => wrapper.findComponent(RelatedIssue);
+  const findRelatedIssuesProvider = () => wrapper.findComponent(RelatedIssuesProvider);
 
   const setFilters = async (attributes, dateRange, groupBy) => {
     findFilteredSearch().vm.$emit('submit', {
@@ -146,9 +147,7 @@ describe('MetricsDetails', () => {
     });
 
     it('renders the related-issue-provider', () => {
-      const relatedIssues = wrapper.findComponent(RelatedIssuesProvider);
-      expect(relatedIssues.exists()).toBe(true);
-      expect(relatedIssues.props()).toEqual({
+      expect(findRelatedIssuesProvider().props()).toEqual({
         metricName: defaultProps.metricId,
         metricType: defaultProps.metricType,
         projectFullPath: defaultProps.projectFullPath,
diff --git a/ee/spec/frontend/metrics/details/related_issues/mock_data.js b/ee/spec/frontend/metrics/details/related_issues/mock_data.js
index 1149ccb60017e..077aa3d1ef02c 100644
--- a/ee/spec/frontend/metrics/details/related_issues/mock_data.js
+++ b/ee/spec/frontend/metrics/details/related_issues/mock_data.js
@@ -1,4 +1,4 @@
-export const mockData = {
+export const mockQueryResult = {
   data: {
     project: {
       __typename: 'Project',
diff --git a/ee/spec/frontend/metrics/details/related_issues/related_issues_provider_spec.js b/ee/spec/frontend/metrics/details/related_issues/related_issues_provider_spec.js
index 8fc237e324e50..2207cc74d9624 100644
--- a/ee/spec/frontend/metrics/details/related_issues/related_issues_provider_spec.js
+++ b/ee/spec/frontend/metrics/details/related_issues/related_issues_provider_spec.js
@@ -5,7 +5,10 @@ import createMockApollo from 'helpers/mock_apollo_helper';
 import waitForPromises from 'helpers/wait_for_promises';
 import RelatedIssuesProvider from 'ee/metrics/details/related_issues/related_issues_provider.vue';
 import relatedIssuesQuery from 'ee/metrics/details/related_issues/graphql/get_metrics_related_issues.query.graphql';
-import { mockData } from './mock_data';
+import { parseGraphQLIssueLinksToRelatedIssues } from '~/observability/utils';
+import { mockQueryResult } from './mock_data';
+
+jest.mock('~/observability/utils');
 
 Vue.use(VueApollo);
 
@@ -18,7 +21,7 @@ describe('RelatedIssuesProvider component', () => {
   let wrapper;
 
   function createComponent({ props = defaultProps, slots, queryMock } = {}) {
-    relatedIssuesQueryMock = queryMock ?? jest.fn().mockResolvedValue(mockData);
+    relatedIssuesQueryMock = queryMock ?? jest.fn().mockResolvedValue(mockQueryResult);
     const apolloProvider = createMockApollo([[relatedIssuesQuery, relatedIssuesQueryMock]]);
 
     defaultSlotSpy = jest.fn();
@@ -34,6 +37,16 @@ describe('RelatedIssuesProvider component', () => {
     });
   }
 
+  const mockIssues = [
+    {
+      id: 'mock-issue',
+    },
+  ];
+
+  beforeEach(() => {
+    parseGraphQLIssueLinksToRelatedIssues.mockReturnValue(mockIssues);
+  });
+
   describe('rendered output', () => {
     it('renders correctly with default slot', () => {
       createComponent({ slots: { default: '<div>Test slot content</div>' } });
@@ -70,30 +83,8 @@ describe('RelatedIssuesProvider component', () => {
       });
     });
 
-    it('calls the default slots with issues', () => {
-      const mockIssue = mockData.data.project.observabilityMetricsLinks.nodes[0].issue;
-      const expectedIssues = [
-        {
-          ...mockIssue,
-          id: 647,
-          type: 'issue',
-          path: mockIssue.webUrl,
-          milestone: {
-            ...mockIssue.milestone,
-            id: 13,
-          },
-          assignees: [
-            {
-              ...mockIssue.assignees.nodes[0],
-              id: 1,
-            },
-          ],
-        },
-      ];
-
-      expect(defaultSlotSpy).toHaveBeenCalledWith(
-        expect.objectContaining({ issues: expectedIssues }),
-      );
+    it('calls the default slots with parsed issue objects', () => {
+      expect(defaultSlotSpy).toHaveBeenCalledWith(expect.objectContaining({ issues: mockIssues }));
     });
 
     it('calls the default slots with loading = false', async () => {
diff --git a/spec/frontend/observability/mock_data.js b/spec/frontend/observability/mock_data.js
index 7fd0834c44352..a3cf6bc0048d8 100644
--- a/spec/frontend/observability/mock_data.js
+++ b/spec/frontend/observability/mock_data.js
@@ -1,3 +1,42 @@
+export const mockGraphQlIssueLinks = [
+  {
+    issue: {
+      id: 'gid://gitlab/Issue/647',
+      title: 'Minus corrupti provident autem nisi veritatis dicta.',
+      state: 'opened',
+      description: 'Illum harum laborum ipsum repellendus unde maxime eaque.',
+      confidential: false,
+      createdAt: '2024-05-25T22:49:16Z',
+      closedAt: null,
+      webUrl: 'http://127.0.0.1:3000/gitlab-org/gitlab-shell/-/issues/45',
+      dueDate: '2024-08-30',
+      reference: '#45',
+      weight: 2,
+      assignees: {
+        nodes: [
+          {
+            id: 'gid://gitlab/User/1',
+            avatarUrl:
+              'https://www.gravatar.com/avatar/6ff3626da4f065bf63f9fa28289f327903d1aefca2308ef28e02dfc7ca298b11?s=80&d=identicon',
+            name: 'Administrator',
+            username: 'root',
+            webUrl: 'http://127.0.0.1:3000/root',
+            webPath: '/root',
+          },
+        ],
+      },
+      milestone: {
+        expired: false,
+        id: 'gid://gitlab/Milestone/13',
+        title: 'v2.0',
+        state: 'active',
+        startDate: '2024-08-31',
+        dueDate: '2024-09-30',
+      },
+    },
+  },
+];
+
 export const mockRelatedIssues = [
   {
     id: 647,
diff --git a/spec/frontend/observability/utils_spec.js b/spec/frontend/observability/utils_spec.js
index 73c9471137938..ada44593ac260 100644
--- a/spec/frontend/observability/utils_spec.js
+++ b/spec/frontend/observability/utils_spec.js
@@ -7,6 +7,7 @@ import {
   formattedTimeFromDate,
   isTracingDateRangeOutOfBounds,
   validatedDateRangeQuery,
+  parseGraphQLIssueLinksToRelatedIssues,
   createIssueUrlWithDetails,
 } from '~/observability/utils';
 import {
@@ -18,6 +19,8 @@ import {
   TIME_RANGE_OPTIONS_VALUES,
 } from '~/observability/constants';
 
+import { mockGraphQlIssueLinks, mockRelatedIssues } from './mock_data';
+
 const MOCK_NOW_DATE = new Date('2023-10-09 15:30:00');
 const realDateNow = Date.now;
 describe('periodToDate', () => {
@@ -282,3 +285,9 @@ describe('createIssueUrlWithDetails', () => {
     );
   });
 });
+
+describe('parseGraphQLIssueLinksToRelatedIssues', () => {
+  it('converts a graphql issue object to a related issue', () => {
+    expect(parseGraphQLIssueLinksToRelatedIssues(mockGraphQlIssueLinks)).toEqual(mockRelatedIssues);
+  });
+});
-- 
GitLab