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