diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-06-03 00:08:00 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-06-03 00:08:00 +0300 |
commit | 2164573e4531de7949b0ad9fe1d55bfb9c42765d (patch) | |
tree | 10cf954a1225eb3162009f5c2457bacdc388aa63 | |
parent | f3e7bc80608c100227030030a6a601897f8e4ff9 (diff) |
Add latest changes from gitlab-org/gitlab@master
54 files changed, 878 insertions, 321 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index e527659a939..41d59f22224 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -51,6 +51,7 @@ const Api = { pipelinesPath: '/api/:version/projects/:id/pipelines/', environmentsPath: '/api/:version/projects/:id/environments', rawFilePath: '/api/:version/projects/:id/repository/files/:path/raw', + issuePath: '/api/:version/projects/:id/issues/:issue_iid', group(groupId, callback) { const url = Api.buildUrl(Api.groupPath).replace(':id', groupId); @@ -540,6 +541,22 @@ const Api = { return axios.get(url, { params }); }, + updateIssue(project, issue, data = {}) { + const url = Api.buildUrl(Api.issuePath) + .replace(':id', encodeURIComponent(project)) + .replace(':issue_iid', encodeURIComponent(issue)); + + return axios.put(url, data); + }, + + updateMergeRequest(project, mergeRequest, data = {}) { + const url = Api.buildUrl(Api.projectMergeRequestPath) + .replace(':id', encodeURIComponent(project)) + .replace(':mrid', encodeURIComponent(mergeRequest)); + + return axios.put(url, data); + }, + buildUrl(url) { return joinPaths(gon.relative_url_root || '', url.replace(':version', gon.api_version)); }, diff --git a/app/assets/javascripts/design_management/components/design_note_pin.vue b/app/assets/javascripts/design_management/components/design_note_pin.vue index 50ea69d52ce..7827f2ce204 100644 --- a/app/assets/javascripts/design_management/components/design_note_pin.vue +++ b/app/assets/javascripts/design_management/components/design_note_pin.vue @@ -47,7 +47,7 @@ export default { 'btn-transparent comment-indicator': isNewNote, 'js-image-badge badge badge-pill': !isNewNote, }" - class="position-absolute" + class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center" type="button" @mousedown="$emit('mousedown', $event)" @mouseup="$emit('mouseup', $event)" diff --git a/app/assets/javascripts/pipelines/components/dag/dag.vue b/app/assets/javascripts/pipelines/components/dag/dag.vue index 13cbf47ad4f..3547909ac7d 100644 --- a/app/assets/javascripts/pipelines/components/dag/dag.vue +++ b/app/assets/javascripts/pipelines/components/dag/dag.vue @@ -4,7 +4,7 @@ import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; import DagGraph from './dag_graph.vue'; import { DEFAULT, PARSE_FAILURE, LOAD_FAILURE, UNSUPPORTED_DATA } from './constants'; -import { parseData } from './utils'; +import { parseData } from './parsing_utils'; export default { // eslint-disable-next-line @gitlab/require-i18n-strings diff --git a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue b/app/assets/javascripts/pipelines/components/dag/dag_graph.vue index c6187827510..d91fe9bb7f4 100644 --- a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue +++ b/app/assets/javascripts/pipelines/components/dag/dag_graph.vue @@ -3,7 +3,8 @@ import * as d3 from 'd3'; import { uniqueId } from 'lodash'; import { PARSE_FAILURE } from './constants'; -import { createSankey, getMaxNodes, removeOrphanNodes } from './utils'; +import { getMaxNodes, removeOrphanNodes } from './parsing_utils'; +import { calculateClip, createLinkPath, createSankey, labelPosition } from './drawing_utils'; export default { viewOptions: { @@ -78,7 +79,7 @@ export default { return ( link .append('path') - .attr('d', this.createLinkPath) + .attr('d', (d, i) => createLinkPath(d, i, this.$options.viewOptions.nodeWidth)) .attr('stroke', ({ gradId }) => `url(#${gradId})`) .style('stroke-linejoin', 'round') // minus two to account for the rounded nodes @@ -89,7 +90,10 @@ export default { appendLabelAsForeignObject(d, i, n) { const currentNode = n[i]; - const { height, wrapperWidth, width, x, y, textAlign } = this.labelPosition(d); + const { height, wrapperWidth, width, x, y, textAlign } = labelPosition(d, { + ...this.$options.viewOptions, + width: this.width, + }); const labelClasses = [ 'gl-display-flex', @@ -128,44 +132,13 @@ export default { }, createClip(link) { - /* - Because large link values can overrun their box, we create a clip path - to trim off the excess in charts that have few nodes per column and are - therefore tall. - - The box is created by - M: moving to outside midpoint of the source node - V: drawing a vertical line to maximum of the bottom link edge or - the lowest edge of the node (can be d.y0 or d.y1 depending on the link's path) - H: drawing a horizontal line to the outside edge of the destination node - V: drawing a vertical line back up to the minimum of the top link edge or - the highest edge of the node (can be d.y0 or d.y1 depending on the link's path) - H: drawing a horizontal line back to the outside edge of the source node - Z: closing the path, back to the start point - */ - - const clip = ({ y0, y1, source, target, width }) => { - const bottomLinkEdge = Math.max(y1, y0) + width / 2; - const topLinkEdge = Math.min(y0, y1) - width / 2; - - /* eslint-disable @gitlab/require-i18n-strings */ - return ` - M${source.x0}, ${y1} - V${Math.max(bottomLinkEdge, y0, y1)} - H${target.x1} - V${Math.min(topLinkEdge, y0, y1)} - H${source.x0} - Z`; - /* eslint-enable @gitlab/require-i18n-strings */ - }; - return link .append('clipPath') .attr('id', d => { return this.createAndAssignId(d, 'clipId', 'dag-clip'); }) .append('path') - .attr('d', clip); + .attr('d', calculateClip); }, createGradient(link) { @@ -189,44 +162,6 @@ export default { .attr('stop-color', ({ target }) => this.color(target)); }, - createLinkPath({ y0, y1, source, target, width }, idx) { - const { nodeWidth } = this.$options.viewOptions; - - /* - Creates a series of staggered midpoints for the link paths, so they - don't run along one channel and can be distinguished. - - First, get a point staggered by index and link width, modulated by the link box - to find a point roughly between the nodes. - - Then offset it by nodeWidth, so it doesn't run under any nodes at the left. - - Determine where it would overlap at the right. - - Finally, select the leftmost of these options: - - offset from the source node based on index + fudge; - - a fuzzy offset from the right node, using Math.random adds a little blur - - a hard offset from the end node, if random pushes it over - - Then draw a line from the start node to the bottom-most point of the midline - up to the topmost point in that line and then to the middle of the end node - */ - - const xValRaw = source.x1 + (((idx + 1) * width) % (target.x1 - source.x0)); - const xValMin = xValRaw + nodeWidth; - const overlapPoint = source.x1 + (target.x0 - source.x1); - const xValMax = overlapPoint - nodeWidth * 1.4; - - const midPointX = Math.min(xValMin, target.x0 - nodeWidth * 4 * Math.random(), xValMax); - - return d3.line()([ - [(source.x0 + source.x1) / 2, y0], - [midPointX, y0], - [midPointX, y1], - [(target.x0 + target.x1) / 2, y1], - ]); - }, - createLinks(svg, linksData) { const link = this.generateLinks(svg, linksData); this.createGradient(link); @@ -322,42 +257,6 @@ export default { return ({ name }) => colorFn(name); }, - labelPosition({ x0, x1, y0, y1 }) { - const { paddingForLabels, labelMargin, nodePadding } = this.$options.viewOptions; - - const firstCol = x0 <= paddingForLabels; - const lastCol = x1 >= this.width - paddingForLabels; - - if (firstCol) { - return { - x: 0 + labelMargin, - y: y0, - height: `${y1 - y0}px`, - width: paddingForLabels - 2 * labelMargin, - textAlign: 'right', - }; - } - - if (lastCol) { - return { - x: this.width - paddingForLabels + labelMargin, - y: y0, - height: `${y1 - y0}px`, - width: paddingForLabels - 2 * labelMargin, - textAlign: 'left', - }; - } - - return { - x: (x1 + x0) / 2, - y: y0 - nodePadding, - height: `${nodePadding}px`, - width: 'max-content', - wrapperWidth: paddingForLabels - 2 * labelMargin, - textAlign: x0 < this.width / 2 ? 'left' : 'right', - }; - }, - transformData(parsed) { const baseLayout = createSankey()(parsed); const cleanedNodes = removeOrphanNodes(baseLayout.nodes); diff --git a/app/assets/javascripts/pipelines/components/dag/drawing_utils.js b/app/assets/javascripts/pipelines/components/dag/drawing_utils.js new file mode 100644 index 00000000000..d56addc473f --- /dev/null +++ b/app/assets/javascripts/pipelines/components/dag/drawing_utils.js @@ -0,0 +1,134 @@ +import * as d3 from 'd3'; +import { sankey, sankeyLeft } from 'd3-sankey'; + +export const calculateClip = ({ y0, y1, source, target, width }) => { + /* + Because large link values can overrun their box, we create a clip path + to trim off the excess in charts that have few nodes per column and are + therefore tall. + + The box is created by + M: moving to outside midpoint of the source node + V: drawing a vertical line to maximum of the bottom link edge or + the lowest edge of the node (can be d.y0 or d.y1 depending on the link's path) + H: drawing a horizontal line to the outside edge of the destination node + V: drawing a vertical line back up to the minimum of the top link edge or + the highest edge of the node (can be d.y0 or d.y1 depending on the link's path) + H: drawing a horizontal line back to the outside edge of the source node + Z: closing the path, back to the start point + */ + + const bottomLinkEdge = Math.max(y1, y0) + width / 2; + const topLinkEdge = Math.min(y0, y1) - width / 2; + + /* eslint-disable @gitlab/require-i18n-strings */ + return ` + M${source.x0}, ${y1} + V${Math.max(bottomLinkEdge, y0, y1)} + H${target.x1} + V${Math.min(topLinkEdge, y0, y1)} + H${source.x0} + Z + `; + /* eslint-enable @gitlab/require-i18n-strings */ +}; + +export const createLinkPath = ({ y0, y1, source, target, width }, idx, nodeWidth) => { + /* + Creates a series of staggered midpoints for the link paths, so they + don't run along one channel and can be distinguished. + + First, get a point staggered by index and link width, modulated by the link box + to find a point roughly between the nodes. + + Then offset it by nodeWidth, so it doesn't run under any nodes at the left. + + Determine where it would overlap at the right. + + Finally, select the leftmost of these options: + - offset from the source node based on index + fudge; + - a fuzzy offset from the right node, using Math.random adds a little blur + - a hard offset from the end node, if random pushes it over + + Then draw a line from the start node to the bottom-most point of the midline + up to the topmost point in that line and then to the middle of the end node + */ + + const xValRaw = source.x1 + (((idx + 1) * width) % (target.x1 - source.x0)); + const xValMin = xValRaw + nodeWidth; + const overlapPoint = source.x1 + (target.x0 - source.x1); + const xValMax = overlapPoint - nodeWidth * 1.4; + + const midPointX = Math.min(xValMin, target.x0 - nodeWidth * 4 * Math.random(), xValMax); + + return d3.line()([ + [(source.x0 + source.x1) / 2, y0], + [midPointX, y0], + [midPointX, y1], + [(target.x0 + target.x1) / 2, y1], + ]); +}; + +/* + createSankey calls the d3 layout to generate the relationships and positioning + values for the nodes and links in the graph. + */ + +export const createSankey = ({ + width = 10, + height = 10, + nodeWidth = 10, + nodePadding = 10, + paddingForLabels = 1, +} = {}) => { + const sankeyGenerator = sankey() + .nodeId(({ name }) => name) + .nodeAlign(sankeyLeft) + .nodeWidth(nodeWidth) + .nodePadding(nodePadding) + .extent([ + [paddingForLabels, paddingForLabels], + [width - paddingForLabels, height - paddingForLabels], + ]); + return ({ nodes, links }) => + sankeyGenerator({ + nodes: nodes.map(d => ({ ...d })), + links: links.map(d => ({ ...d })), + }); +}; + +export const labelPosition = ({ x0, x1, y0, y1 }, viewOptions) => { + const { paddingForLabels, labelMargin, nodePadding, width } = viewOptions; + + const firstCol = x0 <= paddingForLabels; + const lastCol = x1 >= width - paddingForLabels; + + if (firstCol) { + return { + x: 0 + labelMargin, + y: y0, + height: `${y1 - y0}px`, + width: paddingForLabels - 2 * labelMargin, + textAlign: 'right', + }; + } + + if (lastCol) { + return { + x: width - paddingForLabels + labelMargin, + y: y0, + height: `${y1 - y0}px`, + width: paddingForLabels - 2 * labelMargin, + textAlign: 'left', + }; + } + + return { + x: (x1 + x0) / 2, + y: y0 - nodePadding, + height: `${nodePadding}px`, + width: 'max-content', + wrapperWidth: paddingForLabels - 2 * labelMargin, + textAlign: x0 < width / 2 ? 'left' : 'right', + }; +}; diff --git a/app/assets/javascripts/pipelines/components/dag/utils.js b/app/assets/javascripts/pipelines/components/dag/parsing_utils.js index 76cfd75f9fd..3234f80ee91 100644 --- a/app/assets/javascripts/pipelines/components/dag/utils.js +++ b/app/assets/javascripts/pipelines/components/dag/parsing_utils.js @@ -1,4 +1,3 @@ -import { sankey, sankeyLeft } from 'd3-sankey'; import { uniqWith, isEqual } from 'lodash'; /* @@ -137,34 +136,6 @@ export const parseData = data => { }; /* - createSankey calls the d3 layout to generate the relationships and positioning - values for the nodes and links in the graph. - */ - -export const createSankey = ({ - width = 10, - height = 10, - nodeWidth = 10, - nodePadding = 10, - paddingForLabels = 1, -} = {}) => { - const sankeyGenerator = sankey() - .nodeId(({ name }) => name) - .nodeAlign(sankeyLeft) - .nodeWidth(nodeWidth) - .nodePadding(nodePadding) - .extent([ - [paddingForLabels, paddingForLabels], - [width - paddingForLabels, height - paddingForLabels], - ]); - return ({ nodes, links }) => - sankeyGenerator({ - nodes: nodes.map(d => ({ ...d })), - links: links.map(d => ({ ...d })), - }); -}; - -/* The number of nodes in the most populous generation drives the height of the graph. */ diff --git a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue index 0f7a0e60dc0..a670cad5f9f 100644 --- a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue +++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue @@ -98,25 +98,27 @@ export default { :has-issues="reports.length > 0" class="mr-widget-section grouped-security-reports mr-report" > - <div slot="body" class="mr-widget-grouped-section report-block"> - <template v-for="(report, i) in reports"> - <summary-row - :key="`summary-row-${i}`" - :summary="reportText(report)" - :status-icon="getReportIcon(report)" - /> - <issues-list - v-if="shouldRenderIssuesList(report)" - :key="`issues-list-${i}`" - :unresolved-issues="unresolvedIssues(report)" - :new-issues="newIssues(report)" - :resolved-issues="resolvedIssues(report)" - :component="$options.componentNames.TestIssueBody" - class="report-block-group-list" - /> - </template> + <template #body> + <div class="mr-widget-grouped-section report-block"> + <template v-for="(report, i) in reports"> + <summary-row + :key="`summary-row-${i}`" + :summary="reportText(report)" + :status-icon="getReportIcon(report)" + /> + <issues-list + v-if="shouldRenderIssuesList(report)" + :key="`issues-list-${i}`" + :unresolved-issues="unresolvedIssues(report)" + :new-issues="newIssues(report)" + :resolved-issues="resolvedIssues(report)" + :component="$options.componentNames.TestIssueBody" + class="report-block-group-list" + /> + </template> - <modal :title="modalTitle" :modal-data="modalData" /> - </div> + <modal :title="modalTitle" :modal-data="modalData" /> + </div> + </template> </report-section> </template> diff --git a/app/assets/stylesheets/components/avatar.scss b/app/assets/stylesheets/components/avatar.scss index 312123aeef9..6bb7e9d215e 100644 --- a/app/assets/stylesheets/components/avatar.scss +++ b/app/assets/stylesheets/components/avatar.scss @@ -70,7 +70,7 @@ $avatar-sizes: ( $identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $identicon-blue, $identicon-teal, $identicon-orange, $gray-darker; -.avatar-circle { +%avatar-circle { float: left; margin-right: $gl-padding; border-radius: $avatar-radius; @@ -84,7 +84,7 @@ $identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $i } .avatar { - @extend .avatar-circle; + @extend %avatar-circle; transition-property: none; width: 40px; @@ -100,10 +100,7 @@ $identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $i margin-left: 2px; flex-shrink: 0; - &.s16 { - margin-right: 4px; - } - + &.s16, &.s24 { margin-right: 4px; } @@ -154,7 +151,7 @@ $identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $i } .avatar-container { - @extend .avatar-circle; + @extend %avatar-circle; overflow: hidden; display: flex; diff --git a/app/assets/stylesheets/components/design_management/design.scss b/app/assets/stylesheets/components/design_management/design.scss index 0b5c32021b8..8a83a1612ff 100644 --- a/app/assets/stylesheets/components/design_management/design.scss +++ b/app/assets/stylesheets/components/design_management/design.scss @@ -9,8 +9,16 @@ top: 35px; } - .inactive { - opacity: 0.5; + .design-pin { + transition: opacity 0.5s ease; + + &.inactive { + @include gl-opacity-5; + + &:hover { + @include gl-opacity-10; + } + } } } diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index 566ab5bc5c4..136ff82e0f8 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -93,7 +93,6 @@ } .dropdown-menu-toggle, -.avatar-circle, .header-user-avatar { @include transition(border-color); } diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 92c6ce324f7..5ed9baf04f6 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -2,6 +2,7 @@ class Projects::ServicesController < Projects::ApplicationController include ServiceParams + include InternalRedirect # Authorize before_action :authorize_admin_project! @@ -26,8 +27,8 @@ class Projects::ServicesController < Projects::ApplicationController respond_to do |format| format.html do if saved - redirect_to project_settings_integrations_path(@project), - notice: success_message + target_url = safe_redirect_path(params[:redirect_to]).presence || project_settings_integrations_path(@project) + redirect_to target_url, notice: success_message else render 'edit' end diff --git a/app/graphql/types/release_link_type.rb b/app/graphql/types/release_link_type.rb index c7a1d3f3767..070f14a90df 100644 --- a/app/graphql/types/release_link_type.rb +++ b/app/graphql/types/release_link_type.rb @@ -12,7 +12,8 @@ module Types description: 'Name of the link' field :url, GraphQL::STRING_TYPE, null: true, description: 'URL of the link' - + field :link_type, Types::ReleaseLinkTypeEnum, null: true, + description: 'Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`' field :external, GraphQL::BOOLEAN_TYPE, null: true, method: :external?, description: 'Indicates the link points to an external resource' end diff --git a/app/graphql/types/release_link_type_enum.rb b/app/graphql/types/release_link_type_enum.rb new file mode 100644 index 00000000000..b364855833f --- /dev/null +++ b/app/graphql/types/release_link_type_enum.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Types + class ReleaseLinkTypeEnum < BaseEnum + graphql_name 'ReleaseLinkType' + description 'Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`' + + ::Releases::Link.link_types.keys.each do |link_type| + value link_type.upcase, value: link_type, description: "#{link_type.titleize} link type" + end + end +end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index e1d53a700ec..8d4fe633aa4 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -688,6 +688,10 @@ module Ci job_artifacts.any? end + def has_test_reports? + job_artifacts.test_reports.exists? + end + def has_old_trace? old_trace.present? end diff --git a/app/models/ci/build_report_result.rb b/app/models/ci/build_report_result.rb index a65ef885b88..530233ad5c0 100644 --- a/app/models/ci/build_report_result.rb +++ b/app/models/ci/build_report_result.rb @@ -11,5 +11,35 @@ module Ci validates :build, :project, presence: true validates :data, json_schema: { filename: "build_report_result_data" } + + store_accessor :data, :tests + + def tests_name + tests.dig("name") + end + + def tests_duration + tests.dig("duration") + end + + def tests_success + tests.dig("success").to_i + end + + def tests_failed + tests.dig("failed").to_i + end + + def tests_errored + tests.dig("errored").to_i + end + + def tests_skipped + tests.dig("skipped").to_i + end + + def tests_total + [tests_success, tests_failed, tests_errored, tests_skipped].sum + end end end diff --git a/app/services/ci/build_report_result_service.rb b/app/services/ci/build_report_result_service.rb new file mode 100644 index 00000000000..758ba1c73bf --- /dev/null +++ b/app/services/ci/build_report_result_service.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Ci + class BuildReportResultService + def execute(build) + return unless Feature.enabled?(:build_report_summary, build.project) + return unless build.has_test_reports? + + build.report_results.create!( + project_id: build.project_id, + data: tests_params(build) + ) + end + + private + + def generate_test_suite_report(build) + build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new) + end + + def tests_params(build) + test_suite = generate_test_suite_report(build) + + { + tests: { + name: test_suite.name, + duration: test_suite.total_time, + failed: test_suite.failed_count, + errored: test_suite.error_count, + skipped: test_suite.skipped_count, + success: test_suite.success_count + } + } + end + end +end diff --git a/app/services/concerns/measurable.rb b/app/services/concerns/measurable.rb index 5a74f15506e..b099a58a9ae 100644 --- a/app/services/concerns/measurable.rb +++ b/app/services/concerns/measurable.rb @@ -4,8 +4,6 @@ # Example: # ``` # class DummyService -# prepend Measurable -# # def execute # # ... # end diff --git a/app/validators/json_schemas/build_report_result_data.json b/app/validators/json_schemas/build_report_result_data.json index b364266b9d6..0fb4fd6d0b7 100644 --- a/app/validators/json_schemas/build_report_result_data.json +++ b/app/validators/json_schemas/build_report_result_data.json @@ -3,9 +3,9 @@ "type": "object", "properties": { "coverage": { "type": "float" }, - "junit": { + "tests": { "type": "object", - "items": { "$ref": "./build_report_result_data_junit.json" } + "items": { "$ref": "./build_report_result_data_tests.json" } } }, "additionalProperties": false diff --git a/app/validators/json_schemas/build_report_result_data_junit.json b/app/validators/json_schemas/build_report_result_data_tests.json index f69cbd4f16d..b38559e727f 100644 --- a/app/validators/json_schemas/build_report_result_data_junit.json +++ b/app/validators/json_schemas/build_report_result_data_tests.json @@ -1,5 +1,5 @@ { - "description": "Build report result data junit", + "description": "Build report result data tests", "type": "object", "properties": { "name": { "type": "string" }, diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index 05c49a7b6ce..e6761807409 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -13,6 +13,7 @@ = form_for(@service, as: :service, url: scoped_integration_path(@service), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_project_service_path(@project, @service) } }) do |form| = render 'shared/service_settings', form: form, service: @service .footer-block.row-content-block + %input{ id: 'services_redirect_to', type: 'hidden', name: 'redirect_to', value: request.referrer } = service_save_button = link_to _('Cancel'), project_settings_integrations_path(@project), class: 'btn btn-cancel' diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 0dded4d6474..8f3f14c75f0 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -803,6 +803,14 @@ :weight: 1 :idempotent: :tags: [] +- :name: pipeline_background:ci_build_report_result + :feature_category: :continuous_integration + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: pipeline_background:ci_build_trace_chunk_flush :feature_category: :continuous_integration :has_external_dependencies: diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb index b6ef9ab4710..d38780dd08d 100644 --- a/app/workers/build_finished_worker.rb +++ b/app/workers/build_finished_worker.rb @@ -28,6 +28,7 @@ class BuildFinishedWorker # rubocop:disable Scalability/IdempotentWorker # We execute these in sync to reduce IO. BuildTraceSectionsWorker.new.perform(build.id) BuildCoverageWorker.new.perform(build.id) + Ci::BuildReportResultWorker.new.perform(build.id) # We execute these async as these are independent operations. BuildHooksWorker.perform_async(build.id) diff --git a/app/workers/ci/build_report_result_worker.rb b/app/workers/ci/build_report_result_worker.rb new file mode 100644 index 00000000000..60387936d0b --- /dev/null +++ b/app/workers/ci/build_report_result_worker.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Ci + class BuildReportResultWorker + include ApplicationWorker + include PipelineBackgroundQueue + + idempotent! + + def perform(build_id) + Ci::Build.find_by_id(build_id).try do |build| + Ci::BuildReportResultService.new.execute(build) + end + end + end +end diff --git a/bin/secpick b/bin/secpick index a68dabc8c47..4d056ceecaf 100755 --- a/bin/secpick +++ b/bin/secpick @@ -21,10 +21,6 @@ module Secpick @options = self.class.options end - def ee? - File.exist?(File.expand_path('../ee/app/models/license.rb', __dir__)) - end - def dry_run? @options[:try] == true end @@ -40,9 +36,7 @@ module Secpick end def stable_branch - "#{@options[:version]}-#{STABLE_SUFFIX}".tap do |name| - name << "-ee" if ee? - end.freeze + "#{@options[:version]}-#{STABLE_SUFFIX}-ee".freeze end def git_commands @@ -64,11 +58,7 @@ module Secpick end def new_mr_url - if ee? - SECURITY_MR_URL - else - SECURITY_MR_URL.sub('/gitlab/', '/gitlab-foss/') - end + SECURITY_MR_URL end def create! diff --git a/changelogs/unreleased/191455-add-a-button-to-quickly-assign-users-who-have-commented-on-an-issu.yml b/changelogs/unreleased/191455-add-a-button-to-quickly-assign-users-who-have-commented-on-an-issu.yml new file mode 100644 index 00000000000..df303695865 --- /dev/null +++ b/changelogs/unreleased/191455-add-a-button-to-quickly-assign-users-who-have-commented-on-an-issu.yml @@ -0,0 +1,5 @@ +--- +title: Add api.js methods to update issues and merge requests +merge_request: 32893 +author: +type: added diff --git a/changelogs/unreleased/207257-specify-asset-types-in-releases-2.yml b/changelogs/unreleased/207257-specify-asset-types-in-releases-2.yml new file mode 100644 index 00000000000..f897faeeaa2 --- /dev/null +++ b/changelogs/unreleased/207257-specify-asset-types-in-releases-2.yml @@ -0,0 +1,5 @@ +--- +title: Expose `release_links.type` via API +merge_request: 33154 +author: +type: changed diff --git a/changelogs/unreleased/207257-specify-asset-types-in-releases-3.yml b/changelogs/unreleased/207257-specify-asset-types-in-releases-3.yml new file mode 100644 index 00000000000..bf5de47e7ff --- /dev/null +++ b/changelogs/unreleased/207257-specify-asset-types-in-releases-3.yml @@ -0,0 +1,5 @@ +--- +title: Add `link_type` to `ReleaseLink` GraphQL type +merge_request: 33386 +author: +type: added diff --git a/changelogs/unreleased/217692-design-view-highlight-focused-design-pins-follow-up.yml b/changelogs/unreleased/217692-design-view-highlight-focused-design-pins-follow-up.yml new file mode 100644 index 00000000000..27b6d0dd58c --- /dev/null +++ b/changelogs/unreleased/217692-design-view-highlight-focused-design-pins-follow-up.yml @@ -0,0 +1,5 @@ +--- +title: Add opacity transition to active design discussion pins +merge_request: 33493 +author: +type: other diff --git a/changelogs/unreleased/update-deprecated-slot-syntax-in-app-assets-javascripts-reports-component.yml b/changelogs/unreleased/update-deprecated-slot-syntax-in-app-assets-javascripts-reports-component.yml new file mode 100644 index 00000000000..b880020aa7f --- /dev/null +++ b/changelogs/unreleased/update-deprecated-slot-syntax-in-app-assets-javascripts-reports-component.yml @@ -0,0 +1,5 @@ +--- +title: Update deprecated slot syntax in app/assets/javascripts/reports/components/grouped_test_reports_app.vue +merge_request: 31975 +author: Gilang Gumilar +type: other diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index c3fd02d8c9c..626b99eeecb 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -9649,6 +9649,11 @@ type ReleaseLink { id: ID! """ + Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other` + """ + linkType: ReleaseLinkType + + """ Name of the link """ name: String @@ -9694,6 +9699,31 @@ type ReleaseLinkEdge { node: ReleaseLink } +""" +Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other` +""" +enum ReleaseLinkType { + """ + Image link type + """ + IMAGE + + """ + Other link type + """ + OTHER + + """ + Package link type + """ + PACKAGE + + """ + Runbook link type + """ + RUNBOOK +} + type ReleaseSource { """ Format of the source diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 2ac543cb14a..dc1e909384e 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -28195,6 +28195,20 @@ "deprecationReason": null }, { + "name": "linkType", + "description": "Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`", + "args": [ + + ], + "type": { + "kind": "ENUM", + "name": "ReleaseLinkType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "name", "description": "Name of the link", "args": [ @@ -28343,6 +28357,41 @@ "possibleTypes": null }, { + "kind": "ENUM", + "name": "ReleaseLinkType", + "description": "Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "OTHER", + "description": "Other link type", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "RUNBOOK", + "description": "Runbook link type", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "PACKAGE", + "description": "Package link type", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "IMAGE", + "description": "Image link type", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { "kind": "OBJECT", "name": "ReleaseSource", "description": null, diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 452d0059cca..6a6427948dc 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1342,6 +1342,7 @@ Information about pagination in a connection. | --- | ---- | ---------- | | `external` | Boolean | Indicates the link points to an external resource | | `id` | ID! | ID of the link | +| `linkType` | ReleaseLinkType | Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other` | | `name` | String | Name of the link | | `url` | String | URL of the link | diff --git a/doc/development/performance.md b/doc/development/performance.md index 050444c57af..69ad524675d 100644 --- a/doc/development/performance.md +++ b/doc/development/performance.md @@ -42,6 +42,7 @@ GitLab provides built-in tools to help improve performance and availability: - [Request Profiling](../administration/monitoring/performance/request_profiling.md). - [QueryRecoder](query_recorder.md) for preventing `N+1` regressions. - [Chaos endpoints](chaos_endpoints.md) for testing failure scenarios. Intended mainly for testing availability. +- [Service measurement](service_measurement.md) for measuring and logging service execution. GitLab team members can use [GitLab.com's performance monitoring systems](https://about.gitlab.com/handbook/engineering/monitoring/) located at <https://dashboards.gitlab.net>, this requires you to log in using your diff --git a/doc/development/service_measurement.md b/doc/development/service_measurement.md new file mode 100644 index 00000000000..bc3f3534cc6 --- /dev/null +++ b/doc/development/service_measurement.md @@ -0,0 +1,81 @@ +# GitLab Developers Guide to service measurement + +You can enable service measurement in order to debug any slow service's execution time, number of SQL calls, garbage collection stats, memory usage, etc. + +## Measuring module + +The measuring module is a tool that allows to measure a service's execution, and log: + +- Service class name +- Execution time +- Number of sql calls +- Detailed gc stats and diffs +- RSS memory usage +- Server worker ID + +The measuring module will log these measurements into a structured log called [`service_measurement.log`](../administration/logs.md#service_measurementlog), +as a single entry for each service execution. + +NOTE: **Note:** +For GitLab.com, `service_measurement.log` is ingested in Elasticsearch and Kibana as part of our monitoring solution. + +## How to use it + +The measuring module allows you to easily measure and log execution of any service, +by just prepending `Measurable` in any Service class, on the last line of the file that the class resides in. + +For example, to prepend a module into the `DummyService` class, you would use the following approach: + +```ruby +class DummyService + def execute + # ... + end +end + +DummyService.prepend(Measurable) +``` + +In case when you are prepending a module from the `EE` namespace with EE features, you need to prepend Measurable after prepending the `EE` module. + +This way, `Measurable` will be at the bottom of the ancestor chain, in order to measure execution of `EE` features as well: + +```ruby +class DummyService + def execute + # ... + end +end + +DummyService.prepend_if_ee('EE::DummyService') +DummyService.prepend(Measurable) +``` + +### Log additional attributes + +In case you need to log some additional attributes, it is possible to define `extra_attributes_for_measurement` in the service class: + +```ruby +def extra_attributes_for_measurement + { + project_path: @project.full_path, + user: current_user.name + } +end +``` + +NOTE: **Note:** +Once the measurement module is injected in the service, it will be behind generic feature flag. +In order to actually use it, you need to enable measuring for the desired service by enabling the feature flag. + +### Enabling measurement using feature flags + +In the following example, the `:gitlab_service_measuring_projects_import_service` +[feature flag](feature_flags/development.md#enabling-a-feature-flag-in-development) is used to enable the measuring feature +for `Projects::ImportService`. + +From chatops: + +```shell +/chatops run feature set gitlab_service_measuring_projects_import_service true +``` diff --git a/lib/api/entities/releases/link.rb b/lib/api/entities/releases/link.rb index f4edb83bd58..654df2e2caf 100644 --- a/lib/api/entities/releases/link.rb +++ b/lib/api/entities/releases/link.rb @@ -9,6 +9,7 @@ module API expose :url expose :direct_asset_url expose :external?, as: :external + expose :link_type def direct_asset_url return object.url unless object.filepath diff --git a/lib/api/release/links.rb b/lib/api/release/links.rb index f72230c084c..07c27f39539 100644 --- a/lib/api/release/links.rb +++ b/lib/api/release/links.rb @@ -40,6 +40,7 @@ module API requires :name, type: String, desc: 'The name of the link' requires :url, type: String, desc: 'The URL of the link' optional :filepath, type: String, desc: 'The filepath of the link' + optional :link_type, type: String, desc: 'The link type' end post 'links' do authorize! :create_release, release @@ -75,6 +76,7 @@ module API optional :name, type: String, desc: 'The name of the link' optional :url, type: String, desc: 'The URL of the link' optional :filepath, type: String, desc: 'The filepath of the link' + optional :link_type, type: String, desc: 'The link type' at_least_one_of :name, :url end put do diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb index c669119fa4e..b591e52d45c 100644 --- a/spec/controllers/projects/services_controller_spec.rb +++ b/spec/controllers/projects/services_controller_spec.rb @@ -134,24 +134,50 @@ describe Projects::ServicesController do describe 'PUT #update' do describe 'as HTML' do let(:service_params) { { active: true } } + let(:params) { project_params(service: service_params) } + + let(:message) { 'Jira activated.' } + let(:redirect_url) { project_settings_integrations_path(project) } before do - put :update, params: project_params(service: service_params) + put :update, params: params + end + + shared_examples 'service update' do + it 'redirects to the correct url with a flash message' do + expect(response).to redirect_to(redirect_url) + expect(flash[:notice]).to eq(message) + end end context 'when param `active` is set to true' do - it 'activates the service and redirects to integrations paths' do - expect(response).to redirect_to(project_settings_integrations_path(project)) - expect(flash[:notice]).to eq 'Jira activated.' + let(:params) { project_params(service: service_params, redirect_to: redirect) } + + context 'when redirect_to param is present' do + let(:redirect) { '/redirect_here' } + let(:redirect_url) { redirect } + + it_behaves_like 'service update' + end + + context 'when redirect_to is an external domain' do + let(:redirect) { 'http://examle.com' } + + it_behaves_like 'service update' + end + + context 'when redirect_to param is an empty string' do + let(:redirect) { '' } + + it_behaves_like 'service update' end end context 'when param `active` is set to false' do let(:service_params) { { active: false } } + let(:message) { 'Jira settings saved, but not activated.' } - it 'does not activate the service but saves the settings' do - expect(flash[:notice]).to eq 'Jira settings saved, but not activated.' - end + it_behaves_like 'service update' end end diff --git a/spec/factories/ci/build_report_results.rb b/spec/factories/ci/build_report_results.rb index 00009ead126..0685c0e5554 100644 --- a/spec/factories/ci/build_report_results.rb +++ b/spec/factories/ci/build_report_results.rb @@ -6,7 +6,7 @@ FactoryBot.define do project factory: :project data do { - junit: { + tests: { name: "rspec", duration: 0.42, failed: 0, @@ -20,7 +20,7 @@ FactoryBot.define do trait :with_junit_success do data do { - junit: { + tests: { name: "rspec", duration: 0.42, failed: 0, diff --git a/spec/factories/releases/link.rb b/spec/factories/releases/link.rb index 001deeb71a0..da0efe4a749 100644 --- a/spec/factories/releases/link.rb +++ b/spec/factories/releases/link.rb @@ -6,5 +6,6 @@ FactoryBot.define do sequence(:name) { |n| "release-18.#{n}.dmg" } sequence(:url) { |n| "https://example.com/scrambled-url/app-#{n}.zip" } sequence(:filepath) { |n| "/binaries/awesome-app-#{n}" } + link_type { 'other' } end end diff --git a/spec/fixtures/api/schemas/release/link.json b/spec/fixtures/api/schemas/release/link.json index bf175be2bc0..b3aebfa131e 100644 --- a/spec/fixtures/api/schemas/release/link.json +++ b/spec/fixtures/api/schemas/release/link.json @@ -7,7 +7,8 @@ "filepath": { "type": "string" }, "url": { "type": "string" }, "direct_asset_url": { "type": "string" }, - "external": { "type": "boolean" } + "external": { "type": "boolean" }, + "link_type": { "type": "string" } }, "additionalProperties": false } diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js index d365048ab0b..d3659693405 100644 --- a/spec/frontend/api_spec.js +++ b/spec/frontend/api_spec.js @@ -691,4 +691,38 @@ describe('Api', () => { }); }); }); + + describe('updateIssue', () => { + it('update an issue with the given payload', done => { + const projectId = 8; + const issue = 1; + const expectedArray = [1, 2, 3]; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/issues/${issue}`; + mock.onPut(expectedUrl).reply(200, { assigneeIds: expectedArray }); + + Api.updateIssue(projectId, issue, { assigneeIds: expectedArray }) + .then(({ data }) => { + expect(data.assigneeIds).toEqual(expectedArray); + done(); + }) + .catch(done.fail); + }); + }); + + describe('updateMergeRequest', () => { + it('update an issue with the given payload', done => { + const projectId = 8; + const mergeRequest = 1; + const expectedArray = [1, 2, 3]; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/merge_requests/${mergeRequest}`; + mock.onPut(expectedUrl).reply(200, { assigneeIds: expectedArray }); + + Api.updateMergeRequest(projectId, mergeRequest, { assigneeIds: expectedArray }) + .then(({ data }) => { + expect(data.assigneeIds).toEqual(expectedArray); + done(); + }) + .catch(done.fail); + }); + }); }); diff --git a/spec/frontend/design_management/components/__snapshots__/design_note_pin_spec.js.snap b/spec/frontend/design_management/components/__snapshots__/design_note_pin_spec.js.snap index 4828e8cb3c2..4c848256e5b 100644 --- a/spec/frontend/design_management/components/__snapshots__/design_note_pin_spec.js.snap +++ b/spec/frontend/design_management/components/__snapshots__/design_note_pin_spec.js.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Design discussions component should match the snapshot of note when repositioning 1`] = ` +exports[`Design note pin component should match the snapshot of note when repositioning 1`] = ` <button aria-label="Comment form position" - class="position-absolute btn-transparent comment-indicator" + class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center btn-transparent comment-indicator" style="left: 10px; top: 10px; cursor: move;" type="button" > @@ -14,10 +14,10 @@ exports[`Design discussions component should match the snapshot of note when rep </button> `; -exports[`Design discussions component should match the snapshot of note with index 1`] = ` +exports[`Design note pin component should match the snapshot of note with index 1`] = ` <button aria-label="Comment '1' position" - class="position-absolute js-image-badge badge badge-pill" + class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center js-image-badge badge badge-pill" style="left: 10px; top: 10px;" type="button" > @@ -27,10 +27,10 @@ exports[`Design discussions component should match the snapshot of note with ind </button> `; -exports[`Design discussions component should match the snapshot of note without index 1`] = ` +exports[`Design note pin component should match the snapshot of note without index 1`] = ` <button aria-label="Comment form position" - class="position-absolute btn-transparent comment-indicator" + class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center btn-transparent comment-indicator" style="left: 10px; top: 10px;" type="button" > diff --git a/spec/frontend/design_management/components/design_note_pin_spec.js b/spec/frontend/design_management/components/design_note_pin_spec.js index 4f7260b1363..76fe5402053 100644 --- a/spec/frontend/design_management/components/design_note_pin_spec.js +++ b/spec/frontend/design_management/components/design_note_pin_spec.js @@ -1,7 +1,7 @@ import { shallowMount } from '@vue/test-utils'; import DesignNotePin from '~/design_management/components/design_note_pin.vue'; -describe('Design discussions component', () => { +describe('Design note pin component', () => { let wrapper; function createComponent(propsData = {}) { diff --git a/spec/frontend/pipelines/components/dag/__snapshots__/dag_graph_spec.js.snap b/spec/frontend/pipelines/components/dag/__snapshots__/dag_graph_spec.js.snap index 5390c2f8e0c..629efc6d3fa 100644 --- a/spec/frontend/pipelines/components/dag/__snapshots__/dag_graph_spec.js.snap +++ b/spec/frontend/pipelines/components/dag/__snapshots__/dag_graph_spec.js.snap @@ -10,12 +10,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = ` </linearGradient> <clipPath id=\\"dag-clip63\\"> <path d=\\" - M100, 129 - V158 - H377.3333333333333 - V100 - H100 - Z\\"></path> + M100, 129 + V158 + H377.3333333333333 + V100 + H100 + Z + \\"></path> </clipPath> <path d=\\"M108,129L190,129L190,129L369.3333333333333,129\\" stroke=\\"url(#dag-grad53)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip63)\\"></path> </g> @@ -26,12 +27,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = ` </linearGradient> <clipPath id=\\"dag-clip64\\"> <path d=\\" - M361.3333333333333, 129.0000000000002 - V158.0000000000002 - H638.6666666666666 - V100 - H361.3333333333333 - Z\\"></path> + M361.3333333333333, 129.0000000000002 + V158.0000000000002 + H638.6666666666666 + V100 + H361.3333333333333 + Z + \\"></path> </clipPath> <path d=\\"M369.3333333333333,129L509.3333333333333,129L509.3333333333333,129.0000000000002L630.6666666666666,129.0000000000002\\" stroke=\\"url(#dag-grad54)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip64)\\"></path> </g> @@ -42,12 +44,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = ` </linearGradient> <clipPath id=\\"dag-clip65\\"> <path d=\\" - M100, 187.0000000000002 - V241.00000000000003 - H638.6666666666666 - V158.0000000000002 - H100 - Z\\"></path> + M100, 187.0000000000002 + V241.00000000000003 + H638.6666666666666 + V158.0000000000002 + H100 + Z + \\"></path> </clipPath> <path d=\\"M108,212.00000000000003L306,212.00000000000003L306,187.0000000000002L630.6666666666666,187.0000000000002\\" stroke=\\"url(#dag-grad55)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip65)\\"></path> </g> @@ -58,12 +61,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = ` </linearGradient> <clipPath id=\\"dag-clip66\\"> <path d=\\" - M100, 269.9999999999998 - V324 - H377.3333333333333 - V240.99999999999977 - H100 - Z\\"></path> + M100, 269.9999999999998 + V324 + H377.3333333333333 + V240.99999999999977 + H100 + Z + \\"></path> </clipPath> <path d=\\"M108,295L338.93333333333334,295L338.93333333333334,269.9999999999998L369.3333333333333,269.9999999999998\\" stroke=\\"url(#dag-grad56)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip66)\\"></path> </g> @@ -74,12 +78,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = ` </linearGradient> <clipPath id=\\"dag-clip67\\"> <path d=\\" - M100, 352.99999999999994 - V407.00000000000006 - H377.3333333333333 - V323.99999999999994 - H100 - Z\\"></path> + M100, 352.99999999999994 + V407.00000000000006 + H377.3333333333333 + V323.99999999999994 + H100 + Z + \\"></path> </clipPath> <path d=\\"M108,378.00000000000006L144.66666666666669,378.00000000000006L144.66666666666669,352.99999999999994L369.3333333333333,352.99999999999994\\" stroke=\\"url(#dag-grad57)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip67)\\"></path> </g> @@ -90,12 +95,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = ` </linearGradient> <clipPath id=\\"dag-clip68\\"> <path d=\\" - M361.3333333333333, 270.0000000000001 - V299.0000000000001 - H638.6666666666666 - V240.99999999999977 - H361.3333333333333 - Z\\"></path> + M361.3333333333333, 270.0000000000001 + V299.0000000000001 + H638.6666666666666 + V240.99999999999977 + H361.3333333333333 + Z + \\"></path> </clipPath> <path d=\\"M369.3333333333333,269.9999999999998L464,269.9999999999998L464,270.0000000000001L630.6666666666666,270.0000000000001\\" stroke=\\"url(#dag-grad58)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip68)\\"></path> </g> @@ -106,12 +112,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = ` </linearGradient> <clipPath id=\\"dag-clip69\\"> <path d=\\" - M361.3333333333333, 328.0000000000001 - V381.99999999999994 - H638.6666666666666 - V299.0000000000001 - H361.3333333333333 - Z\\"></path> + M361.3333333333333, 328.0000000000001 + V381.99999999999994 + H638.6666666666666 + V299.0000000000001 + H361.3333333333333 + Z + \\"></path> </clipPath> <path d=\\"M369.3333333333333,352.99999999999994L522,352.99999999999994L522,328.0000000000001L630.6666666666666,328.0000000000001\\" stroke=\\"url(#dag-grad59)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip69)\\"></path> </g> @@ -122,12 +129,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = ` </linearGradient> <clipPath id=\\"dag-clip70\\"> <path d=\\" - M361.3333333333333, 411 - V440 - H638.6666666666666 - V381.99999999999994 - H361.3333333333333 - Z\\"></path> + M361.3333333333333, 411 + V440 + H638.6666666666666 + V381.99999999999994 + H361.3333333333333 + Z + \\"></path> </clipPath> <path d=\\"M369.3333333333333,410.99999999999994L580,410.99999999999994L580,411L630.6666666666666,411\\" stroke=\\"url(#dag-grad60)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip70)\\"></path> </g> @@ -138,12 +146,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = ` </linearGradient> <clipPath id=\\"dag-clip71\\"> <path d=\\" - M622.6666666666666, 270.1890725105691 - V299.1890725105691 - H900 - V241.0000000000001 - H622.6666666666666 - Z\\"></path> + M622.6666666666666, 270.1890725105691 + V299.1890725105691 + H900 + V241.0000000000001 + H622.6666666666666 + Z + \\"></path> </clipPath> <path d=\\"M630.6666666666666,270.0000000000001L861.6,270.0000000000001L861.6,270.1890725105691L892,270.1890725105691\\" stroke=\\"url(#dag-grad61)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip71)\\"></path> </g> @@ -154,12 +163,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = ` </linearGradient> <clipPath id=\\"dag-clip72\\"> <path d=\\" - M622.6666666666666, 411 - V440 - H900 - V382 - H622.6666666666666 - Z\\"></path> + M622.6666666666666, 411 + V440 + H900 + V382 + H622.6666666666666 + Z + \\"></path> </clipPath> <path d=\\"M630.6666666666666,411L679.9999999999999,411L679.9999999999999,411L892,411\\" stroke=\\"url(#dag-grad62)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip72)\\"></path> </g> diff --git a/spec/frontend/pipelines/components/dag/dag_graph_spec.js b/spec/frontend/pipelines/components/dag/dag_graph_spec.js index bc576397967..a6f712b1984 100644 --- a/spec/frontend/pipelines/components/dag/dag_graph_spec.js +++ b/spec/frontend/pipelines/components/dag/dag_graph_spec.js @@ -1,6 +1,7 @@ import { mount } from '@vue/test-utils'; import DagGraph from '~/pipelines/components/dag/dag_graph.vue'; -import { createSankey, removeOrphanNodes } from '~/pipelines/components/dag/utils'; +import { createSankey } from '~/pipelines/components/dag/drawing_utils'; +import { removeOrphanNodes } from '~/pipelines/components/dag/parsing_utils'; import { parsedData } from './mock_data'; describe('The DAG graph', () => { diff --git a/spec/frontend/pipelines/components/dag/drawing_utils_spec.js b/spec/frontend/pipelines/components/dag/drawing_utils_spec.js new file mode 100644 index 00000000000..a50163411ed --- /dev/null +++ b/spec/frontend/pipelines/components/dag/drawing_utils_spec.js @@ -0,0 +1,57 @@ +import { createSankey } from '~/pipelines/components/dag/drawing_utils'; +import { parseData } from '~/pipelines/components/dag/parsing_utils'; +import { mockBaseData } from './mock_data'; + +describe('DAG visualization drawing utilities', () => { + const parsed = parseData(mockBaseData.stages); + + const layoutSettings = { + width: 200, + height: 200, + nodeWidth: 10, + nodePadding: 20, + paddingForLabels: 100, + }; + + const sankeyLayout = createSankey(layoutSettings)(parsed); + + describe('createSankey', () => { + it('returns a nodes data structure with expected d3-added properties', () => { + const exampleNode = sankeyLayout.nodes[0]; + expect(exampleNode).toHaveProperty('sourceLinks'); + expect(exampleNode).toHaveProperty('targetLinks'); + expect(exampleNode).toHaveProperty('depth'); + expect(exampleNode).toHaveProperty('layer'); + expect(exampleNode).toHaveProperty('x0'); + expect(exampleNode).toHaveProperty('x1'); + expect(exampleNode).toHaveProperty('y0'); + expect(exampleNode).toHaveProperty('y1'); + }); + + it('returns a links data structure with expected d3-added properties', () => { + const exampleLink = sankeyLayout.links[0]; + expect(exampleLink).toHaveProperty('source'); + expect(exampleLink).toHaveProperty('target'); + expect(exampleLink).toHaveProperty('width'); + expect(exampleLink).toHaveProperty('y0'); + expect(exampleLink).toHaveProperty('y1'); + }); + + describe('data structure integrity', () => { + const newObject = { name: 'bad-actor' }; + + beforeEach(() => { + sankeyLayout.nodes.unshift(newObject); + }); + + it('sankey does not propagate changes back to the original', () => { + expect(sankeyLayout.nodes[0]).toBe(newObject); + expect(parsed.nodes[0]).not.toBe(newObject); + }); + + afterEach(() => { + sankeyLayout.nodes.shift(); + }); + }); + }); +}); diff --git a/spec/frontend/pipelines/components/dag/utils_spec.js b/spec/frontend/pipelines/components/dag/parsing_utils_spec.js index be0e4f0ea8e..d9a1296e572 100644 --- a/spec/frontend/pipelines/components/dag/utils_spec.js +++ b/spec/frontend/pipelines/components/dag/parsing_utils_spec.js @@ -3,11 +3,11 @@ import { makeLinksFromNodes, filterByAncestors, parseData, - createSankey, removeOrphanNodes, getMaxNodes, -} from '~/pipelines/components/dag/utils'; +} from '~/pipelines/components/dag/parsing_utils'; +import { createSankey } from '~/pipelines/components/dag/drawing_utils'; import { mockBaseData } from './mock_data'; describe('DAG visualization parsing utilities', () => { @@ -105,44 +105,6 @@ describe('DAG visualization parsing utilities', () => { }); }); - describe('createSankey', () => { - it('returns a nodes data structure with expected d3-added properties', () => { - expect(sankeyLayout.nodes[0]).toHaveProperty('sourceLinks'); - expect(sankeyLayout.nodes[0]).toHaveProperty('targetLinks'); - expect(sankeyLayout.nodes[0]).toHaveProperty('depth'); - expect(sankeyLayout.nodes[0]).toHaveProperty('layer'); - expect(sankeyLayout.nodes[0]).toHaveProperty('x0'); - expect(sankeyLayout.nodes[0]).toHaveProperty('x1'); - expect(sankeyLayout.nodes[0]).toHaveProperty('y0'); - expect(sankeyLayout.nodes[0]).toHaveProperty('y1'); - }); - - it('returns a links data structure with expected d3-added properties', () => { - expect(sankeyLayout.links[0]).toHaveProperty('source'); - expect(sankeyLayout.links[0]).toHaveProperty('target'); - expect(sankeyLayout.links[0]).toHaveProperty('width'); - expect(sankeyLayout.links[0]).toHaveProperty('y0'); - expect(sankeyLayout.links[0]).toHaveProperty('y1'); - }); - - describe('data structure integrity', () => { - const newObject = { name: 'bad-actor' }; - - beforeEach(() => { - sankeyLayout.nodes.unshift(newObject); - }); - - it('sankey does not propagate changes back to the original', () => { - expect(sankeyLayout.nodes[0]).toBe(newObject); - expect(parsed.nodes[0]).not.toBe(newObject); - }); - - afterEach(() => { - sankeyLayout.nodes.shift(); - }); - }); - }); - describe('removeOrphanNodes', () => { it('removes sankey nodes that have no needs and are not needed', () => { const cleanedNodes = removeOrphanNodes(sankeyLayout.nodes); diff --git a/spec/graphql/types/release_links_type_spec.rb b/spec/graphql/types/release_links_type_spec.rb index aaedb9ea29c..49e04e120f4 100644 --- a/spec/graphql/types/release_links_type_spec.rb +++ b/spec/graphql/types/release_links_type_spec.rb @@ -7,7 +7,7 @@ describe GitlabSchema.types['ReleaseLink'] do it 'has the expected fields' do expected_fields = %w[ - id name url external + id name url external link_type ] expect(described_class).to include_graphql_fields(*expected_fields) diff --git a/spec/models/ci/build_report_result_spec.rb b/spec/models/ci/build_report_result_spec.rb index e9211a22d08..078b0d100a1 100644 --- a/spec/models/ci/build_report_result_spec.rb +++ b/spec/models/ci/build_report_result_spec.rb @@ -29,4 +29,46 @@ describe Ci::BuildReportResult do end end end + + describe '#tests_name' do + it 'returns the suite name' do + expect(build_report_result.tests_name).to eq("rspec") + end + end + + describe '#tests_duration' do + it 'returns the suite duration' do + expect(build_report_result.tests_duration).to eq(0.42) + end + end + + describe '#tests_success' do + it 'returns the success count' do + expect(build_report_result.tests_success).to eq(2) + end + end + + describe '#tests_failed' do + it 'returns the failed count' do + expect(build_report_result.tests_failed).to eq(0) + end + end + + describe '#tests_errored' do + it 'returns the errored count' do + expect(build_report_result.tests_errored).to eq(0) + end + end + + describe '#tests_skipped' do + it 'returns the skipped count' do + expect(build_report_result.tests_skipped).to eq(0) + end + end + + describe '#tests_total' do + it 'returns the total count' do + expect(build_report_result.tests_total).to eq(2) + end + end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index f5f2d176636..01961847806 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -875,6 +875,22 @@ describe Ci::Build do end end + describe '#has_test_reports?' do + subject { build.has_test_reports? } + + context 'when build has a test report' do + let(:build) { create(:ci_build, :test_reports) } + + it { is_expected.to be_truthy } + end + + context 'when build does not have a test report' do + let(:build) { create(:ci_build) } + + it { is_expected.to be_falsey } + end + end + describe '#has_old_trace?' do subject { build.has_old_trace? } diff --git a/spec/services/ci/build_report_result_service_spec.rb b/spec/services/ci/build_report_result_service_spec.rb new file mode 100644 index 00000000000..dbdfc774314 --- /dev/null +++ b/spec/services/ci/build_report_result_service_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Ci::BuildReportResultService do + describe "#execute" do + subject(:build_report_result) { described_class.new.execute(build) } + + context 'when build is finished' do + let(:build) { create(:ci_build, :success, :test_reports) } + + it 'creates a build report result entry', :aggregate_failures do + expect(build_report_result.tests_name).to eq("test") + expect(build_report_result.tests_success).to eq(2) + expect(build_report_result.tests_failed).to eq(2) + expect(build_report_result.tests_errored).to eq(0) + expect(build_report_result.tests_skipped).to eq(0) + expect(build_report_result.tests_duration).to eq(0.010284) + expect(Ci::BuildReportResult.count).to eq(1) + end + + context 'when feature is disable' do + it 'does not persist the data' do + stub_feature_flags(build_report_summary: false) + + subject + + expect(Ci::BuildReportResult.count).to eq(0) + end + end + + context 'when data has already been persisted' do + it 'raises an error and do not persist the same data twice' do + expect { 2.times { described_class.new.execute(build) } }.to raise_error(ActiveRecord::RecordNotUnique) + + expect(Ci::BuildReportResult.count).to eq(1) + end + end + end + + context 'when build is running and test report does not exist' do + let(:build) { create(:ci_build, :running) } + + it 'does not persist data' do + subject + + expect(Ci::BuildReportResult.count).to eq(0) + end + end + end +end diff --git a/spec/views/projects/services/_form.haml_spec.rb b/spec/views/projects/services/_form.haml_spec.rb index a3faa92b50e..720e0aaf450 100644 --- a/spec/views/projects/services/_form.haml_spec.rb +++ b/spec/views/projects/services/_form.haml_spec.rb @@ -15,7 +15,8 @@ describe 'projects/services/_form' do allow(view).to receive_messages(current_user: user, can?: true, - current_application_settings: Gitlab::CurrentSettings.current_application_settings) + current_application_settings: Gitlab::CurrentSettings.current_application_settings, + request: double(referrer: '/services')) end context 'commit_events and merge_request_events' do @@ -30,6 +31,7 @@ describe 'projects/services/_form' do expect(rendered).to have_content('Event will be triggered when a commit is created/updated') expect(rendered).to have_content('Event will be triggered when a merge request is created/updated/merged') + expect(rendered).to have_css("input[name='redirect_to'][value='/services']", count: 1, visible: false) end end end diff --git a/spec/workers/build_finished_worker_spec.rb b/spec/workers/build_finished_worker_spec.rb index 4adb795b1d6..849563d9608 100644 --- a/spec/workers/build_finished_worker_spec.rb +++ b/spec/workers/build_finished_worker_spec.rb @@ -3,7 +3,11 @@ require 'spec_helper' describe BuildFinishedWorker do + subject { described_class.new.perform(build.id) } + describe '#perform' do + let(:build) { create(:ci_build, :success, pipeline: create(:ci_pipeline)) } + context 'when build exists' do let!(:build) { create(:ci_build) } @@ -18,8 +22,10 @@ describe BuildFinishedWorker do expect(BuildHooksWorker).to receive(:perform_async) expect(ArchiveTraceWorker).to receive(:perform_async) expect(ExpirePipelineCacheWorker).to receive(:perform_async) + expect(ChatNotificationWorker).not_to receive(:perform_async) + expect(Ci::BuildReportResultWorker).not_to receive(:perform) - described_class.new.perform(build.id) + subject end end @@ -30,23 +36,26 @@ describe BuildFinishedWorker do end end - it 'schedules a ChatNotification job for a chat build' do - build = create(:ci_build, :success, pipeline: create(:ci_pipeline, source: :chat)) + context 'when build has a chat' do + let(:build) { create(:ci_build, :success, pipeline: create(:ci_pipeline, source: :chat)) } - expect(ChatNotificationWorker) - .to receive(:perform_async) - .with(build.id) + it 'schedules a ChatNotification job' do + expect(ChatNotificationWorker).to receive(:perform_async).with(build.id) - described_class.new.perform(build.id) + subject + end end - it 'does not schedule a ChatNotification job for a regular build' do - build = create(:ci_build, :success, pipeline: create(:ci_pipeline)) + context 'when build has a test report' do + let(:build) { create(:ci_build, :test_reports) } - expect(ChatNotificationWorker) - .not_to receive(:perform_async) + it 'schedules a BuildReportResult job' do + expect_next_instance_of(Ci::BuildReportResultWorker) do |worker| + expect(worker).to receive(:perform).with(build.id) + end - described_class.new.perform(build.id) + subject + end end end end diff --git a/spec/workers/ci/build_report_result_worker_spec.rb b/spec/workers/ci/build_report_result_worker_spec.rb new file mode 100644 index 00000000000..290a98366b4 --- /dev/null +++ b/spec/workers/ci/build_report_result_worker_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Ci::BuildReportResultWorker do + subject { described_class.new.perform(build_id) } + + context 'when build exists' do + let(:build) { create(:ci_build) } + let(:build_id) { build.id } + + it 'calls build report result service' do + expect_next_instance_of(Ci::BuildReportResultService) do |build_report_result_service| + expect(build_report_result_service).to receive(:execute) + end + + subject + end + end + + context 'when build does not exist' do + let(:build_id) { -1 } + + it 'does not call build report result service' do + expect(Ci::BuildReportResultService).not_to receive(:execute) + + subject + end + end +end |