diff options
163 files changed, 1685 insertions, 560 deletions
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 21784a993b1..5a1fc68a915 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -862,7 +862,6 @@ Rails/SaveBang: - 'ee/spec/services/todo_service_spec.rb' - 'ee/spec/services/update_build_minutes_service_spec.rb' - 'ee/spec/services/vulnerability_feedback/create_service_spec.rb' - - 'ee/spec/support/helpers/ee/geo_helpers.rb' - 'ee/spec/support/protected_tags/access_control_shared_examples.rb' - 'ee/spec/support/shared_examples/features/protected_branches_access_control_shared_examples.rb' - 'ee/spec/support/shared_examples/finders/geo/framework_registry_finder_shared_examples.rb' @@ -1306,12 +1305,6 @@ Rails/SaveBang: - 'spec/services/users/repair_ldap_blocked_service_spec.rb' - 'spec/services/verify_pages_domain_service_spec.rb' - 'spec/sidekiq/cron/job_gem_dependency_spec.rb' - - 'spec/support/helpers/cycle_analytics_helpers.rb' - - 'spec/support/helpers/design_management_test_helpers.rb' - - 'spec/support/helpers/jira_service_helper.rb' - - 'spec/support/helpers/login_helpers.rb' - - 'spec/support/helpers/notification_helpers.rb' - - 'spec/support/helpers/stub_object_storage.rb' - 'spec/support/migrations_helpers/cluster_helpers.rb' - 'spec/support/migrations_helpers/namespaces_helper.rb' - 'spec/support/shared_contexts/email_shared_context.rb' diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue index 14c71f73291..fbf19847e9d 100644 --- a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue +++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue @@ -3,7 +3,6 @@ import { GlAlert, GlButton, GlCollapse, - GlDeprecatedButton, GlFormCheckbox, GlFormCombobox, GlFormGroup, @@ -39,7 +38,6 @@ export default { GlAlert, GlButton, GlCollapse, - GlDeprecatedButton, GlFormCheckbox, GlFormCombobox, GlFormGroup, @@ -340,24 +338,25 @@ export default { </gl-alert> </gl-collapse> <template #modal-footer> - <gl-deprecated-button @click="hideModal">{{ __('Cancel') }}</gl-deprecated-button> - <gl-deprecated-button + <gl-button @click="hideModal">{{ __('Cancel') }}</gl-button> + <gl-button v-if="variableBeingEdited" ref="deleteCiVariable" - category="secondary" variant="danger" + category="secondary" data-qa-selector="ci_variable_delete_button" @click="deleteVarAndClose" - >{{ __('Delete variable') }}</gl-deprecated-button + >{{ __('Delete variable') }}</gl-button > - <gl-deprecated-button + <gl-button ref="updateOrAddVariable" :disabled="!canSubmit" variant="success" + category="primary" data-qa-selector="ci_variable_save_button" @click="updateOrAddVariable" >{{ modalActionText }} - </gl-deprecated-button> + </gl-button> </template> </gl-modal> </template> diff --git a/app/assets/javascripts/frequent_items/utils.js b/app/assets/javascripts/frequent_items/utils.js index d4b7ffdcbe1..112e8eaaf17 100644 --- a/app/assets/javascripts/frequent_items/utils.js +++ b/app/assets/javascripts/frequent_items/utils.js @@ -1,6 +1,6 @@ import { take } from 'lodash'; import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; -import sanitize from 'sanitize-html'; +import { sanitize } from 'dompurify'; import { FREQUENT_ITEMS, HOUR_IN_MS } from './constants'; export const isMobile = () => ['md', 'sm', 'xs'].includes(bp.getBreakpointSize()); @@ -52,7 +52,7 @@ export const sanitizeItem = item => { return {}; } - return { [key]: sanitize(item[key].toString(), { allowedTags: [] }) }; + return { [key]: sanitize(item[key].toString(), { ALLOWED_TAGS: [] }) }; }; return { diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue index 85f24c5b122..ecd8acb449e 100644 --- a/app/assets/javascripts/incidents/components/incidents_list.vue +++ b/app/assets/javascripts/incidents/components/incidents_list.vue @@ -13,6 +13,7 @@ import { GlPagination, GlTabs, GlTab, + GlBadge, } from '@gitlab/ui'; import { debounce } from 'lodash'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; @@ -20,7 +21,8 @@ import { convertToSnakeCase } from '~/lib/utils/text_utility'; import { s__ } from '~/locale'; import { mergeUrlParams, joinPaths, visitUrl } from '~/lib/utils/url_utility'; import getIncidents from '../graphql/queries/get_incidents.query.graphql'; -import { I18N, DEFAULT_PAGE_SIZE, INCIDENT_SEARCH_DELAY, INCIDENT_STATE_TABS } from '../constants'; +import getIncidentsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql'; +import { I18N, DEFAULT_PAGE_SIZE, INCIDENT_SEARCH_DELAY, INCIDENT_STATUS_TABS } from '../constants'; const TH_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' }; const tdClass = @@ -39,7 +41,7 @@ const initialPaginationState = { export default { i18n: I18N, - stateTabs: INCIDENT_STATE_TABS, + statusTabs: INCIDENT_STATUS_TABS, fields: [ { key: 'title', @@ -77,6 +79,7 @@ export default { GlTabs, GlTab, PublishedCell: () => import('ee_component/incidents/components/published_cell.vue'), + GlBadge, }, directives: { GlTooltip: GlTooltipDirective, @@ -94,7 +97,7 @@ export default { variables() { return { searchTerm: this.searchTerm, - state: this.stateFilter, + status: this.statusFilter, projectPath: this.projectPath, issueTypes: ['INCIDENT'], sort: this.sort, @@ -114,6 +117,19 @@ export default { this.errored = true; }, }, + incidentsCount: { + query: getIncidentsCountByStatus, + variables() { + return { + searchTerm: this.searchTerm, + projectPath: this.projectPath, + issueTypes: ['INCIDENT'], + }; + }, + update(data) { + return data.project?.issueStatusCounts; + }, + }, }, data() { return { @@ -123,15 +139,16 @@ export default { searchTerm: '', pagination: initialPaginationState, incidents: {}, - stateFilter: '', sort: 'created_desc', sortBy: 'createdAt', sortDesc: true, + statusFilter: '', + filteredByStatus: '', }; }, computed: { showErrorMsg() { - return this.errored && !this.isErrorAlertDismissed && !this.searchTerm; + return this.errored && !this.isErrorAlertDismissed && this.incidentsCount?.all === 0; }, loading() { return this.$apollo.queries.incidents.loading; @@ -139,6 +156,9 @@ export default { hasIncidents() { return this.incidents?.list?.length; }, + incidentsForCurrentTab() { + return this.incidentsCount?.[this.filteredByStatus.toLowerCase()] ?? 0; + }, showPaginationControls() { return Boolean( this.incidents?.pageInfo?.hasNextPage || this.incidents?.pageInfo?.hasPreviousPage, @@ -149,7 +169,9 @@ export default { }, nextPage() { const nextPage = this.pagination.currentPage + 1; - return this.incidents?.list?.length < DEFAULT_PAGE_SIZE ? null : nextPage; + return nextPage > Math.ceil(this.incidentsForCurrentTab / DEFAULT_PAGE_SIZE) + ? null + : nextPage; }, tbodyTrClass() { return { @@ -181,9 +203,10 @@ export default { this.searchTerm = trimmedInput; } }, INCIDENT_SEARCH_DELAY), - filterIncidentsByState(tabIndex) { - const { filters } = this.$options.stateTabs[tabIndex]; - this.stateFilter = filters; + filterIncidentsByStatus(tabIndex) { + const { filters, status } = this.$options.statusTabs[tabIndex]; + this.statusFilter = filters; + this.filteredByStatus = status; }, hasAssignees(assignees) { return Boolean(assignees.nodes?.length); @@ -231,10 +254,13 @@ export default { <div class="incident-management-list-header gl-display-flex gl-justify-content-space-between gl-border-b-solid gl-border-b-1 gl-border-gray-100" > - <gl-tabs content-class="gl-p-0" @input="filterIncidentsByState"> - <gl-tab v-for="tab in $options.stateTabs" :key="tab.state" :data-testid="tab.state"> + <gl-tabs content-class="gl-p-0" @input="filterIncidentsByStatus"> + <gl-tab v-for="tab in $options.statusTabs" :key="tab.status" :data-testid="tab.status"> <template #title> <span>{{ tab.title }}</span> + <gl-badge v-if="incidentsCount" pill size="sm" class="gl-tab-counter-badge"> + {{ incidentsCount[tab.status.toLowerCase()] }} + </gl-badge> </template> </gl-tab> </gl-tabs> diff --git a/app/assets/javascripts/incidents/constants.js b/app/assets/javascripts/incidents/constants.js index fe92f131738..dc90f30991c 100644 --- a/app/assets/javascripts/incidents/constants.js +++ b/app/assets/javascripts/incidents/constants.js @@ -9,20 +9,20 @@ export const I18N = { searchPlaceholder: __('Search results…'), }; -export const INCIDENT_STATE_TABS = [ +export const INCIDENT_STATUS_TABS = [ { title: s__('IncidentManagement|Open'), - state: 'OPENED', + status: 'OPENED', filters: 'opened', }, { title: s__('IncidentManagement|Closed'), - state: 'CLOSED', + status: 'CLOSED', filters: 'closed', }, { title: s__('IncidentManagement|All'), - state: 'ALL', + status: 'ALL', filters: 'all', }, ]; diff --git a/app/assets/javascripts/incidents/graphql/queries/get_count_by_status.query.graphql b/app/assets/javascripts/incidents/graphql/queries/get_count_by_status.query.graphql new file mode 100644 index 00000000000..0b784b104a8 --- /dev/null +++ b/app/assets/javascripts/incidents/graphql/queries/get_count_by_status.query.graphql @@ -0,0 +1,9 @@ +query getIncidentsCountByStatus($searchTerm: String, $projectPath: ID!, $issueTypes: [IssueType!]) { + project(fullPath: $projectPath) { + issueStatusCounts(search: $searchTerm, types: $issueTypes) { + all + opened + closed + } + } +} diff --git a/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql b/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql index 6e8e6a1254c..0f56e8640bd 100644 --- a/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql +++ b/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql @@ -2,7 +2,7 @@ query getIncidents( $projectPath: ID! $issueTypes: [IssueType!] $sort: IssueSort - $state: IssuableState + $status: IssuableState $firstPageSize: Int $lastPageSize: Int $prevPageCursor: String = "" @@ -12,9 +12,9 @@ query getIncidents( project(fullPath: $projectPath) { issues( search: $searchTerm - state: $state types: $issueTypes sort: $sort + state: $status first: $firstPageSize last: $lastPageSize after: $nextPageCursor diff --git a/app/assets/javascripts/issue_show/utils/parse_data.js b/app/assets/javascripts/issue_show/utils/parse_data.js index 05e384adad3..8cd1c1b0e56 100644 --- a/app/assets/javascripts/issue_show/utils/parse_data.js +++ b/app/assets/javascripts/issue_show/utils/parse_data.js @@ -1,4 +1,4 @@ -import sanitize from 'sanitize-html'; +import { sanitize } from 'dompurify'; export const parseIssuableData = () => { try { diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 08daac15754..e26b63fbb85 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -710,3 +710,16 @@ export const dateFromParams = (year, month, day) => { return date; }; + +/** + * A utility function which computes the difference in seconds + * between 2 dates. + * + * @param {Date} startDate the start sate + * @param {Date} endDate the end date + * + * @return {Int} the difference in seconds + */ +export const differenceInSeconds = (startDate, endDate) => { + return (endDate.getTime() - startDate.getTime()) / 1000; +}; diff --git a/app/assets/javascripts/lib/utils/highlight.js b/app/assets/javascripts/lib/utils/highlight.js index b1dd562f63a..32553af9af3 100644 --- a/app/assets/javascripts/lib/utils/highlight.js +++ b/app/assets/javascripts/lib/utils/highlight.js @@ -1,5 +1,5 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus'; -import sanitize from 'sanitize-html'; +import { sanitize } from 'dompurify'; /** * Wraps substring matches with HTML `<span>` elements. @@ -24,7 +24,7 @@ export default function highlight(string, match = '', matchPrefix = '<b>', match return string; } - const sanitizedValue = sanitize(string.toString(), { allowedTags: [] }); + const sanitizedValue = sanitize(string.toString(), { ALLOWED_TAGS: [] }); // occurrences is an array of character indices that should be // highlighted in the original string, i.e. [3, 4, 5, 7] diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue index fcb09ea90db..fa1afdcd16f 100644 --- a/app/assets/javascripts/notebook/cells/markdown.vue +++ b/app/assets/javascripts/notebook/cells/markdown.vue @@ -1,6 +1,6 @@ <script> import marked from 'marked'; -import sanitize from 'sanitize-html'; +import { sanitize } from 'dompurify'; import katex from 'katex'; import Prompt from './prompt.vue'; @@ -104,65 +104,58 @@ export default { return sanitize(marked(this.cell.source.join('').replace(/\\/g, '\\\\')), { // allowedTags from GitLab's inline HTML guidelines // https://docs.gitlab.com/ee/user/markdown.html#inline-html - allowedTags: [ + ALLOWED_TAGS: [ + 'a', + 'abbr', + 'b', + 'blockquote', + 'br', + 'code', + 'dd', + 'del', + 'div', + 'dl', + 'dt', + 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', - 'h7', - 'h8', - 'br', - 'b', + 'hr', 'i', - 'strong', - 'em', - 'a', - 'pre', - 'code', 'img', - 'tt', - 'div', 'ins', - 'del', - 'sup', - 'sub', - 'p', - 'ol', - 'ul', - 'table', - 'thead', - 'tbody', - 'tfoot', - 'blockquote', - 'dl', - 'dt', - 'dd', 'kbd', + 'li', + 'ol', + 'p', + 'pre', 'q', - 'samp', - 'var', - 'hr', - 'ruby', - 'rt', 'rp', - 'li', - 'tr', - 'td', - 'th', + 'rt', + 'ruby', 's', - 'strike', + 'samp', 'span', - 'abbr', - 'abbr', + 'strike', + 'strong', + 'sub', 'summary', + 'sup', + 'table', + 'tbody', + 'td', + 'tfoot', + 'th', + 'thead', + 'tr', + 'tt', + 'ul', + 'var', ], - allowedAttributes: { - '*': ['class', 'style'], - a: ['href'], - img: ['src'], - }, + ALLOWED_ATTR: ['class', 'style', 'href', 'src'], }); }, }, diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue index 8dc2d73af9b..b36761993ea 100644 --- a/app/assets/javascripts/notebook/cells/output/html.vue +++ b/app/assets/javascripts/notebook/cells/output/html.vue @@ -1,5 +1,5 @@ <script> -import sanitize from 'sanitize-html'; +import { sanitize } from 'dompurify'; import Prompt from '../prompt.vue'; export default { @@ -23,10 +23,7 @@ export default { computed: { sanitizedOutput() { return sanitize(this.rawCode, { - allowedTags: sanitize.defaults.allowedTags.concat(['img', 'svg']), - allowedAttributes: { - img: ['src'], - }, + ALLOWED_ATTR: ['src'], }); }, showOutput() { diff --git a/app/assets/javascripts/packages/details/store/getters.js b/app/assets/javascripts/packages/details/store/getters.js index 77dc24ff169..d1814d506ad 100644 --- a/app/assets/javascripts/packages/details/store/getters.js +++ b/app/assets/javascripts/packages/details/store/getters.js @@ -110,6 +110,6 @@ export const composerRegistryInclude = ({ composerPath }) => { return JSON.stringify(base); }; export const composerPackageInclude = ({ packageEntity }) => { - const base = { package_name: packageEntity.name }; + const base = { [packageEntity.name]: packageEntity.version }; return JSON.stringify(base); }; diff --git a/app/assets/javascripts/packages/shared/components/package_list_row.vue b/app/assets/javascripts/packages/shared/components/package_list_row.vue index 3515ab4ef03..8cc07632695 100644 --- a/app/assets/javascripts/packages/shared/components/package_list_row.vue +++ b/app/assets/javascripts/packages/shared/components/package_list_row.vue @@ -54,9 +54,6 @@ export default { hasProjectLink() { return Boolean(this.packageEntity.project_path); }, - deleteAvailable() { - return !this.disableDelete && !this.isGroup; - }, }, }; </script> @@ -111,7 +108,7 @@ export default { <div class="table-section d-flex flex-md-column justify-content-between align-items-md-end" - :class="!deleteAvailable ? 'section-50' : 'section-40'" + :class="disableDelete ? 'section-50' : 'section-40'" > <publish-method :package-entity="packageEntity" :is-group="isGroup" /> @@ -126,7 +123,7 @@ export default { </div> </div> - <div v-if="deleteAvailable" class="table-section section-10 d-flex justify-content-end"> + <div v-if="!disableDelete" class="table-section section-10 d-flex justify-content-end"> <gl-button data-testid="action-delete" icon="remove" diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index a31034361a8..599888b1fe0 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -2,7 +2,7 @@ import $ from 'jquery'; import fuzzaldrinPlus from 'fuzzaldrin-plus'; -import sanitize from 'sanitize-html'; +import { sanitize } from 'dompurify'; import axios from '~/lib/utils/axios_utils'; import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility'; import flash from '~/flash'; diff --git a/app/assets/javascripts/user_popovers.js b/app/assets/javascripts/user_popovers.js index 290de55e6f9..c8f95dac48e 100644 --- a/app/assets/javascripts/user_popovers.js +++ b/app/assets/javascripts/user_popovers.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import sanitize from 'sanitize-html'; +import { sanitize } from 'dompurify'; import UsersCache from './lib/utils/users_cache'; import UserPopover from './vue_shared/components/user_popover/user_popover.vue'; diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 8f209d2d99a..ee83ce67c03 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -458,6 +458,23 @@ } .vue-filtered-search-bar-container { + .gl-search-box-by-click { + // Absolute width is needed to prevent flex to grow + // beyond the available width. + .gl-filtered-search-scrollable { + width: 1px; + } + + // There are several styling issues happening while using + // `GlFilteredSearch` in roadmap due to some of our global + // styles which we need to override until those are fixed + // at framework level. + // See https://gitlab.com/gitlab-org/gitlab-ui/-/issues/908 + .input-group-prepend + .gl-filtered-search-scrollable { + border-radius: 0; + } + } + @include media-breakpoint-up(md) { .sort-dropdown-container { margin-left: 10px; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 48c6db51103..02f81157fce 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -446,6 +446,8 @@ $context-header-height: 60px; $breadcrumb-min-height: 48px; $home-panel-title-row-height: 64px; $home-panel-avatar-mobile-size: 24px; +$issuable-title-max-width: 350px; +$milestone-title-max-width: 75px; $gl-line-height: 16px; $gl-line-height-18: 18px; $gl-line-height-20: 20px; diff --git a/app/graphql/resolvers/concerns/resolves_merge_requests.rb b/app/graphql/resolvers/concerns/resolves_merge_requests.rb index 7ed88be52b9..0c01efd4f9a 100644 --- a/app/graphql/resolvers/concerns/resolves_merge_requests.rb +++ b/app/graphql/resolvers/concerns/resolves_merge_requests.rb @@ -38,6 +38,9 @@ module ResolvesMergeRequests assignees: [:assignees], labels: [:labels], author: [:author], + merged_at: [:metrics], + commit_count: [:metrics], + approved_by: [:approver_users], milestone: [:milestone], head_pipeline: [:merge_request_diff, { head_pipeline: [:merge_request] }] } diff --git a/app/graphql/types/alert_management/alert_type.rb b/app/graphql/types/alert_management/alert_type.rb index 2e9b729a0b3..1a0b0685ffe 100644 --- a/app/graphql/types/alert_management/alert_type.rb +++ b/app/graphql/types/alert_management/alert_type.rb @@ -107,6 +107,16 @@ module Types description: 'Todos of the current user for the alert', resolver: Resolvers::TodoResolver + field :details_url, + GraphQL::STRING_TYPE, + null: false, + description: 'The URL of the alert detail page' + + field :prometheus_alert, + Types::PrometheusAlertType, + null: true, + description: 'The alert condition for Prometheus' + def notes object.ordered_notes end diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb index 179a5393b17..caa7079e2c6 100644 --- a/app/graphql/types/ci/pipeline_type.rb +++ b/app/graphql/types/ci/pipeline_type.rb @@ -5,6 +5,8 @@ module Types class PipelineType < BaseObject graphql_name 'Pipeline' + connection_type_class(Types::CountableConnectionType) + authorize :read_pipeline expose_permissions Types::PermissionTypes::Ci::Pipeline diff --git a/app/graphql/types/issuable_connection_type.rb b/app/graphql/types/countable_connection_type.rb index ec180cc1576..2538366b786 100644 --- a/app/graphql/types/issuable_connection_type.rb +++ b/app/graphql/types/countable_connection_type.rb @@ -2,13 +2,14 @@ module Types # rubocop: disable Graphql/AuthorizeTypes - class IssuableConnectionType < GraphQL::Types::Relay::BaseConnection + class CountableConnectionType < GraphQL::Types::Relay::BaseConnection field :count, Integer, null: false, description: 'Total count of collection' def count # rubocop: disable CodeReuse/ActiveRecord relation = object.items + # sometimes relation is an Array relation = relation.reorder(nil) if relation.respond_to?(:reorder) # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/graphql/types/environment_type.rb b/app/graphql/types/environment_type.rb index 34a90006d03..239b26f9c38 100644 --- a/app/graphql/types/environment_type.rb +++ b/app/graphql/types/environment_type.rb @@ -19,5 +19,10 @@ module Types field :metrics_dashboard, Types::Metrics::DashboardType, null: true, description: 'Metrics dashboard schema for the environment', resolver: Resolvers::Metrics::DashboardResolver + + field :latest_opened_most_severe_alert, + Types::AlertManagement::AlertType, + null: true, + description: 'The most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned.' end end diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb index a5059276e84..0a73ce95424 100644 --- a/app/graphql/types/issue_type.rb +++ b/app/graphql/types/issue_type.rb @@ -4,7 +4,7 @@ module Types class IssueType < BaseObject graphql_name 'Issue' - connection_type_class(Types::IssuableConnectionType) + connection_type_class(Types::CountableConnectionType) implements(Types::Notes::NoteableType) diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index c58f76ddb5c..01b02b7976f 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -4,7 +4,7 @@ module Types class MergeRequestType < BaseObject graphql_name 'MergeRequest' - connection_type_class(Types::IssuableConnectionType) + connection_type_class(Types::CountableConnectionType) implements(Types::Notes::NoteableType) @@ -143,6 +143,8 @@ module Types end field :task_completion_status, Types::TaskCompletionStatus, null: false, description: Types::TaskCompletionStatus.description + field :commit_count, GraphQL::INT_TYPE, null: true, + description: 'Number of commits in the merge request' def diff_stats(path: nil) stats = Array.wrap(object.diff_stats&.to_a) @@ -162,5 +164,14 @@ module Types hash.merge!(additions: status.additions, deletions: status.deletions, file_count: 1) { |_, x, y| x + y } end end + + def commit_count + object&.metrics&.commits_count + end + + def approvers + object.approver_users + end end end +Types::MergeRequestType.prepend_if_ee('::EE::Types::MergeRequestType') diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index b0778a07214..523e019b955 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -169,6 +169,12 @@ module Types description: 'Environments of the project', resolver: Resolvers::EnvironmentsResolver + field :environment, + Types::EnvironmentType, + null: true, + description: 'A single environment of the project', + resolver: Resolvers::EnvironmentsResolver.single + field :sast_ci_configuration, ::Types::CiConfiguration::Sast::Type, null: true, description: 'SAST CI configuration for the project', resolver: ::Resolvers::CiConfiguration::SastResolver diff --git a/app/graphql/types/prometheus_alert_type.rb b/app/graphql/types/prometheus_alert_type.rb new file mode 100644 index 00000000000..1d09a8dbeb7 --- /dev/null +++ b/app/graphql/types/prometheus_alert_type.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Types + class PrometheusAlertType < BaseObject + graphql_name 'PrometheusAlert' + description 'The alert condition for Prometheus' + + authorize :read_prometheus_alerts + + present_using PrometheusAlertPresenter + + field :id, GraphQL::ID_TYPE, null: false, + description: 'ID of the alert condition' + + field :humanized_text, + GraphQL::STRING_TYPE, + null: false, + description: 'The human-readable text of the alert condition' + end +end diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb index 397c09f38fc..75581805b49 100644 --- a/app/models/alert_management/alert.rb +++ b/app/models/alert_management/alert.rb @@ -118,7 +118,7 @@ module AlertManagement end delegate :iid, to: :issue, prefix: true, allow_nil: true - delegate :metrics_dashboard_url, :runbook, to: :present + delegate :metrics_dashboard_url, :runbook, :details_url, to: :present scope :for_iid, -> (iid) { where(iid: iid) } scope :for_status, -> (status) { where(status: status) } @@ -137,6 +137,7 @@ module AlertManagement # Descending sort order sorts severity from more critical to less critical. # https://gitlab.com/gitlab-org/gitlab/-/issues/221242#what-is-the-expected-correct-behavior scope :order_severity, -> (sort_order) { order(severity: sort_order == :asc ? :desc : :asc) } + scope :order_severity_with_open_prometheus_alert, -> { open.with_prometheus_alert.order(severity: :asc, started_at: :desc) } # Ascending sort order sorts statuses: Ignored > Resolved > Acknowledged > Triggered # Descending sort order sorts statuses: Triggered > Acknowledged > Resolved > Ignored diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index de7bd9fb67b..75c3ce98c95 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -9,6 +9,7 @@ module Ci include Sortable include IgnorableColumns include Artifactable + include FileStoreMounter extend Gitlab::Ci::Model NotSupportedAdapterError = Class.new(StandardError) @@ -115,7 +116,7 @@ module Ci belongs_to :project belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id - mount_uploader :file, JobArtifactUploader + mount_file_store_uploader JobArtifactUploader validates :file_format, presence: true, unless: :trace?, on: :create validate :validate_supported_file_format!, on: :create @@ -124,8 +125,6 @@ module Ci update_project_statistics project_statistics_name: :build_artifacts_size - after_save :update_file_store, if: :saved_change_to_file? - scope :not_expired, -> { where('expire_at IS NULL OR expire_at > ?', Time.current) } scope :with_files_stored_locally, -> { where(file_store: ::JobArtifactUploader::Store::LOCAL) } scope :with_files_stored_remotely, -> { where(file_store: ::JobArtifactUploader::Store::REMOTE) } @@ -229,12 +228,6 @@ module Ci end end - def update_file_store - # The file.object_store is set during `uploader.store!` - # which happens after object is inserted/updated - self.update_column(:file_store, file.object_store) - end - def self.associated_file_types_for(file_type) return unless file_types.include?(file_type) diff --git a/app/models/concerns/file_store_mounter.rb b/app/models/concerns/file_store_mounter.rb new file mode 100644 index 00000000000..9d4463e5297 --- /dev/null +++ b/app/models/concerns/file_store_mounter.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module FileStoreMounter + extend ActiveSupport::Concern + + class_methods do + def mount_file_store_uploader(uploader) + mount_uploader(:file, uploader) + + after_save :update_file_store, if: :saved_change_to_file? + end + end + + private + + def update_file_store + # The file.object_store is set during `uploader.store!` + # which happens after object is inserted/updated + self.update_column(:file_store, file.object_store) + end +end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 87587bb5afa..d6508ffceba 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -148,7 +148,7 @@ class Deployment < ApplicationRecord def execute_hooks deployment_data = Gitlab::DataBuilder::Deployment.build(self) - project.execute_hooks(deployment_data, :deployment_hooks) if Feature.enabled?(:deployment_webhooks, project) + project.execute_hooks(deployment_data, :deployment_hooks) if Feature.enabled?(:deployment_webhooks, project, default_enabled: true) project.execute_services(deployment_data, :deployment_hooks) end diff --git a/app/models/environment.rb b/app/models/environment.rb index bddc84f10b5..c6a08c996da 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -29,6 +29,7 @@ class Environment < ApplicationRecord has_one :last_visible_deployment, -> { visible.distinct_on_environment }, inverse_of: :environment, class_name: 'Deployment' has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus' has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline' + has_one :latest_opened_most_severe_alert, -> { order_severity_with_open_prometheus_alert }, class_name: 'AlertManagement::Alert', inverse_of: :environment before_validation :nullify_external_url before_validation :generate_slug, if: ->(env) { env.slug.blank? } @@ -291,6 +292,10 @@ class Environment < ApplicationRecord !!ENV['USE_SAMPLE_METRICS'] end + def has_opened_alert? + latest_opened_most_severe_alert.present? + end + def metrics prometheus_adapter.query(:environment, self) if has_metrics_and_can_query? end diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb index 3761484b15d..d60baa299cb 100644 --- a/app/models/lfs_object.rb +++ b/app/models/lfs_object.rb @@ -5,6 +5,7 @@ class LfsObject < ApplicationRecord include Checksummable include EachBatch include ObjectStorage::BackgroundMove + include FileStoreMounter has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :projects, -> { distinct }, through: :lfs_objects_projects @@ -15,21 +16,13 @@ class LfsObject < ApplicationRecord validates :oid, presence: true, uniqueness: true - mount_uploader :file, LfsObjectUploader - - after_save :update_file_store, if: :saved_change_to_file? + mount_file_store_uploader LfsObjectUploader def self.not_linked_to_project(project) where('NOT EXISTS (?)', project.lfs_objects_projects.select(1).where('lfs_objects_projects.lfs_object_id = lfs_objects.id')) end - def update_file_store - # The file.object_store is set during `uploader.store!` - # which happens after object is inserted/updated - self.update_column(:file_store, file.object_store) - end - def project_allowed_access?(project) if project.fork_network_member lfs_objects_projects diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 1c95789d7ba..f4c2d568b4d 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -264,10 +264,14 @@ class MergeRequest < ApplicationRecord end scope :by_target_branch, ->(branch_name) { where(target_branch: branch_name) } scope :preload_source_project, -> { preload(:source_project) } + scope :preload_target_project, -> { preload(:target_project) } scope :preload_routables, -> do preload(target_project: [:route, { namespace: :route }], source_project: [:route, { namespace: :route }]) end + scope :preload_author, -> { preload(:author) } + scope :preload_approved_by_users, -> { preload(:approved_by_users) } + scope :preload_metrics, -> (relation) { preload(metrics: relation) } scope :with_auto_merge_enabled, -> do with_state(:opened).where(auto_merge_enabled: true) diff --git a/app/models/prometheus_alert.rb b/app/models/prometheus_alert.rb index 1c870f4391a..80eef1705e7 100644 --- a/app/models/prometheus_alert.rb +++ b/app/models/prometheus_alert.rb @@ -3,6 +3,7 @@ class PrometheusAlert < ApplicationRecord include Sortable include UsageStatistics + include Presentable OPERATORS_MAP = { lt: "<", diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb index 759b9ce1eec..c50b9da1310 100644 --- a/app/models/terraform/state.rb +++ b/app/models/terraform/state.rb @@ -3,6 +3,7 @@ module Terraform class State < ApplicationRecord include UsageStatistics + include FileStoreMounter DEFAULT = '{"version":1}'.freeze HEX_REGEXP = %r{\A\h+\z}.freeze @@ -17,18 +18,10 @@ module Terraform default_value_for(:uuid, allows_nil: false) { SecureRandom.hex(UUID_LENGTH / 2) } - after_save :update_file_store, if: :saved_change_to_file? - - mount_uploader :file, StateUploader + mount_file_store_uploader StateUploader default_value_for(:file) { CarrierWaveStringFile.new(DEFAULT) } - def update_file_store - # The file.object_store is set during `uploader.store!` - # which happens after object is inserted/updated - self.update_column(:file_store, file.object_store) - end - def file_store super || StateUploader.default_store end diff --git a/app/policies/prometheus_alert_policy.rb b/app/policies/prometheus_alert_policy.rb new file mode 100644 index 00000000000..e6b0e6e8c17 --- /dev/null +++ b/app/policies/prometheus_alert_policy.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class PrometheusAlertPolicy < ::BasePolicy + delegate { @subject.project } +end diff --git a/app/presenters/alert_management/alert_presenter.rb b/app/presenters/alert_management/alert_presenter.rb index c3067e6377f..5bfa6dee18b 100644 --- a/app/presenters/alert_management/alert_presenter.rb +++ b/app/presenters/alert_management/alert_presenter.rb @@ -4,6 +4,7 @@ module AlertManagement class AlertPresenter < Gitlab::View::Presenter::Delegated include Gitlab::Utils::StrongMemoize include IncidentManagement::Settings + include ActionView::Helpers::UrlHelper MARKDOWN_LINE_BREAK = " \n".freeze @@ -45,15 +46,12 @@ module AlertManagement def metrics_dashboard_url; end - private - def details_url - ::Gitlab::Routing.url_helpers.details_project_alert_management_url( - project, - alert.iid - ) + details_project_alert_management_url(project, alert.iid) end + private + attr_reader :alert, :project def alerting_alert diff --git a/app/presenters/prometheus_alert_presenter.rb b/app/presenters/prometheus_alert_presenter.rb new file mode 100644 index 00000000000..99e24bdcdb9 --- /dev/null +++ b/app/presenters/prometheus_alert_presenter.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class PrometheusAlertPresenter < Gitlab::View::Presenter::Delegated + include ActionView::Helpers::UrlHelper + + presents :prometheus_alert + + def humanized_text + operator_text = + case prometheus_alert.operator + when 'lt' then s_('PrometheusAlerts|is less than') + when 'eq' then s_('PrometheusAlerts|is equal to') + when 'gt' then s_('PrometheusAlerts|exceeded') + end + + "#{operator_text} #{prometheus_alert.threshold}#{prometheus_alert.prometheus_metric.unit}" + end +end diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb index 7da5910a75b..a2bf9716f8f 100644 --- a/app/serializers/environment_entity.rb +++ b/app/serializers/environment_entity.rb @@ -71,6 +71,8 @@ class EnvironmentEntity < Grape::Entity can?(current_user, :destroy_environment, environment) end + expose :has_opened_alert?, if: -> (*) { can_read_alert_management_alert? }, expose_nil: false, as: :has_opened_alert + private alias_method :environment, :object @@ -91,6 +93,10 @@ class EnvironmentEntity < Grape::Entity can?(current_user, :read_pod_logs, environment.project) end + def can_read_alert_management_alert? + can?(current_user, :read_alert_management_alert, environment.project) + end + def cluster_platform_kubernetes? deployment_platform && deployment_platform.is_a?(Clusters::Platforms::Kubernetes) end diff --git a/changelogs/unreleased/227714-delete-packages-from-your-group-level-package-registry-view.yml b/changelogs/unreleased/227714-delete-packages-from-your-group-level-package-registry-view.yml new file mode 100644 index 00000000000..e40960f1e6c --- /dev/null +++ b/changelogs/unreleased/227714-delete-packages-from-your-group-level-package-registry-view.yml @@ -0,0 +1,5 @@ +--- +title: Enable delete button on Package group level view list +merge_request: 39430 +author: +type: changed diff --git a/changelogs/unreleased/232580-state-count.yml b/changelogs/unreleased/232580-state-count.yml new file mode 100644 index 00000000000..9fc05eecdab --- /dev/null +++ b/changelogs/unreleased/232580-state-count.yml @@ -0,0 +1,5 @@ +--- +title: Add incident count badge to the incident list +merge_request: 38278 +author: +type: changed diff --git a/changelogs/unreleased/233942-expose-more-data-for-mr-metrics-dashboard.yml b/changelogs/unreleased/233942-expose-more-data-for-mr-metrics-dashboard.yml new file mode 100644 index 00000000000..85f0dd4080f --- /dev/null +++ b/changelogs/unreleased/233942-expose-more-data-for-mr-metrics-dashboard.yml @@ -0,0 +1,5 @@ +--- +title: Expose counts (pipeline, commits) and approvers for a merge request in GraphQL +merge_request: 39086 +author: +type: added diff --git a/changelogs/unreleased/eb-skip-cobertura-sources.yml b/changelogs/unreleased/eb-skip-cobertura-sources.yml new file mode 100644 index 00000000000..fe0ed7d3648 --- /dev/null +++ b/changelogs/unreleased/eb-skip-cobertura-sources.yml @@ -0,0 +1,5 @@ +--- +title: Ignore the sources node from the cobertura XML +merge_request: 39385 +author: +type: fixed diff --git a/changelogs/unreleased/extend-graphql-api-for-alerts-in-environments.yml b/changelogs/unreleased/extend-graphql-api-for-alerts-in-environments.yml new file mode 100644 index 00000000000..5f60e9b6c51 --- /dev/null +++ b/changelogs/unreleased/extend-graphql-api-for-alerts-in-environments.yml @@ -0,0 +1,5 @@ +--- +title: Expose alert information for environments +merge_request: 38881 +author: +type: added diff --git a/changelogs/unreleased/group-coverage-reporting-csv.yml b/changelogs/unreleased/group-coverage-reporting-csv.yml new file mode 100644 index 00000000000..9f9b0546e24 --- /dev/null +++ b/changelogs/unreleased/group-coverage-reporting-csv.yml @@ -0,0 +1,5 @@ +--- +title: Add CoverageReportsController#index CSV response +merge_request: 38520 +author: +type: added diff --git a/changelogs/unreleased/rails-save-bang-12.yml b/changelogs/unreleased/rails-save-bang-12.yml new file mode 100644 index 00000000000..5e4f8ccf900 --- /dev/null +++ b/changelogs/unreleased/rails-save-bang-12.yml @@ -0,0 +1,5 @@ +--- +title: Refactor spec/support/helpers/* and ee/spec/support/helpers/* to fix Rails/SaveBang Cop +merge_request: 38995 +author: Rajendra Kadam +type: fixed diff --git a/changelogs/unreleased/sabrams-fix_composer_installation_code.yml b/changelogs/unreleased/sabrams-fix_composer_installation_code.yml new file mode 100644 index 00000000000..367d8a73baa --- /dev/null +++ b/changelogs/unreleased/sabrams-fix_composer_installation_code.yml @@ -0,0 +1,5 @@ +--- +title: Fix Composer installation code snippet to include package name and version +merge_request: 39400 +author: +type: fixed diff --git a/changelogs/unreleased/sh-refactor-file-store-mounter.yml b/changelogs/unreleased/sh-refactor-file-store-mounter.yml new file mode 100644 index 00000000000..19d8b346ab4 --- /dev/null +++ b/changelogs/unreleased/sh-refactor-file-store-mounter.yml @@ -0,0 +1,5 @@ +--- +title: Move file store updates and mount_uploader into a concern +merge_request: 37907 +author: +type: other diff --git a/changelogs/unreleased/specify-ruby-image-in-fail-fast-template.yml b/changelogs/unreleased/specify-ruby-image-in-fail-fast-template.yml new file mode 100644 index 00000000000..1a565bba4ea --- /dev/null +++ b/changelogs/unreleased/specify-ruby-image-in-fail-fast-template.yml @@ -0,0 +1,5 @@ +--- +title: Specify Ruby image in FailFast template +merge_request: 38523 +author: +type: changed diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml index 790b09c1dfa..d6386329d83 100644 --- a/config/dependency_decisions.yml +++ b/config/dependency_decisions.yml @@ -639,3 +639,10 @@ :why: MIT license :versions: [] :when: 2020-07-28 20:35:27.574875000 Z +- - :license + - dompurify + - Apache-2.0 + - :who: Lukas Eipert + :why: "https://github.com/cure53/DOMPurify/blob/main/LICENSE and https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31928#note_346604841" + :versions: [] + :when: 2020-08-13 13:42:46.508082000 Z diff --git a/config/webpack.vendor.config.js b/config/webpack.vendor.config.js index 548eca4200f..29c4c33314e 100644 --- a/config/webpack.vendor.config.js +++ b/config/webpack.vendor.config.js @@ -40,7 +40,7 @@ module.exports = { 'select2', 'moment-mini', 'aws-sdk', - 'sanitize-html', + 'dompurify', 'bootstrap/dist/js/bootstrap.js', 'sortablejs/modular/sortable.esm.js', 'popper.js', diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 82344869fa1..92766ab68e4 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -210,6 +210,11 @@ type AlertManagementAlert implements Noteable { details: JSON """ + The URL of the alert detail page + """ + detailsUrl: String! + + """ All discussions on this noteable """ discussions( @@ -295,6 +300,11 @@ type AlertManagementAlert implements Noteable { ): NoteConnection! """ + The alert condition for Prometheus + """ + prometheusAlert: PrometheusAlert + + """ Runbook for the alert as defined in alert details """ runbook: String @@ -4419,6 +4429,11 @@ type Environment { id: ID! """ + The most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned. + """ + latestOpenedMostSevereAlert: AlertManagementAlert + + """ Metrics dashboard schema for the environment """ metricsDashboard( @@ -8241,6 +8256,31 @@ type MergeRequest implements Noteable { allowCollaboration: Boolean """ + Users who approved the merge request + """ + approvedBy( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UserConnection + + """ Assignees of the merge request """ assignees( @@ -8271,6 +8311,11 @@ type MergeRequest implements Noteable { author: User """ + Number of commits in the merge request + """ + commitCount: Int + + """ Timestamp of when the merge request was created """ createdAt: Time! @@ -10250,6 +10295,11 @@ The connection type for Pipeline. """ type PipelineConnection { """ + Total count of collection + """ + count: Int! + + """ A list of edges. """ edges: [PipelineEdge] @@ -10554,6 +10604,26 @@ type Project { descriptionHtml: String """ + A single environment of the project + """ + environment( + """ + Name of the environment + """ + name: String + + """ + Search query for environment name + """ + search: String + + """ + States of environments that should be included in result + """ + states: [String!] + ): Environment + + """ Environments of the project """ environments( @@ -12116,6 +12186,21 @@ type ProjectStatistics { wikiSize: Float } +""" +The alert condition for Prometheus +""" +type PrometheusAlert { + """ + The human-readable text of the alert condition + """ + humanizedText: String! + + """ + ID of the alert condition + """ + id: ID! +} + type Query { """ Get information about current user diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 0f13079f202..719d448b5f4 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -578,6 +578,24 @@ "deprecationReason": null }, { + "name": "detailsUrl", + "description": "The URL of the alert detail page", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "discussions", "description": "All discussions on this noteable", "args": [ @@ -802,6 +820,20 @@ "deprecationReason": null }, { + "name": "prometheusAlert", + "description": "The alert condition for Prometheus", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "PrometheusAlert", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "runbook", "description": "Runbook for the alert as defined in alert details", "args": [ @@ -12339,6 +12371,20 @@ "deprecationReason": null }, { + "name": "latestOpenedMostSevereAlert", + "description": "The most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned.", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "AlertManagementAlert", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "metricsDashboard", "description": "Metrics dashboard schema for the environment", "args": [ @@ -22903,6 +22949,59 @@ "deprecationReason": null }, { + "name": "approvedBy", + "description": "Users who approved the merge request", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "UserConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "assignees", "description": "Assignees of the merge request", "args": [ @@ -22970,6 +23069,20 @@ "deprecationReason": null }, { + "name": "commitCount", + "description": "Number of commits in the merge request", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "createdAt", "description": "Timestamp of when the merge request was created", "args": [ @@ -30646,6 +30759,24 @@ "description": "The connection type for Pipeline.", "fields": [ { + "name": "count", + "description": "Total count of collection", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "edges", "description": "A list of edges.", "args": [ @@ -31458,6 +31589,57 @@ "deprecationReason": null }, { + "name": "environment", + "description": "A single environment of the project", + "args": [ + { + "name": "name", + "description": "Name of the environment", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "search", + "description": "Search query for environment name", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "states", + "description": "States of environments that should be included in result", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "Environment", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "environments", "description": "Environments of the project", "args": [ @@ -35657,6 +35839,55 @@ }, { "kind": "OBJECT", + "name": "PrometheusAlert", + "description": "The alert condition for Prometheus", + "fields": [ + { + "name": "humanizedText", + "description": "The human-readable text of the alert condition", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": "ID of the alert condition", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", "name": "Query", "description": null, "fields": [ diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 932135d443d..cadcacb7f48 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -64,6 +64,7 @@ Describes an alert from the project's Alert Management | `createdAt` | Time | Timestamp the alert was created | | `description` | String | Description of the alert | | `details` | JSON | Alert details | +| `detailsUrl` | String! | The URL of the alert detail page | | `endedAt` | Time | Timestamp the alert ended | | `eventCount` | Int | Number of events of this alert | | `hosts` | String! => Array | List of hosts the alert came from | @@ -71,6 +72,7 @@ Describes an alert from the project's Alert Management | `issueIid` | ID | Internal ID of the GitLab issue attached to the alert | | `metricsDashboardUrl` | String | URL for metrics embed for the alert | | `monitoringTool` | String | Monitoring tool the alert came from | +| `prometheusAlert` | PrometheusAlert | The alert condition for Prometheus | | `runbook` | String | Runbook for the alert as defined in alert details | | `service` | String | Service the alert came from | | `severity` | AlertManagementSeverity | Severity of the alert | @@ -739,6 +741,7 @@ Describes where code is deployed for a project | Name | Type | Description | | --- | ---- | ---------- | | `id` | ID! | ID of the environment | +| `latestOpenedMostSevereAlert` | AlertManagementAlert | The most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned. | | `metricsDashboard` | MetricsDashboard | Metrics dashboard schema for the environment | | `name` | String! | Human-readable name of the environment | | `state` | String! | State of the environment, for example: available/stopped | @@ -1257,6 +1260,7 @@ Autogenerated return type of MarkAsSpamSnippet | --- | ---- | ---------- | | `allowCollaboration` | Boolean | Indicates if members of the target project can push to the fork | | `author` | User | User who created this merge request | +| `commitCount` | Int | Number of commits in the merge request | | `createdAt` | Time! | Timestamp of when the merge request was created | | `defaultMergeCommitMessage` | String | Default merge commit message of the merge request | | `description` | String | Description of the merge request (Markdown rendered as HTML for caching) | @@ -1602,6 +1606,7 @@ Information about pagination in a connection. | `createdAt` | Time | Timestamp of the project creation | | `description` | String | Short description of the project | | `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` | +| `environment` | Environment | A single environment of the project | | `forksCount` | Int! | Number of times the project has been forked | | `fullPath` | ID! | Full path of the project | | `grafanaIntegration` | GrafanaIntegration | Grafana integration details for the project | @@ -1732,6 +1737,15 @@ Represents a Project Member | `storageSize` | Float! | Storage size of the project | | `wikiSize` | Float | Wiki size of the project | +## PrometheusAlert + +The alert condition for Prometheus + +| Name | Type | Description | +| --- | ---- | ---------- | +| `humanizedText` | String! | The human-readable text of the alert condition | +| `id` | ID! | ID of the alert condition | + ## Release Represents a release diff --git a/doc/api/personal_access_tokens.md b/doc/api/personal_access_tokens.md index 162ba88f727..517e26f3d85 100644 --- a/doc/api/personal_access_tokens.md +++ b/doc/api/personal_access_tokens.md @@ -4,7 +4,7 @@ You can read more about [personal access tokens](../user/profile/personal_access ## List personal access tokens -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/22726) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.3. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/227264) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.3. Get a list of personal access tokens. @@ -60,3 +60,29 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a } ] ``` + +## Revoke a personal access token + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216004) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.3. + +Revoke a personal access token. + +```plaintext +DELETE /personal_access_tokens/:id +``` + +| Attribute | Type | required | Description | +|-----------|---------|----------|---------------------| +| `id` | integer/string | yes | ID of personal access token | + +NOTE: **Note:** +Non-administrators can revoke their own tokens. Administrators can revoke tokens of any user. + +```shell +curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/personal_access_tokens/<personal_access_token_id>" +``` + +### Responses + +- `204: No Content` if successfully revoked. +- `400 Bad Request` if not revoked successfully. diff --git a/doc/api/vulnerabilities.md b/doc/api/vulnerabilities.md index 70f29d961e3..a0d871af127 100644 --- a/doc/api/vulnerabilities.md +++ b/doc/api/vulnerabilities.md @@ -6,7 +6,7 @@ NOTE: **Note:** The former Vulnerabilities API was renamed to Vulnerability Findings API and its documentation was moved to [a different location](vulnerability_findings.md). This document now describes the new Vulnerabilities API that provides access to -[Standalone Vulnerabilities](https://gitlab.com/groups/gitlab-org/-/epics/634). +[Vulnerabilities](https://gitlab.com/groups/gitlab-org/-/epics/634). CAUTION: **Caution:** This API is in an alpha stage and considered unstable. diff --git a/doc/api/vulnerability_findings.md b/doc/api/vulnerability_findings.md index e21d903e474..96171f0229d 100644 --- a/doc/api/vulnerability_findings.md +++ b/doc/api/vulnerability_findings.md @@ -4,7 +4,7 @@ NOTE: **Note:** This API resource is renamed from Vulnerabilities to Vulnerability Findings because the Vulnerabilities are reserved -for serving the upcoming [Standalone Vulnerability objects](https://gitlab.com/gitlab-org/gitlab/-/issues/13561). +for serving [Vulnerability objects](https://gitlab.com/gitlab-org/gitlab/-/issues/13561). To fix any broken integrations with the former Vulnerabilities API, change the `vulnerabilities` URL part to be `vulnerability_findings`. diff --git a/doc/ci/parent_child_pipelines.md b/doc/ci/parent_child_pipelines.md index a8ae49543a6..1cfa698bfa5 100644 --- a/doc/ci/parent_child_pipelines.md +++ b/doc/ci/parent_child_pipelines.md @@ -43,8 +43,8 @@ Child pipelines work well with other GitLab CI/CD features: - Since the parent pipeline in `.gitlab-ci.yml` and the child pipeline run as normal pipelines, they can have their own behaviors and sequencing in relation to triggers. -All of this will work with the [`include:`](yaml/README.md#include) feature so you can compose -the child pipeline configuration. +See the [`trigger:`](yaml/README.md#trigger) keyword documentation for full details on how to +include the child pipeline configuration. <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> For an overview, see [Parent-Child Pipelines feature demo](https://youtu.be/n8KpBSqZNbk). diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md index 1c778788827..c252f6425d0 100644 --- a/doc/development/documentation/styleguide.md +++ b/doc/development/documentation/styleguide.md @@ -360,10 +360,10 @@ Credit: [Avoid ableist language](https://developers.google.com/style/inclusive-d Avoid terms that reflect negative cultural stereotypes and history. In most cases, you can replace terms such as `master` and `slave` with terms that are more precise and functional, such as `primary` and `secondary`. -| Use | Avoid | -|-----------------------|----------------------| -| Primary / secondary | Master / slave | -| Blacklist / whitelist | Allowlist / denylist | +| Use | Avoid | +|----------------------|-----------------------| +| Primary / secondary | Master / slave | +| Allowlist / denylist | Blacklist / whitelist | For more information see the following [Internet Draft specification](https://tools.ietf.org/html/draft-knodel-terminology-02). diff --git a/doc/operations/incident_management/img/incident_list.png b/doc/operations/incident_management/img/incident_list.png Binary files differnew file mode 100644 index 00000000000..0498fec6c9c --- /dev/null +++ b/doc/operations/incident_management/img/incident_list.png diff --git a/doc/operations/incident_management/index.md b/doc/operations/incident_management/index.md index 5db6c76a42b..a44d2cc0807 100644 --- a/doc/operations/incident_management/index.md +++ b/doc/operations/incident_management/index.md @@ -16,9 +16,7 @@ GitLab offers solutions for handling incidents in your applications and services such as [setting up Prometheus alerts](#configure-prometheus-alerts), [displaying metrics](#embed-metrics-in-incidents-and-issues), and sending notifications. While no configuration is required to use the [manual features](#create-an-incident-manually) -of incident management, both automation and [configuration](#configure-incidents-ultimate) -of incident management are only available in -[GitLab Ultimate and GitLab.com Gold](https://about.gitlab.com/pricing/). +of incident management, some simple [configuration](#configure-incidents) is needed to automate incident creation. For users with at least Developer [permissions](../../user/permissions.md), the Incident Management list is available at **Operations > Incidents** @@ -328,7 +326,7 @@ You can be alerted via a Slack message when a new alert has been received. See the [Slack Notifications Service docs](../../user/project/integrations/slack.md) for information on how to set this up. -## Configure incidents **(ULTIMATE)** +## Configure incidents > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/4925) in GitLab Ultimate 11.11. @@ -366,7 +364,23 @@ sends these emails to [owners and maintainers](../../user/permissions.md) of the These emails contain details of the alert, and a link for more information. To send separate email notifications to users with -[Developer permissions](../../user/permissions.md), see [Configure incidents](#configure-incidents-ultimate). +[Developer permissions](../../user/permissions.md), see [Configure incidents](#configure-incidents). + +## Incident List + +Incidents in GitLab are aggregated in the Incident List, available at +**Operations > Incidents**. This list displays all incidents in GitLab, with tabs +to display open incidents, closed incidents, and all incidents: + +![Incident list](img/incident_list.png) + +The list displays the following attributes: + +- **Incident title** +- **Date created** - in 'time ago' format. +- **Assignees** - the avatar of the user assigned to the incident. +- **Published** - Displays a green check mark (**{check-circle}**) if the incident is published + to a [Status Page](status_page.md). ## Create an incident manually diff --git a/doc/operations/metrics/alerts.md b/doc/operations/metrics/alerts.md index 6b5cbab8399..2ed8de9396a 100644 --- a/doc/operations/metrics/alerts.md +++ b/doc/operations/metrics/alerts.md @@ -14,7 +14,6 @@ your team when environment performance falls outside of the boundaries you set. ## Managed Prometheus instances > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6590) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.2 for [custom metrics](index.md#adding-custom-metrics), and GitLab 11.3 for [library metrics](../../user/project/integrations/prometheus_library/metrics.md). -> - Runbook URLs [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39315) in GitLab 13.3. For managed Prometheus instances using auto configuration, you can [configure alerts for metrics](index.md#adding-custom-metrics) directly in the @@ -32,6 +31,18 @@ For managed Prometheus instances using auto configuration, you can To remove the alert, click back on the alert icon for the desired metric, and click **Delete**. +### Link runbooks to alerts + +> - Runbook URLs [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39315) in GitLab 13.3. + +When creating alerts from the metrics dashboard for [managed Prometheus instances](#managed-prometheus-instances), +you can also link a runbook. When the alert triggers, the +[chart context menu](dashboards/index.md#chart-context-menu) on the metrics chart +links to the runbook, making it easy for you to locate and access the correct runbook +as soon as the alert fires: + +![Linked Runbook in charts](img/linked_runbooks_on_charts.png) + ## External Prometheus instances >- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9258) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.8. diff --git a/doc/operations/metrics/dashboards/img/panel_context_menu_v13_0.png b/doc/operations/metrics/dashboards/img/panel_context_menu_v13_0.png Binary files differdeleted file mode 100644 index 2d7cb923981..00000000000 --- a/doc/operations/metrics/dashboards/img/panel_context_menu_v13_0.png +++ /dev/null diff --git a/doc/operations/metrics/dashboards/img/panel_context_menu_v13_3.png b/doc/operations/metrics/dashboards/img/panel_context_menu_v13_3.png Binary files differnew file mode 100644 index 00000000000..1259917608b --- /dev/null +++ b/doc/operations/metrics/dashboards/img/panel_context_menu_v13_3.png diff --git a/doc/operations/metrics/dashboards/index.md b/doc/operations/metrics/dashboards/index.md index 11d2dc45008..ffcb7dc92c6 100644 --- a/doc/operations/metrics/dashboards/index.md +++ b/doc/operations/metrics/dashboards/index.md @@ -136,7 +136,7 @@ You can take action related to a chart's data by clicking the **{ellipsis_v}** **More actions** dropdown box above the upper right corner of any chart on a dashboard: -![Context Menu](img/panel_context_menu_v13_0.png) +![Context Menu](img/panel_context_menu_v13_3.png) The options are: @@ -148,7 +148,10 @@ The options are: feature, logs narrow down to the selected time range. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/122013) in GitLab 12.8.) - **Download CSV** - Data from Prometheus charts on the metrics dashboard can be downloaded as CSV. - [Copy link to chart](../embed.md#embedding-gitlab-managed-kubernetes-metrics) -- [Alerts](../alerts.md) +- **Alerts** - Display any [alerts](../alerts.md) configured for this metric. +- **View Runbook** - Displays the runbook for an alert. For information about configuring + runbooks, read [Set up alerts for Prometheus metrics](../alerts.md). + ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/211844) in GitLab 13.3.) ### Timeline zoom and URL sharing diff --git a/doc/operations/metrics/img/linked_runbooks_on_charts.png b/doc/operations/metrics/img/linked_runbooks_on_charts.png Binary files differnew file mode 100644 index 00000000000..335ba5dc172 --- /dev/null +++ b/doc/operations/metrics/img/linked_runbooks_on_charts.png diff --git a/doc/user/application_security/security_dashboard/img/standalone_vulnerability_page_v13_1.png b/doc/user/application_security/security_dashboard/img/vulnerability_page_v13_1.png Binary files differindex 9cf95b197fe..9cf95b197fe 100644 --- a/doc/user/application_security/security_dashboard/img/standalone_vulnerability_page_v13_1.png +++ b/doc/user/application_security/security_dashboard/img/vulnerability_page_v13_1.png diff --git a/doc/user/application_security/security_dashboard/index.md b/doc/user/application_security/security_dashboard/index.md index e4a3345d321..b8fcc513cb1 100644 --- a/doc/user/application_security/security_dashboard/index.md +++ b/doc/user/application_security/security_dashboard/index.md @@ -203,7 +203,7 @@ Clicking any vulnerability in the table takes you to its [Vulnerability Details](../vulnerabilities) page to see more information on that vulnerability. To create an issue associated with the vulnerability, click the **Create Issue** button. -![Create an issue for the vulnerability](img/standalone_vulnerability_page_v13_1.png) +![Create an issue for the vulnerability](img/vulnerability_page_v13_1.png) Once you create the issue, the vulnerability list contains a link to the issue and an icon whose color indicates the issue's status (green for open issues, blue for closed issues). diff --git a/doc/user/application_security/vulnerabilities/img/standalone_vulnerability_page_download_patch_button_v13_1.png b/doc/user/application_security/vulnerabilities/img/vulnerability_page_download_patch_button_v13_1.png Binary files differindex b925c342a11..b925c342a11 100644 --- a/doc/user/application_security/vulnerabilities/img/standalone_vulnerability_page_download_patch_button_v13_1.png +++ b/doc/user/application_security/vulnerabilities/img/vulnerability_page_download_patch_button_v13_1.png diff --git a/doc/user/application_security/vulnerabilities/img/standalone_vulnerability_page_merge_request_button_dropdown_v13_1.png b/doc/user/application_security/vulnerabilities/img/vulnerability_page_merge_request_button_dropdown_v13_1.png Binary files differindex 05ca74c3d5c..05ca74c3d5c 100644 --- a/doc/user/application_security/vulnerabilities/img/standalone_vulnerability_page_merge_request_button_dropdown_v13_1.png +++ b/doc/user/application_security/vulnerabilities/img/vulnerability_page_merge_request_button_dropdown_v13_1.png diff --git a/doc/user/application_security/vulnerabilities/img/standalone_vulnerability_page_merge_request_button_v13_1.png b/doc/user/application_security/vulnerabilities/img/vulnerability_page_merge_request_button_v13_1.png Binary files differindex a3034a7db04..a3034a7db04 100644 --- a/doc/user/application_security/vulnerabilities/img/standalone_vulnerability_page_merge_request_button_v13_1.png +++ b/doc/user/application_security/vulnerabilities/img/vulnerability_page_merge_request_button_v13_1.png diff --git a/doc/user/application_security/vulnerabilities/img/standalone_vulnerability_page_v13_1.png b/doc/user/application_security/vulnerabilities/img/vulnerability_page_v13_1.png Binary files differindex 30a7195e1ab..30a7195e1ab 100644 --- a/doc/user/application_security/vulnerabilities/img/standalone_vulnerability_page_v13_1.png +++ b/doc/user/application_security/vulnerabilities/img/vulnerability_page_v13_1.png diff --git a/doc/user/application_security/vulnerabilities/index.md b/doc/user/application_security/vulnerabilities/index.md index d5cce6434d8..ffec4bf336d 100644 --- a/doc/user/application_security/vulnerabilities/index.md +++ b/doc/user/application_security/vulnerabilities/index.md @@ -5,16 +5,16 @@ group: Threat Insights info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers --- -# Standalone Vulnerability pages +# Vulnerability Pages > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13561) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.0. Each security vulnerability in the [Security Dashboard](../security_dashboard/index.md#project-security-dashboard) has its own standalone page. -![Standalone vulnerability page](img/standalone_vulnerability_page_v13_1.png) +![Vulnerability page](img/vulnerability_page_v13_1.png) -On the standalone vulnerability page, you can interact with the vulnerability in +On the vulnerability page, you can interact with the vulnerability in several different ways: - [Change the Vulnerability Status](#changing-vulnerability-status) - You can change the @@ -57,7 +57,7 @@ generates for you. GitLab supports the following scanners: When an automatic solution is available, the button in the header will show "Resolve with merge request": -![Resolve with Merge Request button](img/standalone_vulnerability_page_merge_request_button_v13_1.png) +![Resolve with Merge Request button](img/vulnerability_page_merge_request_button_v13_1.png) Selecting the button will create a merge request with the automatic solution. @@ -66,8 +66,8 @@ Selecting the button will create a merge request with the automatic solution. To manually apply the patch that was generated by GitLab for a vulnerability, select the dropdown arrow on the "Resolve with merge request" button, then select the "Download patch to resolve" option: -![Resolve with Merge Request button dropdown](img/standalone_vulnerability_page_merge_request_button_dropdown_v13_1.png) +![Resolve with Merge Request button dropdown](img/vulnerability_page_merge_request_button_dropdown_v13_1.png) This will change the button text to "Download patch to resolve". Click on it to download the patch: -![Download patch button](img/standalone_vulnerability_page_download_patch_button_v13_1.png) +![Download patch button](img/vulnerability_page_download_patch_button_v13_1.png) diff --git a/doc/user/group/saml_sso/group_managed_accounts.md b/doc/user/group/saml_sso/group_managed_accounts.md index 08455dc4725..126970ebbb6 100644 --- a/doc/user/group/saml_sso/group_managed_accounts.md +++ b/doc/user/group/saml_sso/group_managed_accounts.md @@ -7,8 +7,11 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Group Managed Accounts **(PREMIUM)** -CAUTION: **Warning:** -This is a [Closed Beta](https://about.gitlab.com/handbook/product/#closed-beta) feature. +CAUTION: **Caution:** +This [Closed Beta](https://about.gitlab.com/handbook/product/#closed-beta) feature is being re-evaluated in favor of a different +[identity model](https://gitlab.com/gitlab-org/gitlab/-/issues/218631) that does not require separate accounts. +We recommend that group administrators who haven't yet implemented this feature wait for +the new solution. > - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/709) in GitLab 12.1. > - It's deployed behind a feature flag, disabled by default. diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index d8e8ab2fab7..800eb1d3359 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -1301,6 +1301,58 @@ X-Gitlab-Event: Job Hook Note that `commit.id` is the ID of the pipeline, not the ID of the commit. +### Deployment events + +Triggered when deployment is finished/failed/canceled. + +**Request Header**: + +```plaintext +X-Gitlab-Event: Deployment Hook +``` + +**Request Body**: + +```json +{ + "object_kind": "deployment", + "status": "success", + "deployable_id": 796, + "deployable_url": "http://10.126.0.2:3000/root/test-deployment-webhooks/-/jobs/796", + "environment": "staging", + "project": { + "id": 30, + "name": "test-deployment-webhooks", + "description": "", + "web_url": "http://10.126.0.2:3000/root/test-deployment-webhooks", + "avatar_url": null, + "git_ssh_url": "ssh://vlad@10.126.0.2:2222/root/test-deployment-webhooks.git", + "git_http_url": "http://10.126.0.2:3000/root/test-deployment-webhooks.git", + "namespace": "Administrator", + "visibility_level": 0, + "path_with_namespace": "root/test-deployment-webhooks", + "default_branch": "master", + "ci_config_path": "", + "homepage": "http://10.126.0.2:3000/root/test-deployment-webhooks", + "url": "ssh://vlad@10.126.0.2:2222/root/test-deployment-webhooks.git", + "ssh_url": "ssh://vlad@10.126.0.2:2222/root/test-deployment-webhooks.git", + "http_url": "http://10.126.0.2:3000/root/test-deployment-webhooks.git" + }, + "short_sha": "279484c0", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "email": "admin@example.com" + }, + "user_url": "http://10.126.0.2:3000/root", + "commit_url": "http://10.126.0.2:3000/root/test-deployment-webhooks/-/commit/279484c09fbe69ededfced8c1bb6e6d24616b468", + "commit_title": "Add new file" +} +``` + +Note that `deployable_id` is the ID of the CI job. + ## Image URL rewriting From GitLab 11.2, simple image references are rewritten to use an absolute URL diff --git a/doc/user/project/issues/design_management.md b/doc/user/project/issues/design_management.md index 4e7d460e7c7..371469a6ed6 100644 --- a/doc/user/project/issues/design_management.md +++ b/doc/user/project/issues/design_management.md @@ -206,13 +206,10 @@ viewed by browsing previous versions. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34382) in GitLab 13.3. -You can change designs order with dragging design to the new position: +You can change the order of designs by dragging them to a new position: ![Reorder designs](img/designs_reordering_v13_3.gif) -NOTE: **Note:** -You can reorder designs only on the latest version. - ## Starting discussions on designs When a design is uploaded, you can start a discussion by clicking on diff --git a/doc/user/project/merge_requests/fail_fast_testing.md b/doc/user/project/merge_requests/fail_fast_testing.md index 619a6d04577..60f81159394 100644 --- a/doc/user/project/merge_requests/fail_fast_testing.md +++ b/doc/user/project/merge_requests/fail_fast_testing.md @@ -45,8 +45,9 @@ This template requires: - Use [Pipelines for Merge Requests](../../../ci/merge_request_pipelines/index.md#configuring-pipelines-for-merge-requests) - [Pipelines for Merged Results](../../../ci/merge_request_pipelines/pipelines_for_merged_results/index.md#enable-pipelines-for-merged-results) enabled in the project settings. +- A Docker image with Ruby available. The template uses `image: ruby:2.6` by default, but you [can override](../../../ci/yaml/includes.md#overriding-external-template-values) this. -## Configure Fast RSpec Failure +## Configuring Fast RSpec Failure We'll use the following plain RSpec configuration as a starting point. It installs all the project gems and executes `rspec`, on merge request pipelines only. @@ -69,6 +70,16 @@ include: - template: Verify/FailFast.gitlab-ci.yml ``` +To customize the job, specific options may be set to override the template. For example, to override the default Docker image: + +```yaml +include: + - template: Verify/FailFast.gitlab-ci.yml + +rspec-rails-modified-path-specs: + image: custom-docker-image-with-ruby +``` + ### Example test loads For illustrative purposes, let's say our Rails app spec suite consists of 100 specs per model for ten models. diff --git a/doc/user/project/merge_requests/load_performance_testing.md b/doc/user/project/merge_requests/load_performance_testing.md index 3239269109d..97f4f202ab3 100644 --- a/doc/user/project/merge_requests/load_performance_testing.md +++ b/doc/user/project/merge_requests/load_performance_testing.md @@ -141,7 +141,8 @@ For example, you can override the duration of the test with a CLI option: GitLab only displays the key performance metrics in the MR widget if k6's results are saved via [summary export](https://k6.io/docs/results-visualization/json#summary-export) as a [Load Performance report artifact](../../../ci/pipelines/job_artifacts.md#artifactsreportsload_performance-premium). -The latest Load Performance artifact available is always used. +The latest Load Performance artifact available is always used, using the +summary values from the test. If [GitLab Pages](../pages/index.md) is enabled, you can view the report directly in your browser. diff --git a/doc/user/project/merge_requests/test_coverage_visualization.md b/doc/user/project/merge_requests/test_coverage_visualization.md index 793cedb0210..6751dde155c 100644 --- a/doc/user/project/merge_requests/test_coverage_visualization.md +++ b/doc/user/project/merge_requests/test_coverage_visualization.md @@ -54,6 +54,10 @@ from any job in any stage in the pipeline. The coverage will be displayed for ea Hovering over the coverage bar will provide further information, such as the number of times the line was checked by tests. +NOTE: **Note:** +The Cobertura XML parser currently does not support the `sources` element and ignores it. It is assumed that +the `filename` of a `class` element contains the full path relative to the project root. + ## Example test coverage configuration The following [`gitlab-ci.yml`](../../../ci/yaml/README.md) example uses [Mocha](https://mochajs.org/) diff --git a/lib/api/admin/ci/variables.rb b/lib/api/admin/ci/variables.rb index 6b0ff5e9395..8721d94d642 100644 --- a/lib/api/admin/ci/variables.rb +++ b/lib/api/admin/ci/variables.rb @@ -12,7 +12,7 @@ module API namespace 'ci' do namespace 'variables' do desc 'Get instance-level variables' do - success Entities::Variable + success Entities::Ci::Variable end params do use :pagination @@ -20,11 +20,11 @@ module API get '/' do variables = ::Ci::InstanceVariable.all - present paginate(variables), with: Entities::Variable + present paginate(variables), with: Entities::Ci::Variable end desc 'Get a specific variable from a group' do - success Entities::Variable + success Entities::Ci::Variable end params do requires :key, type: String, desc: 'The key of the variable' @@ -35,11 +35,11 @@ module API break not_found!('InstanceVariable') unless variable - present variable, with: Entities::Variable + present variable, with: Entities::Ci::Variable end desc 'Create a new instance-level variable' do - success Entities::Variable + success Entities::Ci::Variable end params do requires :key, @@ -69,14 +69,14 @@ module API variable = ::Ci::InstanceVariable.new(variable_params) if variable.save - present variable, with: Entities::Variable + present variable, with: Entities::Ci::Variable else render_validation_error!(variable) end end desc 'Update an existing instance-variable' do - success Entities::Variable + success Entities::Ci::Variable end params do optional :key, @@ -108,14 +108,14 @@ module API variable_params = declared_params(include_missing: false).except(:key) if variable.update(variable_params) - present variable, with: Entities::Variable + present variable, with: Entities::Ci::Variable else render_validation_error!(variable) end end desc 'Delete an existing instance-level variable' do - success Entities::Variable + success Entities::Ci::Variable end params do requires :key, type: String, desc: 'The key of the variable' diff --git a/lib/api/ci/pipeline_schedules.rb b/lib/api/ci/pipeline_schedules.rb index 80ad8aa04dd..1afdb0ad34c 100644 --- a/lib/api/ci/pipeline_schedules.rb +++ b/lib/api/ci/pipeline_schedules.rb @@ -12,7 +12,7 @@ module API end resource :projects, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get all pipeline schedules' do - success Entities::PipelineSchedule + success Entities::Ci::PipelineSchedule end params do use :pagination @@ -25,22 +25,22 @@ module API schedules = ::Ci::PipelineSchedulesFinder.new(user_project).execute(scope: params[:scope]) .preload([:owner, :last_pipeline]) - present paginate(schedules), with: Entities::PipelineSchedule + present paginate(schedules), with: Entities::Ci::PipelineSchedule end # rubocop: enable CodeReuse/ActiveRecord desc 'Get a single pipeline schedule' do - success Entities::PipelineScheduleDetails + success Entities::Ci::PipelineScheduleDetails end params do requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' end get ':id/pipeline_schedules/:pipeline_schedule_id' do - present pipeline_schedule, with: Entities::PipelineScheduleDetails + present pipeline_schedule, with: Entities::Ci::PipelineScheduleDetails end desc 'Create a new pipeline schedule' do - success Entities::PipelineScheduleDetails + success Entities::Ci::PipelineScheduleDetails end params do requires :description, type: String, desc: 'The description of pipeline schedule' @@ -57,14 +57,14 @@ module API .execute if pipeline_schedule.persisted? - present pipeline_schedule, with: Entities::PipelineScheduleDetails + present pipeline_schedule, with: Entities::Ci::PipelineScheduleDetails else render_validation_error!(pipeline_schedule) end end desc 'Edit a pipeline schedule' do - success Entities::PipelineScheduleDetails + success Entities::Ci::PipelineScheduleDetails end params do requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' @@ -78,14 +78,14 @@ module API authorize! :update_pipeline_schedule, pipeline_schedule if pipeline_schedule.update(declared_params(include_missing: false)) - present pipeline_schedule, with: Entities::PipelineScheduleDetails + present pipeline_schedule, with: Entities::Ci::PipelineScheduleDetails else render_validation_error!(pipeline_schedule) end end desc 'Take ownership of a pipeline schedule' do - success Entities::PipelineScheduleDetails + success Entities::Ci::PipelineScheduleDetails end params do requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' @@ -94,14 +94,14 @@ module API authorize! :update_pipeline_schedule, pipeline_schedule if pipeline_schedule.own!(current_user) - present pipeline_schedule, with: Entities::PipelineScheduleDetails + present pipeline_schedule, with: Entities::Ci::PipelineScheduleDetails else render_validation_error!(pipeline_schedule) end end desc 'Delete a pipeline schedule' do - success Entities::PipelineScheduleDetails + success Entities::Ci::PipelineScheduleDetails end params do requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' @@ -132,7 +132,7 @@ module API end desc 'Create a new pipeline schedule variable' do - success Entities::Variable + success Entities::Ci::Variable end params do requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' @@ -146,14 +146,14 @@ module API variable_params = declared_params(include_missing: false) variable = pipeline_schedule.variables.create(variable_params) if variable.persisted? - present variable, with: Entities::Variable + present variable, with: Entities::Ci::Variable else render_validation_error!(variable) end end desc 'Edit a pipeline schedule variable' do - success Entities::Variable + success Entities::Ci::Variable end params do requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' @@ -165,14 +165,14 @@ module API authorize! :update_pipeline_schedule, pipeline_schedule if pipeline_schedule_variable.update(declared_params(include_missing: false)) - present pipeline_schedule_variable, with: Entities::Variable + present pipeline_schedule_variable, with: Entities::Ci::Variable else render_validation_error!(pipeline_schedule_variable) end end desc 'Delete a pipeline schedule variable' do - success Entities::Variable + success Entities::Ci::Variable end params do requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' @@ -182,7 +182,7 @@ module API authorize! :admin_pipeline_schedule, pipeline_schedule status :accepted - present pipeline_schedule_variable.destroy, with: Entities::Variable + present pipeline_schedule_variable.destroy, with: Entities::Ci::Variable end end diff --git a/lib/api/ci/pipelines.rb b/lib/api/ci/pipelines.rb index 4fb301f0260..bbbf3b683c5 100644 --- a/lib/api/ci/pipelines.rb +++ b/lib/api/ci/pipelines.rb @@ -13,7 +13,7 @@ module API resource :projects, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get all Pipelines of the project' do detail 'This feature was introduced in GitLab 8.11.' - success Entities::PipelineBasic + success Entities::Ci::PipelineBasic end params do use :pagination @@ -38,12 +38,12 @@ module API authorize! :read_build, user_project pipelines = ::Ci::PipelinesFinder.new(user_project, current_user, params).execute - present paginate(pipelines), with: Entities::PipelineBasic + present paginate(pipelines), with: Entities::Ci::PipelineBasic end desc 'Create a new pipeline' do detail 'This feature was introduced in GitLab 8.14' - success Entities::Pipeline + success Entities::Ci::Pipeline end params do requires :ref, type: String, desc: 'Reference' @@ -64,7 +64,7 @@ module API .execute(:api, ignore_skip_ci: true, save_on_errors: false) if new_pipeline.persisted? - present new_pipeline, with: Entities::Pipeline + present new_pipeline, with: Entities::Ci::Pipeline else render_validation_error!(new_pipeline) end @@ -72,7 +72,7 @@ module API desc 'Gets a the latest pipeline for the project branch' do detail 'This feature was introduced in GitLab 12.3' - success Entities::Pipeline + success Entities::Ci::Pipeline end params do optional :ref, type: String, desc: 'branch ref of pipeline' @@ -80,12 +80,12 @@ module API get ':id/pipelines/latest' do authorize! :read_pipeline, latest_pipeline - present latest_pipeline, with: Entities::Pipeline + present latest_pipeline, with: Entities::Ci::Pipeline end desc 'Gets a specific pipeline for the project' do detail 'This feature was introduced in GitLab 8.11' - success Entities::Pipeline + success Entities::Ci::Pipeline end params do requires :pipeline_id, type: Integer, desc: 'The pipeline ID' @@ -93,12 +93,12 @@ module API get ':id/pipelines/:pipeline_id' do authorize! :read_pipeline, pipeline - present pipeline, with: Entities::Pipeline + present pipeline, with: Entities::Ci::Pipeline end desc 'Gets the variables for a given pipeline' do detail 'This feature was introduced in GitLab 11.11' - success Entities::Variable + success Entities::Ci::Variable end params do requires :pipeline_id, type: Integer, desc: 'The pipeline ID' @@ -106,7 +106,7 @@ module API get ':id/pipelines/:pipeline_id/variables' do authorize! :read_pipeline_variable, pipeline - present pipeline.variables, with: Entities::Variable + present pipeline.variables, with: Entities::Ci::Variable end desc 'Gets the test report for a given pipeline' do @@ -141,7 +141,7 @@ module API desc 'Retry builds in the pipeline' do detail 'This feature was introduced in GitLab 8.11.' - success Entities::Pipeline + success Entities::Ci::Pipeline end params do requires :pipeline_id, type: Integer, desc: 'The pipeline ID' @@ -151,12 +151,12 @@ module API pipeline.retry_failed(current_user) - present pipeline, with: Entities::Pipeline + present pipeline, with: Entities::Ci::Pipeline end desc 'Cancel all builds in the pipeline' do detail 'This feature was introduced in GitLab 8.11.' - success Entities::Pipeline + success Entities::Ci::Pipeline end params do requires :pipeline_id, type: Integer, desc: 'The pipeline ID' @@ -167,7 +167,7 @@ module API pipeline.cancel_running status 200 - present pipeline.reset, with: Entities::Pipeline + present pipeline.reset, with: Entities::Ci::Pipeline end end diff --git a/lib/api/ci/runners.rb b/lib/api/ci/runners.rb index 2c156a71160..7bca72f8028 100644 --- a/lib/api/ci/runners.rb +++ b/lib/api/ci/runners.rb @@ -111,7 +111,7 @@ module API end desc 'List jobs running on a runner' do - success Entities::JobBasicWithProject + success Entities::Ci::JobBasicWithProject end params do requires :id, type: Integer, desc: 'The ID of the runner' @@ -126,7 +126,7 @@ module API jobs = ::Ci::RunnerJobsFinder.new(runner, params).execute - present paginate(jobs), with: Entities::JobBasicWithProject + present paginate(jobs), with: Entities::Ci::JobBasicWithProject end end diff --git a/lib/api/entities/bridge.rb b/lib/api/entities/bridge.rb deleted file mode 100644 index 8f0ee69399a..00000000000 --- a/lib/api/entities/bridge.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - class Bridge < Entities::JobBasic - expose :downstream_pipeline, with: Entities::PipelineBasic - end - end -end diff --git a/lib/api/entities/ci/bridge.rb b/lib/api/entities/ci/bridge.rb new file mode 100644 index 00000000000..502d97fff90 --- /dev/null +++ b/lib/api/entities/ci/bridge.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + class Bridge < JobBasic + expose :downstream_pipeline, with: ::API::Entities::Ci::PipelineBasic + end + end + end +end diff --git a/lib/api/entities/ci/job.rb b/lib/api/entities/ci/job.rb new file mode 100644 index 00000000000..7fe1a802e24 --- /dev/null +++ b/lib/api/entities/ci/job.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + class Job < JobBasic + # artifacts_file is included in job_artifacts, but kept for backward compatibility (remove in api/v5) + expose :artifacts_file, using: ::API::Entities::Ci::JobArtifactFile, if: -> (job, opts) { job.artifacts? } + expose :job_artifacts, as: :artifacts, using: ::API::Entities::Ci::JobArtifact + expose :runner, with: ::API::Entities::Runner + expose :artifacts_expire_at + end + end + end +end diff --git a/lib/api/entities/ci/job_artifact.rb b/lib/api/entities/ci/job_artifact.rb new file mode 100644 index 00000000000..9e504aee383 --- /dev/null +++ b/lib/api/entities/ci/job_artifact.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + class JobArtifact < Grape::Entity + expose :file_type, :size, :filename, :file_format + end + end + end +end diff --git a/lib/api/entities/ci/job_artifact_file.rb b/lib/api/entities/ci/job_artifact_file.rb new file mode 100644 index 00000000000..418eb408ab6 --- /dev/null +++ b/lib/api/entities/ci/job_artifact_file.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + class JobArtifactFile < Grape::Entity + expose :filename + expose :cached_size, as: :size + end + end + end +end diff --git a/lib/api/entities/ci/job_basic.rb b/lib/api/entities/ci/job_basic.rb new file mode 100644 index 00000000000..a29788c7abf --- /dev/null +++ b/lib/api/entities/ci/job_basic.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + class JobBasic < Grape::Entity + expose :id, :status, :stage, :name, :ref, :tag, :coverage, :allow_failure + expose :created_at, :started_at, :finished_at + expose :duration + expose :user, with: ::API::Entities::User + expose :commit, with: ::API::Entities::Commit + expose :pipeline, with: ::API::Entities::Ci::PipelineBasic + + expose :web_url do |job, _options| + Gitlab::Routing.url_helpers.project_job_url(job.project, job) + end + end + end + end +end diff --git a/lib/api/entities/ci/job_basic_with_project.rb b/lib/api/entities/ci/job_basic_with_project.rb new file mode 100644 index 00000000000..736e611e5b1 --- /dev/null +++ b/lib/api/entities/ci/job_basic_with_project.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + class JobBasicWithProject < Entities::Ci::JobBasic + expose :project, with: Entities::ProjectIdentity + end + end + end +end diff --git a/lib/api/entities/ci/pipeline.rb b/lib/api/entities/ci/pipeline.rb new file mode 100644 index 00000000000..3dd3b9c9eff --- /dev/null +++ b/lib/api/entities/ci/pipeline.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + class Pipeline < PipelineBasic + expose :before_sha, :tag, :yaml_errors + + expose :user, with: Entities::UserBasic + expose :created_at, :updated_at, :started_at, :finished_at, :committed_at + expose :duration + expose :coverage + expose :detailed_status, using: DetailedStatusEntity do |pipeline, options| + pipeline.detailed_status(options[:current_user]) + end + end + end + end +end diff --git a/lib/api/entities/ci/pipeline_basic.rb b/lib/api/entities/ci/pipeline_basic.rb new file mode 100644 index 00000000000..dbb9b828757 --- /dev/null +++ b/lib/api/entities/ci/pipeline_basic.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + class PipelineBasic < Grape::Entity + expose :id, :sha, :ref, :status + expose :created_at, :updated_at + + expose :web_url do |pipeline, _options| + Gitlab::Routing.url_helpers.project_pipeline_url(pipeline.project, pipeline) + end + end + end + end +end diff --git a/lib/api/entities/ci/pipeline_schedule.rb b/lib/api/entities/ci/pipeline_schedule.rb new file mode 100644 index 00000000000..f1596b7d285 --- /dev/null +++ b/lib/api/entities/ci/pipeline_schedule.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + class PipelineSchedule < Grape::Entity + expose :id + expose :description, :ref, :cron, :cron_timezone, :next_run_at, :active + expose :created_at, :updated_at + expose :owner, using: ::API::Entities::UserBasic + end + end + end +end diff --git a/lib/api/entities/ci/pipeline_schedule_details.rb b/lib/api/entities/ci/pipeline_schedule_details.rb new file mode 100644 index 00000000000..b233728b95b --- /dev/null +++ b/lib/api/entities/ci/pipeline_schedule_details.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + class PipelineScheduleDetails < PipelineSchedule + expose :last_pipeline, using: ::API::Entities::Ci::PipelineBasic + expose :variables, using: ::API::Entities::Ci::Variable + end + end + end +end diff --git a/lib/api/entities/ci/variable.rb b/lib/api/entities/ci/variable.rb new file mode 100644 index 00000000000..f4d5248245a --- /dev/null +++ b/lib/api/entities/ci/variable.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + class Variable < Grape::Entity + expose :variable_type, :key, :value + expose :protected?, as: :protected, if: -> (entity, _) { entity.respond_to?(:protected?) } + expose :masked?, as: :masked, if: -> (entity, _) { entity.respond_to?(:masked?) } + expose :environment_scope, if: -> (entity, _) { entity.respond_to?(:environment_scope) } + end + end + end +end diff --git a/lib/api/entities/commit_detail.rb b/lib/api/entities/commit_detail.rb index 22424b38bb9..61238102e9d 100644 --- a/lib/api/entities/commit_detail.rb +++ b/lib/api/entities/commit_detail.rb @@ -9,7 +9,7 @@ module API expose :last_pipeline do |commit, options| pipeline = commit.last_pipeline if can_read_pipeline? - ::API::Entities::PipelineBasic.represent(pipeline, options) + ::API::Entities::Ci::PipelineBasic.represent(pipeline, options) end private diff --git a/lib/api/entities/deployment.rb b/lib/api/entities/deployment.rb index 3a97d3e3c09..4e3a4c289d9 100644 --- a/lib/api/entities/deployment.rb +++ b/lib/api/entities/deployment.rb @@ -6,7 +6,7 @@ module API expose :id, :iid, :ref, :sha, :created_at, :updated_at expose :user, using: Entities::UserBasic expose :environment, using: Entities::EnvironmentBasic - expose :deployable, using: Entities::Job + expose :deployable, using: Entities::Ci::Job expose :status end end diff --git a/lib/api/entities/job.rb b/lib/api/entities/job.rb deleted file mode 100644 index cbee8794007..00000000000 --- a/lib/api/entities/job.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - class Job < Entities::JobBasic - # artifacts_file is included in job_artifacts, but kept for backward compatibility (remove in api/v5) - expose :artifacts_file, using: Entities::JobArtifactFile, if: -> (job, opts) { job.artifacts? } - expose :job_artifacts, as: :artifacts, using: Entities::JobArtifact - expose :runner, with: Entities::Runner - expose :artifacts_expire_at - end - end -end diff --git a/lib/api/entities/job_artifact.rb b/lib/api/entities/job_artifact.rb deleted file mode 100644 index 94dbdb38fee..00000000000 --- a/lib/api/entities/job_artifact.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - class JobArtifact < Grape::Entity - expose :file_type, :size, :filename, :file_format - end - end -end diff --git a/lib/api/entities/job_artifact_file.rb b/lib/api/entities/job_artifact_file.rb deleted file mode 100644 index fa2851a7f0e..00000000000 --- a/lib/api/entities/job_artifact_file.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - class JobArtifactFile < Grape::Entity - expose :filename - expose :cached_size, as: :size - end - end -end diff --git a/lib/api/entities/job_basic.rb b/lib/api/entities/job_basic.rb deleted file mode 100644 index a8541039934..00000000000 --- a/lib/api/entities/job_basic.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - class JobBasic < Grape::Entity - expose :id, :status, :stage, :name, :ref, :tag, :coverage, :allow_failure - expose :created_at, :started_at, :finished_at - expose :duration - expose :user, with: Entities::User - expose :commit, with: Entities::Commit - expose :pipeline, with: Entities::PipelineBasic - - expose :web_url do |job, _options| - Gitlab::Routing.url_helpers.project_job_url(job.project, job) - end - end - end -end diff --git a/lib/api/entities/job_basic_with_project.rb b/lib/api/entities/job_basic_with_project.rb deleted file mode 100644 index 09387e045ec..00000000000 --- a/lib/api/entities/job_basic_with_project.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - class JobBasicWithProject < Entities::JobBasic - expose :project, with: Entities::ProjectIdentity - end - end -end diff --git a/lib/api/entities/job_request/dependency.rb b/lib/api/entities/job_request/dependency.rb index 64d779f6575..7d6ec832ba1 100644 --- a/lib/api/entities/job_request/dependency.rb +++ b/lib/api/entities/job_request/dependency.rb @@ -5,7 +5,7 @@ module API module JobRequest class Dependency < Grape::Entity expose :id, :name, :token - expose :artifacts_file, using: Entities::JobArtifactFile, if: ->(job, _) { job.artifacts? } + expose :artifacts_file, using: Entities::Ci::JobArtifactFile, if: ->(job, _) { job.artifacts? } end end end diff --git a/lib/api/entities/merge_request.rb b/lib/api/entities/merge_request.rb index 7fc76a4071e..05ae041c7a9 100644 --- a/lib/api/entities/merge_request.rb +++ b/lib/api/entities/merge_request.rb @@ -23,11 +23,11 @@ module API merge_request.metrics&.first_deployed_to_production_at end - expose :pipeline, using: Entities::PipelineBasic, if: -> (_, options) { build_available?(options) } do |merge_request, _options| + expose :pipeline, using: Entities::Ci::PipelineBasic, if: -> (_, options) { build_available?(options) } do |merge_request, _options| merge_request.metrics&.pipeline end - expose :head_pipeline, using: 'API::Entities::Pipeline', if: -> (_, options) do + expose :head_pipeline, using: '::API::Entities::Ci::Pipeline', if: -> (_, options) do Ability.allowed?(options[:current_user], :read_pipeline, options[:project]) end diff --git a/lib/api/entities/package/pipeline.rb b/lib/api/entities/package/pipeline.rb index e91a12e47fa..0aa888e30ee 100644 --- a/lib/api/entities/package/pipeline.rb +++ b/lib/api/entities/package/pipeline.rb @@ -3,7 +3,7 @@ module API module Entities class Package < Grape::Entity - class Pipeline < ::API::Entities::PipelineBasic + class Pipeline < ::API::Entities::Ci::PipelineBasic expose :user, using: ::API::Entities::UserBasic end end diff --git a/lib/api/entities/pipeline.rb b/lib/api/entities/pipeline.rb deleted file mode 100644 index 778efbe4bcc..00000000000 --- a/lib/api/entities/pipeline.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - class Pipeline < Entities::PipelineBasic - expose :before_sha, :tag, :yaml_errors - - expose :user, with: Entities::UserBasic - expose :created_at, :updated_at, :started_at, :finished_at, :committed_at - expose :duration - expose :coverage - expose :detailed_status, using: DetailedStatusEntity do |pipeline, options| - pipeline.detailed_status(options[:current_user]) - end - end - end -end diff --git a/lib/api/entities/pipeline_basic.rb b/lib/api/entities/pipeline_basic.rb deleted file mode 100644 index 359f6a447ab..00000000000 --- a/lib/api/entities/pipeline_basic.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - class PipelineBasic < Grape::Entity - expose :id, :sha, :ref, :status - expose :created_at, :updated_at - - expose :web_url do |pipeline, _options| - Gitlab::Routing.url_helpers.project_pipeline_url(pipeline.project, pipeline) - end - end - end -end diff --git a/lib/api/entities/pipeline_schedule.rb b/lib/api/entities/pipeline_schedule.rb deleted file mode 100644 index a72fe3f3141..00000000000 --- a/lib/api/entities/pipeline_schedule.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - class PipelineSchedule < Grape::Entity - expose :id - expose :description, :ref, :cron, :cron_timezone, :next_run_at, :active - expose :created_at, :updated_at - expose :owner, using: Entities::UserBasic - end - end -end diff --git a/lib/api/entities/pipeline_schedule_details.rb b/lib/api/entities/pipeline_schedule_details.rb deleted file mode 100644 index 5e54489a0f9..00000000000 --- a/lib/api/entities/pipeline_schedule_details.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - class PipelineScheduleDetails < Entities::PipelineSchedule - expose :last_pipeline, using: Entities::PipelineBasic - expose :variables, using: Entities::Variable - end - end -end diff --git a/lib/api/entities/variable.rb b/lib/api/entities/variable.rb deleted file mode 100644 index 6705df30b2e..00000000000 --- a/lib/api/entities/variable.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - class Variable < Grape::Entity - expose :variable_type, :key, :value - expose :protected?, as: :protected, if: -> (entity, _) { entity.respond_to?(:protected?) } - expose :masked?, as: :masked, if: -> (entity, _) { entity.respond_to?(:masked?) } - expose :environment_scope, if: -> (entity, _) { entity.respond_to?(:environment_scope) } - end - end -end diff --git a/lib/api/group_variables.rb b/lib/api/group_variables.rb index b5ff151f07d..e7b8cd10197 100644 --- a/lib/api/group_variables.rb +++ b/lib/api/group_variables.rb @@ -13,18 +13,18 @@ module API resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get group-level variables' do - success Entities::Variable + success Entities::Ci::Variable end params do use :pagination end get ':id/variables' do variables = user_group.variables - present paginate(variables), with: Entities::Variable + present paginate(variables), with: Entities::Ci::Variable end desc 'Get a specific variable from a group' do - success Entities::Variable + success Entities::Ci::Variable end params do requires :key, type: String, desc: 'The key of the variable' @@ -36,12 +36,12 @@ module API break not_found!('GroupVariable') unless variable - present variable, with: Entities::Variable + present variable, with: Entities::Ci::Variable end # rubocop: enable CodeReuse/ActiveRecord desc 'Create a new variable in a group' do - success Entities::Variable + success Entities::Ci::Variable end params do requires :key, type: String, desc: 'The key of the variable' @@ -58,14 +58,14 @@ module API ).execute if variable.valid? - present variable, with: Entities::Variable + present variable, with: Entities::Ci::Variable else render_validation_error!(variable) end end desc 'Update an existing variable from a group' do - success Entities::Variable + success Entities::Ci::Variable end params do optional :key, type: String, desc: 'The key of the variable' @@ -83,7 +83,7 @@ module API ).execute if variable.valid? - present variable, with: Entities::Variable + present variable, with: Entities::Ci::Variable else render_validation_error!(variable) end @@ -93,7 +93,7 @@ module API # rubocop: enable CodeReuse/ActiveRecord desc 'Delete an existing variable from a group' do - success Entities::Variable + success Entities::Ci::Variable end params do requires :key, type: String, desc: 'The key of the variable' diff --git a/lib/api/job_artifacts.rb b/lib/api/job_artifacts.rb index 61c279a76e9..bc7bc956580 100644 --- a/lib/api/job_artifacts.rb +++ b/lib/api/job_artifacts.rb @@ -94,7 +94,7 @@ module API end desc 'Keep the artifacts to prevent them from being deleted' do - success Entities::Job + success ::API::Entities::Ci::Job end params do requires :job_id, type: Integer, desc: 'The ID of a job' @@ -109,7 +109,7 @@ module API build.keep_artifacts! status 200 - present build, with: Entities::Job + present build, with: ::API::Entities::Ci::Job end desc 'Delete the artifacts files from a job' do diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb index 9fab722b72e..084c146abe7 100644 --- a/lib/api/jobs.rb +++ b/lib/api/jobs.rb @@ -30,7 +30,7 @@ module API end desc 'Get a projects jobs' do - success Entities::Job + success Entities::Ci::Job end params do use :optional_scope @@ -44,12 +44,12 @@ module API builds = filter_builds(builds, params[:scope]) builds = builds.preload(:user, :job_artifacts_archive, :job_artifacts, :runner, pipeline: :project) - present paginate(builds), with: Entities::Job + present paginate(builds), with: Entities::Ci::Job end # rubocop: enable CodeReuse/ActiveRecord desc 'Get pipeline jobs' do - success Entities::Job + success Entities::Ci::Job end params do requires :pipeline_id, type: Integer, desc: 'The pipeline ID' @@ -66,12 +66,12 @@ module API builds = filter_builds(builds, params[:scope]) builds = builds.preload(:job_artifacts_archive, :job_artifacts, project: [:namespace]) - present paginate(builds), with: Entities::Job + present paginate(builds), with: Entities::Ci::Job end # rubocop: enable CodeReuse/ActiveRecord desc 'Get pipeline bridge jobs' do - success Entities::Bridge + success ::API::Entities::Ci::Bridge end params do requires :pipeline_id, type: Integer, desc: 'The pipeline ID' @@ -92,12 +92,12 @@ module API project: [:namespace] ) - present paginate(bridges), with: Entities::Bridge + present paginate(bridges), with: ::API::Entities::Ci::Bridge end # rubocop: enable CodeReuse/ActiveRecord desc 'Get a specific job of a project' do - success Entities::Job + success Entities::Ci::Job end params do requires :job_id, type: Integer, desc: 'The ID of a job' @@ -107,7 +107,7 @@ module API build = find_build!(params[:job_id]) - present build, with: Entities::Job + present build, with: Entities::Ci::Job end # TODO: We should use `present_disk_file!` and leave this implementation for backward compatibility (when build trace @@ -131,7 +131,7 @@ module API end desc 'Cancel a specific job of a project' do - success Entities::Job + success Entities::Ci::Job end params do requires :job_id, type: Integer, desc: 'The ID of a job' @@ -144,11 +144,11 @@ module API build.cancel - present build, with: Entities::Job + present build, with: Entities::Ci::Job end desc 'Retry a specific build of a project' do - success Entities::Job + success Entities::Ci::Job end params do requires :job_id, type: Integer, desc: 'The ID of a build' @@ -162,11 +162,11 @@ module API build = ::Ci::Build.retry(build, current_user) - present build, with: Entities::Job + present build, with: Entities::Ci::Job end desc 'Erase job (remove artifacts and the trace)' do - success Entities::Job + success Entities::Ci::Job end params do requires :job_id, type: Integer, desc: 'The ID of a build' @@ -179,11 +179,11 @@ module API break forbidden!('Job is not erasable!') unless build.erasable? build.erase(erased_by: current_user) - present build, with: Entities::Job + present build, with: Entities::Ci::Job end desc 'Trigger a actionable job (manual, delayed, etc)' do - success Entities::Job + success Entities::Ci::Job detail 'This feature was added in GitLab 8.11' end params do @@ -200,7 +200,7 @@ module API build.play(current_user) status 200 - present build, with: Entities::Job + present build, with: Entities::Ci::Job end end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 53a7a2498a6..6f25df720c4 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -352,16 +352,16 @@ module API end desc 'Get the merge request pipelines' do - success Entities::PipelineBasic + success Entities::Ci::PipelineBasic end get ':id/merge_requests/:merge_request_iid/pipelines' do pipelines = merge_request_pipelines_with_access - present paginate(pipelines), with: Entities::PipelineBasic + present paginate(pipelines), with: Entities::Ci::PipelineBasic end desc 'Create a pipeline for merge request' do - success Entities::Pipeline + success ::API::Entities::Ci::Pipeline end post ':id/merge_requests/:merge_request_iid/pipelines' do pipeline = ::MergeRequests::CreatePipelineService @@ -372,7 +372,7 @@ module API not_allowed! elsif pipeline.persisted? status :ok - present pipeline, with: Entities::Pipeline + present pipeline, with: ::API::Entities::Ci::Pipeline else render_validation_error!(pipeline) end diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index de67a149274..f398bbf3e32 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -11,7 +11,7 @@ module API end resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Trigger a GitLab project pipeline' do - success Entities::Pipeline + success Entities::Ci::Pipeline end params do requires :ref, type: String, desc: 'The commit sha or name of a branch or tag', allow_blank: false @@ -38,7 +38,7 @@ module API if result[:http_status] render_api_error!(result[:message], result[:http_status]) else - present result[:pipeline], with: Entities::Pipeline + present result[:pipeline], with: Entities::Ci::Pipeline end end diff --git a/lib/api/variables.rb b/lib/api/variables.rb index 50d137ec7c1..6f449fd060a 100644 --- a/lib/api/variables.rb +++ b/lib/api/variables.rb @@ -30,18 +30,18 @@ module API resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get project variables' do - success Entities::Variable + success Entities::Ci::Variable end params do use :pagination end get ':id/variables' do variables = user_project.variables - present paginate(variables), with: Entities::Variable + present paginate(variables), with: Entities::Ci::Variable end desc 'Get a specific variable from a project' do - success Entities::Variable + success Entities::Ci::Variable end params do requires :key, type: String, desc: 'The key of the variable' @@ -51,12 +51,12 @@ module API variable = find_variable(params) not_found!('Variable') unless variable - present variable, with: Entities::Variable + present variable, with: Entities::Ci::Variable end # rubocop: enable CodeReuse/ActiveRecord desc 'Create a new variable in a project' do - success Entities::Variable + success Entities::Ci::Variable end params do requires :key, type: String, desc: 'The key of the variable' @@ -73,14 +73,14 @@ module API variable = user_project.variables.create(variable_params) if variable.valid? - present variable, with: Entities::Variable + present variable, with: Entities::Ci::Variable else render_validation_error!(variable) end end desc 'Update an existing variable from a project' do - success Entities::Variable + success Entities::Ci::Variable end params do optional :key, type: String, desc: 'The key of the variable' @@ -100,7 +100,7 @@ module API variable_params = filter_variable_parameters(variable_params) if variable.update(variable_params) - present variable, with: Entities::Variable + present variable, with: Entities::Ci::Variable else render_validation_error!(variable) end @@ -108,7 +108,7 @@ module API # rubocop: enable CodeReuse/ActiveRecord desc 'Delete an existing variable from a project' do - success Entities::Variable + success Entities::Ci::Variable end params do requires :key, type: String, desc: 'The key of the variable' diff --git a/lib/gitlab/ci/parsers/coverage/cobertura.rb b/lib/gitlab/ci/parsers/coverage/cobertura.rb index 006d5097148..934c797580c 100644 --- a/lib/gitlab/ci/parsers/coverage/cobertura.rb +++ b/lib/gitlab/ci/parsers/coverage/cobertura.rb @@ -28,6 +28,8 @@ module Gitlab end def parse_node(key, value, coverage_report) + return if key == 'sources' + if key == 'class' Array.wrap(value).each do |item| parse_class(item, coverage_report) diff --git a/lib/gitlab/ci/templates/Jobs/Load-Performance-Testing.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Load-Performance-Testing.gitlab-ci.yml index b437ddbd734..4a9849c85c9 100644 --- a/lib/gitlab/ci/templates/Jobs/Load-Performance-Testing.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Load-Performance-Testing.gitlab-ci.yml @@ -5,7 +5,7 @@ load_performance: variables: DOCKER_TLS_CERTDIR: "" K6_IMAGE: loadimpact/k6 - K6_VERSION: 0.26.2 + K6_VERSION: 0.27.0 K6_TEST_FILE: github.com/loadimpact/k6/samples/http_get.js K6_OPTIONS: '' services: diff --git a/lib/gitlab/ci/templates/Verify/FailFast.gitlab-ci.yml b/lib/gitlab/ci/templates/Verify/FailFast.gitlab-ci.yml index 77a1b57d92f..584e6966180 100644 --- a/lib/gitlab/ci/templates/Verify/FailFast.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Verify/FailFast.gitlab-ci.yml @@ -1,4 +1,5 @@ rspec-rails-modified-path-specs: + image: ruby:2.6 stage: .pre rules: - if: $CI_MERGE_REQUEST_EVENT_TYPE == "merged_result" || $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train" diff --git a/lib/gitlab/ci/templates/Verify/Load-Performance-Testing.gitlab-ci.yml b/lib/gitlab/ci/templates/Verify/Load-Performance-Testing.gitlab-ci.yml index d39bd234020..f964b3b2caf 100644 --- a/lib/gitlab/ci/templates/Verify/Load-Performance-Testing.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Verify/Load-Performance-Testing.gitlab-ci.yml @@ -11,7 +11,7 @@ load_performance: image: docker:git variables: K6_IMAGE: loadimpact/k6 - K6_VERSION: 0.26.2 + K6_VERSION: 0.27.0 K6_TEST_FILE: github.com/loadimpact/k6/samples/http_get.js K6_OPTIONS: '' services: diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb index 346a2f9a461..e53ac00e77f 100644 --- a/lib/gitlab/metrics/subscribers/active_record.rb +++ b/lib/gitlab/metrics/subscribers/active_record.rb @@ -22,7 +22,7 @@ module Gitlab return if payload[:name] == 'SCHEMA' || IGNORABLE_SQL.include?(payload[:sql]) current_transaction.observe(:gitlab_sql_duration_seconds, event.duration / 1000.0) do - buckets [0.05, 0.1] + buckets [0.05, 0.1, 0.25] end increment_db_counters(payload) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e6dfda7dec6..38120b51b0f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -14993,6 +14993,27 @@ msgstr "" msgid "MergeConflict|origin//their changes" msgstr "" +msgid "MergeRequestAnalytics|Assignees" +msgstr "" + +msgid "MergeRequestAnalytics|Date Merged" +msgstr "" + +msgid "MergeRequestAnalytics|Line changes" +msgstr "" + +msgid "MergeRequestAnalytics|Merge Request" +msgstr "" + +msgid "MergeRequestAnalytics|Milestone" +msgstr "" + +msgid "MergeRequestAnalytics|Pipelines" +msgstr "" + +msgid "MergeRequestAnalytics|Time to merge" +msgstr "" + msgid "MergeRequestDiffs|Commenting on lines %{selectStart}start%{selectEnd} to %{end}" msgstr "" @@ -19432,9 +19453,18 @@ msgstr "" msgid "PrometheusAlerts|Threshold" msgstr "" +msgid "PrometheusAlerts|exceeded" +msgstr "" + msgid "PrometheusAlerts|https://gitlab.com/gitlab-com/runbooks" msgstr "" +msgid "PrometheusAlerts|is equal to" +msgstr "" + +msgid "PrometheusAlerts|is less than" +msgstr "" + msgid "PrometheusService|%{exporters} with %{metrics} were found" msgstr "" @@ -24491,6 +24521,9 @@ msgstr "" msgid "There is no data available. Please change your selection." msgstr "" +msgid "There is no table data available." +msgstr "" + msgid "There is too much data to calculate. Please change your selection." msgstr "" @@ -24659,6 +24692,9 @@ msgstr "" msgid "There was an error while fetching the chart data." msgstr "" +msgid "There was an error while fetching the table data." +msgstr "" + msgid "There was an error while fetching value stream analytics data." msgstr "" diff --git a/package.json b/package.json index cb19b90d0fa..5f70378f735 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "deckar01-task_list": "^2.3.1", "diff": "^3.4.0", "document-register-element": "1.14.3", + "dompurify": "^2.0.11", "dropzone": "^4.2.0", "editorconfig": "^0.15.3", "emoji-regex": "^7.0.3", @@ -123,7 +124,6 @@ "prosemirror-model": "^1.6.4", "raphael": "^2.2.7", "raw-loader": "^4.0.0", - "sanitize-html": "^1.22.0", "select2": "3.5.2-browserify", "smooshpack": "^0.0.62", "sortablejs": "^1.10.2", diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index 6fe5c9e0ff9..af6e88f73b1 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -43,6 +43,21 @@ FactoryBot.define do state_id { MergeRequest.available_states[:merged] } end + trait :with_merged_metrics do + merged + + transient do + merged_by { author } + end + + after(:build) do |merge_request, evaluator| + metrics = merge_request.build_metrics + metrics.merged_at = 1.week.ago + metrics.merged_by = evaluator.merged_by + metrics.pipeline = create(:ci_empty_pipeline) + end + end + trait :merged_target do source_branch { "merged-target" } target_branch { "improve/awesome" } diff --git a/spec/fixtures/api/schemas/environment.json b/spec/fixtures/api/schemas/environment.json index f42d701834a..d1274bea817 100644 --- a/spec/fixtures/api/schemas/environment.json +++ b/spec/fixtures/api/schemas/environment.json @@ -33,6 +33,7 @@ "updated_at": { "type": "string", "format": "date-time" }, "auto_stop_at": { "type": "string", "format": "date-time" }, "can_stop": { "type": "boolean" }, + "has_opened_alert": { "type": "boolean" }, "cluster_type": { "type": "types/nullable_string.json" }, "terminal_path": { "type": "types/nullable_string.json" }, "last_deployment": { diff --git a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js index ad398d6ccd6..4e35243f484 100644 --- a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js +++ b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js @@ -1,6 +1,6 @@ import Vuex from 'vuex'; import { createLocalVue, shallowMount, mount } from '@vue/test-utils'; -import { GlDeprecatedButton, GlFormCombobox } from '@gitlab/ui'; +import { GlButton, GlFormCombobox } from '@gitlab/ui'; import { AWS_ACCESS_KEY_ID } from '~/ci_variable_list/constants'; import CiVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue'; import createStore from '~/ci_variable_list/store'; @@ -29,14 +29,14 @@ describe('Ci variable modal', () => { }; const findModal = () => wrapper.find(ModalStub); - const addOrUpdateButton = index => + const findAddorUpdateButton = () => findModal() - .findAll(GlDeprecatedButton) - .at(index); + .findAll(GlButton) + .wrappers.find(button => button.props('variant') === 'success'); const deleteVariableButton = () => findModal() - .findAll(GlDeprecatedButton) - .at(1); + .findAll(GlButton) + .wrappers.find(button => button.props('variant') === 'danger'); afterEach(() => { wrapper.destroy(); @@ -69,7 +69,7 @@ describe('Ci variable modal', () => { }); it('button is disabled when no key/value pair are present', () => { - expect(addOrUpdateButton(1).attributes('disabled')).toBeTruthy(); + expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy(); }); }); @@ -82,11 +82,11 @@ describe('Ci variable modal', () => { }); it('button is enabled when key/value pair are present', () => { - expect(addOrUpdateButton(1).attributes('disabled')).toBeFalsy(); + expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy(); }); it('Add variable button dispatches addVariable action', () => { - addOrUpdateButton(1).vm.$emit('click'); + findAddorUpdateButton().vm.$emit('click'); expect(store.dispatch).toHaveBeenCalledWith('addVariable'); }); @@ -152,11 +152,11 @@ describe('Ci variable modal', () => { }); it('button text is Update variable when updating', () => { - expect(addOrUpdateButton(2).text()).toBe('Update variable'); + expect(findAddorUpdateButton().text()).toBe('Update variable'); }); it('Update variable button dispatches updateVariable with correct variable', () => { - addOrUpdateButton(2).vm.$emit('click'); + findAddorUpdateButton().vm.$emit('click'); expect(store.dispatch).toHaveBeenCalledWith('updateVariable'); }); @@ -189,7 +189,7 @@ describe('Ci variable modal', () => { }); it('disables the submit button', () => { - expect(addOrUpdateButton(1).attributes('disabled')).toBeTruthy(); + expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy(); }); it('shows the correct error text', () => { @@ -213,7 +213,7 @@ describe('Ci variable modal', () => { }); it('does not disable the submit button', () => { - expect(addOrUpdateButton(1).attributes('disabled')).toBeFalsy(); + expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy(); }); }); }); diff --git a/spec/frontend/incidents/components/incidents_list_spec.js b/spec/frontend/incidents/components/incidents_list_spec.js index ec42df0b0c3..aabafaa9154 100644 --- a/spec/frontend/incidents/components/incidents_list_spec.js +++ b/spec/frontend/incidents/components/incidents_list_spec.js @@ -7,11 +7,13 @@ import { GlPagination, GlSearchBoxByType, GlTab, + GlTabs, + GlBadge, } from '@gitlab/ui'; import { visitUrl, joinPaths } from '~/lib/utils/url_utility'; import IncidentsList from '~/incidents/components/incidents_list.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; -import { I18N, INCIDENT_STATE_TABS } from '~/incidents/constants'; +import { I18N, INCIDENT_STATUS_TABS } from '~/incidents/constants'; import mockIncidents from '../mocks/incidents.json'; jest.mock('~/lib/utils/url_utility', () => ({ @@ -24,6 +26,11 @@ describe('Incidents List', () => { let wrapper; const newIssuePath = 'namespace/project/-/issues/new'; const incidentTemplateName = 'incident'; + const incidentsCount = { + opened: 14, + closed: 1, + all: 16, + }; const findTable = () => wrapper.find(GlTable); const findTableRows = () => wrapper.findAll('table tbody tr'); @@ -38,8 +45,10 @@ describe('Incidents List', () => { const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']"); const findPagination = () => wrapper.find(GlPagination); const findStatusFilterTabs = () => wrapper.findAll(GlTab); + const findStatusFilterBadge = () => wrapper.findAll(GlBadge); + const findStatusTabs = () => wrapper.find(GlTabs); - function mountComponent({ data = { incidents: [] }, loading = false }) { + function mountComponent({ data = { incidents: [], incidentsCount: {} }, loading = false }) { wrapper = mount(IncidentsList, { data() { return data; @@ -83,7 +92,7 @@ describe('Incidents List', () => { it('shows empty state', () => { mountComponent({ - data: { incidents: { list: [] } }, + data: { incidents: { list: [] }, incidentsCount: {} }, loading: false, }); expect(findTable().text()).toContain(I18N.noIncidents); @@ -91,7 +100,7 @@ describe('Incidents List', () => { it('shows error state', () => { mountComponent({ - data: { incidents: { list: [] }, errored: true }, + data: { incidents: { list: [] }, incidentsCount: { all: 0 }, errored: true }, loading: false, }); expect(findTable().text()).toContain(I18N.noIncidents); @@ -101,7 +110,7 @@ describe('Incidents List', () => { describe('Incident Management list', () => { beforeEach(() => { mountComponent({ - data: { incidents: { list: mockIncidents } }, + data: { incidents: { list: mockIncidents }, incidentsCount }, loading: false, }); }); @@ -153,7 +162,7 @@ describe('Incidents List', () => { describe('Create Incident', () => { beforeEach(() => { mountComponent({ - data: { incidents: { list: [] } }, + data: { incidents: { list: [] }, incidentsCount: {} }, loading: false, }); }); @@ -178,6 +187,7 @@ describe('Incidents List', () => { list: mockIncidents, pageInfo: { hasNextPage: true, hasPreviousPage: true }, }, + incidentsCount, errored: false, }, loading: false, @@ -240,6 +250,7 @@ describe('Incidents List', () => { list: [...mockIncidents, ...mockIncidents, ...mockIncidents], pageInfo: { hasNextPage: true, hasPreviousPage: true }, }, + incidentsCount, errored: false, }, loading: false, @@ -252,6 +263,7 @@ describe('Incidents List', () => { }); it('returns `null` when currentPage is already last page', () => { + findStatusTabs().vm.$emit('input', 1); findPagination().vm.$emit('input', 1); return wrapper.vm.$nextTick(() => { expect(wrapper.vm.nextPage).toBeNull(); @@ -267,6 +279,7 @@ describe('Incidents List', () => { list: mockIncidents, pageInfo: { hasNextPage: true, hasPreviousPage: true }, }, + incidentsCount, errored: false, }, loading: false, @@ -286,10 +299,10 @@ describe('Incidents List', () => { }); }); - describe('State Filter Tabs', () => { + describe('Status Filter Tabs', () => { beforeEach(() => { mountComponent({ - data: { incidents: mockIncidents }, + data: { incidents: mockIncidents, incidentsCount }, loading: false, stubs: { GlTab: true, @@ -301,7 +314,18 @@ describe('Incidents List', () => { const tabs = findStatusFilterTabs().wrappers; tabs.forEach((tab, i) => { - expect(tab.attributes('data-testid')).toContain(INCIDENT_STATE_TABS[i].state); + expect(tab.attributes('data-testid')).toContain(INCIDENT_STATUS_TABS[i].status); + }); + }); + + it('should display filter tabs with alerts count badge for each status', () => { + const tabs = findStatusFilterTabs().wrappers; + const badges = findStatusFilterBadge(); + + tabs.forEach((tab, i) => { + const status = INCIDENT_STATUS_TABS[i].status.toLowerCase(); + expect(tab.attributes('data-testid')).toContain(INCIDENT_STATUS_TABS[i].status); + expect(badges.at(i).text()).toContain(incidentsCount[status]); }); }); }); @@ -310,7 +334,7 @@ describe('Incidents List', () => { describe('sorting the incident list by column', () => { beforeEach(() => { mountComponent({ - data: { incidents: mockIncidents }, + data: { incidents: mockIncidents, incidentsCount }, loading: false, }); }); diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js index 87e6d2724f6..d9866a94ffe 100644 --- a/spec/frontend/lib/utils/datetime_utility_spec.js +++ b/spec/frontend/lib/utils/datetime_utility_spec.js @@ -639,3 +639,17 @@ describe('dateFromParams', () => { expect(date.getDate()).toBe(expectedDate.getDate()); }); }); + +describe('differenceInSeconds', () => { + const startDateTime = new Date('2019-07-17T00:00:00.000Z'); + + it.each` + startDate | endDate | expected + ${startDateTime} | ${new Date('2019-07-17T00:00:00.000Z')} | ${0} + ${startDateTime} | ${new Date('2019-07-17T12:00:00.000Z')} | ${43200} + ${startDateTime} | ${new Date('2019-07-18T00:00:00.000Z')} | ${86400} + ${new Date('2019-07-18T00:00:00.000Z')} | ${startDateTime} | ${-86400} + `('returns $expected for $endDate - $startDate', ({ startDate, endDate, expected }) => { + expect(datetimeUtility.differenceInSeconds(startDate, endDate)).toBe(expected); + }); +}); diff --git a/spec/frontend/notebook/cells/output/html_sanitize_fixtures.js b/spec/frontend/notebook/cells/output/html_sanitize_fixtures.js new file mode 100644 index 00000000000..a886715ce4b --- /dev/null +++ b/spec/frontend/notebook/cells/output/html_sanitize_fixtures.js @@ -0,0 +1,114 @@ +export default [ + [ + 'protocol-based JS injection: simple, no spaces', + { + input: `<a href="javascript:alert('XSS');">foo</a>`, + output: '<a>foo</a>', + }, + ], + [ + 'protocol-based JS injection: simple, spaces before', + { + input: `<a href="javascript :alert('XSS');">foo</a>`, + output: '<a>foo</a>', + }, + ], + [ + 'protocol-based JS injection: simple, spaces after', + { + input: `<a href="javascript: alert('XSS');">foo</a>`, + output: '<a>foo</a>', + }, + ], + [ + 'protocol-based JS injection: simple, spaces before and after', + { + input: `<a href="javascript : alert('XSS');">foo</a>`, + output: '<a>foo</a>', + }, + ], + [ + 'protocol-based JS injection: preceding colon', + { + input: `<a href=":javascript:alert('XSS');">foo</a>`, + output: '<a>foo</a>', + }, + ], + [ + 'protocol-based JS injection: UTF-8 encoding', + { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>', + }, + ], + [ + 'protocol-based JS injection: long UTF-8 encoding', + { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>', + }, + ], + [ + 'protocol-based JS injection: long UTF-8 encoding without semicolons', + { + input: + '<a href=javascript:alert('XSS')>foo</a>', + output: '<a>foo</a>', + }, + ], + [ + 'protocol-based JS injection: hex encoding', + { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>', + }, + ], + [ + 'protocol-based JS injection: long hex encoding', + { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>', + }, + ], + [ + 'protocol-based JS injection: hex encoding without semicolons', + { + input: + '<a href=javascript:alert('XSS')>foo</a>', + output: '<a>foo</a>', + }, + ], + [ + 'protocol-based JS injection: null char', + { + input: '<a href=java\u0000script:alert("XSS")>foo</a>', + output: '<a>foo</a>', + }, + ], + [ + 'protocol-based JS injection: invalid URL char', + { input: '<img src=javascript:alert("XSS")>', output: '<img>' }, + ], + [ + 'protocol-based JS injection: Unicode', + { + input: `<a href="\u0001java\u0003script:alert('XSS')">foo</a>`, + output: '<a>foo</a>', + }, + ], + [ + 'protocol-based JS injection: spaces and entities', + { + input: `<a href="  javascript:alert('XSS');">foo</a>`, + output: '<a>foo</a>', + }, + ], + [ + 'img on error', + { + input: '<img src="x" onerror="alert(document.domain)" />', + output: '<img src="x">', + }, + ], + ['style tags are removed', { input: '<style>.foo {}</style> Foo', output: 'Foo' }], +]; diff --git a/spec/frontend/notebook/cells/output/html_sanitize_tests.js b/spec/frontend/notebook/cells/output/html_sanitize_tests.js deleted file mode 100644 index 74c48f04367..00000000000 --- a/spec/frontend/notebook/cells/output/html_sanitize_tests.js +++ /dev/null @@ -1,68 +0,0 @@ -export default { - 'protocol-based JS injection: simple, no spaces': { - input: '<a href="javascript:alert(\'XSS\');">foo</a>', - output: '<a>foo</a>', - }, - 'protocol-based JS injection: simple, spaces before': { - input: '<a href="javascript :alert(\'XSS\');">foo</a>', - output: '<a>foo</a>', - }, - 'protocol-based JS injection: simple, spaces after': { - input: '<a href="javascript: alert(\'XSS\');">foo</a>', - output: '<a>foo</a>', - }, - 'protocol-based JS injection: simple, spaces before and after': { - input: '<a href="javascript : alert(\'XSS\');">foo</a>', - output: '<a>foo</a>', - }, - 'protocol-based JS injection: preceding colon': { - input: '<a href=":javascript:alert(\'XSS\');">foo</a>', - output: '<a>foo</a>', - }, - 'protocol-based JS injection: UTF-8 encoding': { - input: '<a href="javascript:">foo</a>', - output: '<a>foo</a>', - }, - 'protocol-based JS injection: long UTF-8 encoding': { - input: '<a href="javascript:">foo</a>', - output: '<a>foo</a>', - }, - 'protocol-based JS injection: long UTF-8 encoding without semicolons': { - input: - '<a href=javascript:alert('XSS')>foo</a>', - output: '<a>foo</a>', - }, - 'protocol-based JS injection: hex encoding': { - input: '<a href="javascript:">foo</a>', - output: '<a>foo</a>', - }, - 'protocol-based JS injection: long hex encoding': { - input: '<a href="javascript:">foo</a>', - output: '<a>foo</a>', - }, - 'protocol-based JS injection: hex encoding without semicolons': { - input: - '<a href=javascript:alert('XSS')>foo</a>', - output: '<a>foo</a>', - }, - 'protocol-based JS injection: null char': { - input: '<a href=java\0script:alert("XSS")>foo</a>', - output: '<a>foo</a>', - }, - 'protocol-based JS injection: invalid URL char': { - input: '<img src=javascript:alert("XSS")>', - output: '<img>', - }, - 'protocol-based JS injection: Unicode': { - input: '<a href="\u0001java\u0003script:alert(\'XSS\')">foo</a>', - output: '<a>foo</a>', - }, - 'protocol-based JS injection: spaces and entities': { - input: '<a href="  javascript:alert(\'XSS\');">foo</a>', - output: '<a>foo</a>', - }, - 'img on error': { - input: '<img src="x" onerror="alert(document.domain)" />', - output: '<img src="x">', - }, -}; diff --git a/spec/frontend/notebook/cells/output/html_spec.js b/spec/frontend/notebook/cells/output/html_spec.js index 3ee404fb187..48d62d74a50 100644 --- a/spec/frontend/notebook/cells/output/html_spec.js +++ b/spec/frontend/notebook/cells/output/html_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import htmlOutput from '~/notebook/cells/output/html.vue'; -import sanitizeTests from './html_sanitize_tests'; +import sanitizeTests from './html_sanitize_fixtures'; describe('html output cell', () => { function createComponent(rawCode) { @@ -15,17 +15,12 @@ describe('html output cell', () => { }).$mount(); } - describe('sanitizes output', () => { - Object.keys(sanitizeTests).forEach(key => { - it(key, () => { - const test = sanitizeTests[key]; - const vm = createComponent(test.input); - const outputEl = [...vm.$el.querySelectorAll('div')].pop(); + it.each(sanitizeTests)('sanitizes output for: %p', (name, { input, output }) => { + const vm = createComponent(input); + const outputEl = [...vm.$el.querySelectorAll('div')].pop(); - expect(outputEl.innerHTML).toEqual(test.output); + expect(outputEl.innerHTML).toEqual(output); - vm.$destroy(); - }); - }); + vm.$destroy(); }); }); diff --git a/spec/frontend/notebook/cells/output/index_spec.js b/spec/frontend/notebook/cells/output/index_spec.js index 2b1aa5317c5..b9a2dfb8f34 100644 --- a/spec/frontend/notebook/cells/output/index_spec.js +++ b/spec/frontend/notebook/cells/output/index_spec.js @@ -34,7 +34,7 @@ describe('Output component', () => { expect(vm.$el.querySelector('pre')).not.toBeNull(); }); - it('renders promot', () => { + it('renders prompt', () => { expect(vm.$el.querySelector('.prompt span')).not.toBeNull(); }); }); diff --git a/spec/frontend/packages/details/store/getters_spec.js b/spec/frontend/packages/details/store/getters_spec.js index 95eb5abd0cf..307976d4124 100644 --- a/spec/frontend/packages/details/store/getters_spec.js +++ b/spec/frontend/packages/details/store/getters_spec.js @@ -13,6 +13,8 @@ import { nugetSetupCommand, pypiPipCommand, pypiSetupCommand, + composerRegistryInclude, + composerPackageInclude, } from '~/packages/details/store/getters'; import { conanPackage, @@ -68,6 +70,10 @@ describe('Getters PackageDetails Store', () => { const nugetSetupCommandStr = `nuget source Add -Name "GitLab" -Source "${registryUrl}" -UserName <your_username> -Password <your_token>`; const pypiPipCommandStr = `pip install ${pypiPackage.name} --index-url ${registryUrl}`; + const composerRegistryIncludeStr = '{"type":"composer","url":"foo"}'; + const composerPackageIncludeStr = JSON.stringify({ + [packageWithoutBuildInfo.name]: packageWithoutBuildInfo.version, + }); describe('packagePipeline', () => { it('should return the pipeline info when pipeline exists', () => { @@ -214,4 +220,18 @@ describe('Getters PackageDetails Store', () => { expect(pypiSetupCommand(state)).toBe(pypiSetupCommandStr); }); }); + + describe('composer string getters', () => { + it('gets the correct composerRegistryInclude command', () => { + setupState({ composerPath: 'foo' }); + + expect(composerRegistryInclude(state)).toBe(composerRegistryIncludeStr); + }); + + it('gets the correct composerPackageInclude command', () => { + setupState(); + + expect(composerPackageInclude(state)).toBe(composerPackageIncludeStr); + }); + }); }); diff --git a/spec/frontend/packages/shared/components/package_list_row_spec.js b/spec/frontend/packages/shared/components/package_list_row_spec.js index 61a5bb16edb..c0ae972d519 100644 --- a/spec/frontend/packages/shared/components/package_list_row_spec.js +++ b/spec/frontend/packages/shared/components/package_list_row_spec.js @@ -67,10 +67,6 @@ describe('packages_list_row', () => { it('has project field', () => { expect(findProjectLink().exists()).toBe(true); }); - - it('does not show the delete button', () => { - expect(findDeleteButton().exists()).toBe(false); - }); }); describe('showPackageType', () => { @@ -96,9 +92,7 @@ describe('packages_list_row', () => { }); describe('delete event', () => { - beforeEach(() => - mountComponent({ isGroup: false, packageEntity: packageWithoutTags, shallow: false }), - ); + beforeEach(() => mountComponent({ packageEntity: packageWithoutTags, shallow: false })); it('emits the packageToDelete event when the delete button is clicked', () => { findDeleteButton().trigger('click'); diff --git a/spec/frontend/project_find_file_spec.js b/spec/frontend/project_find_file_spec.js index b4c6d202e14..757a02a04a3 100644 --- a/spec/frontend/project_find_file_spec.js +++ b/spec/frontend/project_find_file_spec.js @@ -1,11 +1,13 @@ import MockAdapter from 'axios-mock-adapter'; import $ from 'jquery'; import { TEST_HOST } from 'helpers/test_constants'; -import sanitize from 'sanitize-html'; +import { sanitize } from 'dompurify'; import ProjectFindFile from '~/project_find_file'; import axios from '~/lib/utils/axios_utils'; -jest.mock('sanitize-html', () => jest.fn(val => val)); +jest.mock('dompurify', () => ({ + sanitize: jest.fn(val => val), +})); const BLOB_URL_TEMPLATE = `${TEST_HOST}/namespace/project/blob/master`; const FILE_FIND_URL = `${TEST_HOST}/namespace/project/files/master?format=json`; diff --git a/spec/graphql/types/alert_management/alert_type_spec.rb b/spec/graphql/types/alert_management/alert_type_spec.rb index 71efcf8768e..e14c189d4b6 100644 --- a/spec/graphql/types/alert_management/alert_type_spec.rb +++ b/spec/graphql/types/alert_management/alert_type_spec.rb @@ -30,6 +30,8 @@ RSpec.describe GitlabSchema.types['AlertManagementAlert'] do metrics_dashboard_url runbook todos + details_url + prometheus_alert ] expect(described_class).to have_graphql_fields(*expected_fields) diff --git a/spec/graphql/types/issuable_connection_type_spec.rb b/spec/graphql/types/countable_connection_type_spec.rb index af34611ecfe..af34611ecfe 100644 --- a/spec/graphql/types/issuable_connection_type_spec.rb +++ b/spec/graphql/types/countable_connection_type_spec.rb diff --git a/spec/graphql/types/environment_type_spec.rb b/spec/graphql/types/environment_type_spec.rb index f7522cb3e2c..abeeeba543f 100644 --- a/spec/graphql/types/environment_type_spec.rb +++ b/spec/graphql/types/environment_type_spec.rb @@ -7,11 +7,76 @@ RSpec.describe GitlabSchema.types['Environment'] do it 'has the expected fields' do expected_fields = %w[ - name id state metrics_dashboard + name id state metrics_dashboard latest_opened_most_severe_alert ] expect(described_class).to have_graphql_fields(*expected_fields) end specify { expect(described_class).to require_graphql_authorizations(:read_environment) } + + context 'when there is an environment' do + let_it_be(:project) { create(:project) } + let_it_be(:environment) { create(:environment, project: project) } + let_it_be(:user) { create(:user) } + + subject { GitlabSchema.execute(query, context: { current_user: user }).as_json } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + name + state + } + } + } + ) + end + + before do + project.add_developer(user) + end + + it 'returns an environment' do + expect(subject['data']['project']['environment']['name']).to eq(environment.name) + end + + context 'when query alert data for the environment' do + let_it_be(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + environment(name: "#{environment.name}") { + name + state + latestOpenedMostSevereAlert { + severity + title + detailsUrl + prometheusAlert { + humanizedText + } + } + } + } + } + ) + end + + it 'does not return alert information' do + expect(subject['data']['project']['environment']['latestOpenedMostSevereAlert']).to be_nil + end + + context 'when alert is raised on the environment' do + let!(:prometheus_alert) { create(:prometheus_alert, project: project, environment: environment) } + let!(:alert) { create(:alert_management_alert, :triggered, :prometheus, project: project, environment: environment, prometheus_alert: prometheus_alert) } + + it 'returns alert information' do + expect(subject['data']['project']['environment']['latestOpenedMostSevereAlert']['severity']).to eq(alert.severity.upcase) + end + end + end + end end diff --git a/spec/graphql/types/merge_request_type_spec.rb b/spec/graphql/types/merge_request_type_spec.rb index b3dccde8ce3..b11951190e0 100644 --- a/spec/graphql/types/merge_request_type_spec.rb +++ b/spec/graphql/types/merge_request_type_spec.rb @@ -24,9 +24,11 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do source_branch_exists target_branch_exists upvotes downvotes head_pipeline pipelines task_completion_status milestone assignees participants subscribed labels discussion_locked time_estimate - total_time_spent reference author merged_at + total_time_spent reference author merged_at commit_count ] + expected_fields << 'approved_by' if Gitlab.ee? + expect(described_class).to have_graphql_fields(*expected_fields) end end diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index 5be1fafffb6..a0b6858fc99 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -24,7 +24,7 @@ RSpec.describe GitlabSchema.types['Project'] do namespace group statistics repository merge_requests merge_request issues issue milestones pipelines removeSourceBranchAfterMerge sentryDetailedError snippets grafanaIntegration autocloseReferencedIssues suggestion_commit_message environments - boards jira_import_status jira_imports services releases release + environment boards jira_import_status jira_imports services releases release alert_management_alerts alert_management_alert alert_management_alert_status_counts container_expiration_policy sast_ci_configuration service_desk_enabled service_desk_address issue_status_counts @@ -98,6 +98,13 @@ RSpec.describe GitlabSchema.types['Project'] do it { is_expected.to have_graphql_resolver(Resolvers::EnvironmentsResolver) } end + describe 'environment field' do + subject { described_class.fields['environment'] } + + it { is_expected.to have_graphql_type(Types::EnvironmentType) } + it { is_expected.to have_graphql_resolver(Resolvers::EnvironmentsResolver.single) } + end + describe 'members field' do subject { described_class.fields['projectMembers'] } diff --git a/spec/graphql/types/prometheus_alert_type_spec.rb b/spec/graphql/types/prometheus_alert_type_spec.rb new file mode 100644 index 00000000000..716537ea716 --- /dev/null +++ b/spec/graphql/types/prometheus_alert_type_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['PrometheusAlert'] do + specify { expect(described_class.graphql_name).to eq('PrometheusAlert') } + + it 'has the expected fields' do + expected_fields = %w[ + id humanized_text + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end + + specify { expect(described_class).to require_graphql_authorizations(:read_prometheus_alerts) } +end diff --git a/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb b/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb index 08a3fbd7867..45e87466532 100644 --- a/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb +++ b/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb @@ -19,6 +19,41 @@ RSpec.describe Gitlab::Ci::Parsers::Coverage::Cobertura do end end + context 'when there is a <sources>' do + shared_examples_for 'ignoring sources' do + it 'parses XML without errors' do + expect { subject }.not_to raise_error + + expect(coverage_report.files).to eq({}) + end + end + + context 'and has a single source' do + let(:cobertura) do + <<-EOF.strip_heredoc + <sources> + <source>project/src</source> + </sources> + EOF + end + + it_behaves_like 'ignoring sources' + end + + context 'and has multiple sources' do + let(:cobertura) do + <<-EOF.strip_heredoc + <sources> + <source>project/src/foo</source> + <source>project/src/bar</source> + </sources> + EOF + end + + it_behaves_like 'ignoring sources' + end + end + context 'when there is a single <class>' do context 'with no lines' do let(:cobertura) do diff --git a/spec/models/alert_management/alert_spec.rb b/spec/models/alert_management/alert_spec.rb index 7c22af54c71..f937a879400 100644 --- a/spec/models/alert_management/alert_spec.rb +++ b/spec/models/alert_management/alert_spec.rb @@ -230,6 +230,17 @@ RSpec.describe AlertManagement::Alert do it { is_expected.to match_array(env_alert) } end + describe '.order_severity_with_open_prometheus_alert' do + subject { described_class.where(project: alert_project).order_severity_with_open_prometheus_alert } + + let_it_be(:alert_project) { create(:project) } + let_it_be(:resolved_critical_alert) { create(:alert_management_alert, :resolved, :critical, project: alert_project) } + let_it_be(:triggered_critical_alert) { create(:alert_management_alert, :triggered, :critical, project: alert_project) } + let_it_be(:triggered_high_alert) { create(:alert_management_alert, :triggered, :high, project: alert_project) } + + it { is_expected.to eq([triggered_critical_alert, triggered_high_alert]) } + end + describe '.counts_by_status' do subject { described_class.counts_by_status } diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index 9445ddfcd9d..91a669aa3f4 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -483,11 +483,7 @@ RSpec.describe Ci::JobArtifact do subject { create(:ci_job_artifact, :archive) } context 'when existing object has local store' do - it 'is stored locally' do - expect(subject.file_store).to be(ObjectStorage::Store::LOCAL) - expect(subject.file).to be_file_storage - expect(subject.file.object_store).to eq(ObjectStorage::Store::LOCAL) - end + it_behaves_like 'mounted file in local store' end context 'when direct upload is enabled' do @@ -496,11 +492,7 @@ RSpec.describe Ci::JobArtifact do end context 'when file is stored' do - it 'is stored remotely' do - expect(subject.file_store).to eq(ObjectStorage::Store::REMOTE) - expect(subject.file).not_to be_file_storage - expect(subject.file.object_store).to eq(ObjectStorage::Store::REMOTE) - end + it_behaves_like 'mounted file in object store' end end end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index c449a3c3c47..2696d144db4 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -19,6 +19,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do it { is_expected.to have_many(:deployments) } it { is_expected.to have_many(:metrics_dashboard_annotations) } it { is_expected.to have_many(:alert_management_alerts) } + it { is_expected.to have_one(:latest_opened_most_severe_alert) } it { is_expected.to delegate_method(:stop_action).to(:last_deployment) } it { is_expected.to delegate_method(:manual_actions).to(:last_deployment) } @@ -1347,4 +1348,27 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do expect(project.environments.count_by_state).to eq({ stopped: 0, available: 0 }) end end + + describe '#has_opened_alert?' do + subject { environment.has_opened_alert? } + + let_it_be(:project) { create(:project) } + let_it_be(:environment, reload: true) { create(:environment, project: project) } + + context 'when environment has an triggered alert' do + let!(:alert) { create(:alert_management_alert, :triggered, project: project, environment: environment) } + + it { is_expected.to be(true) } + end + + context 'when environment has an resolved alert' do + let!(:alert) { create(:alert_management_alert, :resolved, project: project, environment: environment) } + + it { is_expected.to be(false) } + end + + context 'when environment does not have an alert' do + it { is_expected.to be(false) } + end + end end diff --git a/spec/models/lfs_object_spec.rb b/spec/models/lfs_object_spec.rb index 36d45f17392..a0f633218b0 100644 --- a/spec/models/lfs_object_spec.rb +++ b/spec/models/lfs_object_spec.rb @@ -152,14 +152,10 @@ RSpec.describe LfsObject do end describe 'file is being stored' do - let(:lfs_object) { create(:lfs_object, :with_file) } + subject { create(:lfs_object, :with_file) } context 'when existing object has local store' do - it 'is stored locally' do - expect(lfs_object.file_store).to be(ObjectStorage::Store::LOCAL) - expect(lfs_object.file).to be_file_storage - expect(lfs_object.file.object_store).to eq(ObjectStorage::Store::LOCAL) - end + it_behaves_like 'mounted file in local store' end context 'when direct upload is enabled' do @@ -167,13 +163,7 @@ RSpec.describe LfsObject do stub_lfs_object_storage(direct_upload: true) end - context 'when file is stored' do - it 'is stored remotely' do - expect(lfs_object.file_store).to eq(ObjectStorage::Store::REMOTE) - expect(lfs_object.file).not_to be_file_storage - expect(lfs_object.file.object_store).to eq(ObjectStorage::Store::REMOTE) - end - end + it_behaves_like 'mounted file in object store' end end end diff --git a/spec/models/terraform/state_spec.rb b/spec/models/terraform/state_spec.rb index 00e67ad70db..68bb86bfa49 100644 --- a/spec/models/terraform/state_spec.rb +++ b/spec/models/terraform/state_spec.rb @@ -45,9 +45,7 @@ RSpec.describe Terraform::State do describe '#update_file_store' do context 'when file is stored in object storage' do - it 'sets file_store to remote' do - expect(subject.file_store).to eq(ObjectStorage::Store::REMOTE) - end + it_behaves_like 'mounted file in object store' end context 'when file is stored locally' do @@ -55,9 +53,7 @@ RSpec.describe Terraform::State do stub_terraform_state_object_storage(Terraform::StateUploader, enabled: false) end - it 'sets file_store to local' do - expect(subject.file_store).to eq(ObjectStorage::Store::LOCAL) - end + it_behaves_like 'mounted file in local store' end end end diff --git a/spec/presenters/alert_management/alert_presenter_spec.rb b/spec/presenters/alert_management/alert_presenter_spec.rb index 4281babee61..394007a802f 100644 --- a/spec/presenters/alert_management/alert_presenter_spec.rb +++ b/spec/presenters/alert_management/alert_presenter_spec.rb @@ -58,4 +58,10 @@ RSpec.describe AlertManagement::AlertPresenter do expect(presenter.runbook).to eq('https://runbook.com') end end + + describe '#details_url' do + it 'returns the details URL' do + expect(presenter.details_url).to match(%r{#{project.web_url}/-/alert_management/#{alert.iid}/details}) + end + end end diff --git a/spec/presenters/prometheus_alert_presenter_spec.rb b/spec/presenters/prometheus_alert_presenter_spec.rb new file mode 100644 index 00000000000..b9f18e2be28 --- /dev/null +++ b/spec/presenters/prometheus_alert_presenter_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe PrometheusAlertPresenter do + let_it_be(:project) { create(:project) } + let_it_be(:environment) { create(:environment, project: project) } + + let(:presenter) { described_class.new(prometheus_alert) } + + describe '#humanized_text' do + subject { presenter.humanized_text } + + let_it_be(:prometheus_metric) { create(:prometheus_metric, project: project) } + let(:prometheus_alert) { create(:prometheus_alert, operator: operator, project: project, environment: environment, prometheus_metric: prometheus_metric) } + let(:operator) { :gt } + + it { is_expected.to eq('exceeded 1.0m/s') } + + context 'when operator is eq' do + let(:operator) { :eq } + + it { is_expected.to eq('is equal to 1.0m/s') } + end + + context 'when operator is lt' do + let(:operator) { :lt } + + it { is_expected.to eq('is less than 1.0m/s') } + end + end +end diff --git a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb index da1fab42b1f..d3a2e6a1deb 100644 --- a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb +++ b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb @@ -7,9 +7,9 @@ RSpec.describe 'getting Alert Management Alerts' do let_it_be(:payload) { { 'custom' => { 'alert' => 'payload' }, 'runbook' => 'runbook' } } let_it_be(:project) { create(:project, :repository) } let_it_be(:current_user) { create(:user) } - let_it_be(:resolved_alert) { create(:alert_management_alert, :all_fields, :resolved, project: project, issue: nil, severity: :low) } - let_it_be(:triggered_alert) { create(:alert_management_alert, :all_fields, project: project, severity: :critical, payload: payload) } - let_it_be(:other_project_alert) { create(:alert_management_alert, :all_fields) } + let_it_be(:resolved_alert) { create(:alert_management_alert, :all_fields, :resolved, project: project, issue: nil, severity: :low).present } + let_it_be(:triggered_alert) { create(:alert_management_alert, :all_fields, project: project, severity: :critical, payload: payload).present } + let_it_be(:other_project_alert) { create(:alert_management_alert, :all_fields).present } let(:params) { {} } @@ -75,6 +75,8 @@ RSpec.describe 'getting Alert Management Alerts' do 'createdAt' => triggered_alert.created_at.strftime('%Y-%m-%dT%H:%M:%SZ'), 'updatedAt' => triggered_alert.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ'), 'metricsDashboardUrl' => nil, + 'detailsUrl' => triggered_alert.details_url, + 'prometheusAlert' => nil, 'runbook' => 'runbook' ) diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb index e2255fdb048..bb63a5994b0 100644 --- a/spec/requests/api/graphql/project/merge_requests_spec.rb +++ b/spec/requests/api/graphql/project/merge_requests_spec.rb @@ -171,4 +171,43 @@ RSpec.describe 'getting merge request listings nested in a project' do it_behaves_like 'searching with parameters' end + + describe 'fields' do + let(:requested_fields) { nil } + let(:extra_iid_for_second_query) { merge_request_c.iid.to_s } + let(:search_params) { { iids: [merge_request_a.iid.to_s, merge_request_b.iid.to_s] } } + + def execute_query + query = query_merge_requests(requested_fields) + post_graphql(query, current_user: current_user) + end + + context 'when requesting `commit_count`' do + let(:requested_fields) { [:commit_count] } + + it 'exposes `commit_count`' do + merge_request_a.metrics.update!(commits_count: 5) + + execute_query + + expect(results).to include(a_hash_including('commitCount' => 5)) + end + + include_examples 'N+1 query check' + end + + context 'when requesting `merged_at`' do + let(:requested_fields) { [:merged_at] } + + before do + # make the MRs "merged" + [merge_request_a, merge_request_b, merge_request_c].each do |mr| + mr.update_column(:state_id, MergeRequest.available_states[:merged]) + mr.metrics.update_column(:merged_at, Time.now) + end + end + + include_examples 'N+1 query check' + end + end end diff --git a/spec/serializers/environment_entity_spec.rb b/spec/serializers/environment_entity_spec.rb index c90f771335e..c969638614e 100644 --- a/spec/serializers/environment_entity_spec.rb +++ b/spec/serializers/environment_entity_spec.rb @@ -82,6 +82,26 @@ RSpec.describe EnvironmentEntity do end end + context 'with alert' do + let!(:environment) { create(:environment, project: project) } + let!(:prometheus_alert) { create(:prometheus_alert, project: project, environment: environment) } + let!(:alert) { create(:alert_management_alert, :triggered, :prometheus, project: project, environment: environment, prometheus_alert: prometheus_alert) } + + it 'exposes active alert flag' do + project.add_maintainer(user) + + expect(subject[:has_opened_alert]).to eq(true) + end + + context 'when user does not have permission to read alert' do + it 'does not expose active alert flag' do + project.add_reporter(user) + + expect(subject[:has_opened_alert]).to be_nil + end + end + end + context 'pod_logs' do context 'with reporter access' do before do diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index b187025eb11..13da76263b1 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -3,22 +3,23 @@ require 'spec_helper' RSpec.describe TodoService do - let(:author) { create(:user) } - let(:assignee) { create(:user) } - let(:non_member) { create(:user) } - let(:member) { create(:user) } - let(:guest) { create(:user) } - let(:admin) { create(:admin) } - let(:john_doe) { create(:user) } - let(:skipped) { create(:user) } + let_it_be(:project) { create(:project, :repository) } + let_it_be(:author) { create(:user) } + let_it_be(:assignee) { create(:user) } + let_it_be(:non_member) { create(:user) } + let_it_be(:member) { create(:user) } + let_it_be(:guest) { create(:user) } + let_it_be(:admin) { create(:admin) } + let_it_be(:john_doe) { create(:user) } + let_it_be(:skipped) { create(:user) } + let(:skip_users) { [skipped] } - let(:project) { create(:project, :repository) } let(:mentions) { 'FYI: ' + [author, assignee, john_doe, member, guest, non_member, admin, skipped].map(&:to_reference).join(' ') } let(:directly_addressed) { [author, assignee, john_doe, member, guest, non_member, admin, skipped].map(&:to_reference).join(' ') } let(:directly_addressed_and_mentioned) { member.to_reference + ", what do you think? cc: " + [guest, admin, skipped].map(&:to_reference).join(' ') } let(:service) { described_class.new } - before do + before_all do project.add_guest(guest) project.add_developer(author) project.add_developer(assignee) @@ -456,7 +457,16 @@ RSpec.describe TodoService do end context 'leaving a note on a commit in a public project with private code' do - let(:project) { create(:project, :repository, :public, :repository_private) } + let_it_be(:project) { create(:project, :repository, :public, :repository_private) } + + before_all do + project.add_guest(guest) + project.add_developer(author) + project.add_developer(assignee) + project.add_developer(member) + project.add_developer(john_doe) + project.add_developer(skipped) + end it 'creates a todo for each valid mentioned user' do expected_todo = base_commit_todo_attrs.merge( @@ -492,7 +502,16 @@ RSpec.describe TodoService do end context 'leaving a note on a commit in a private project' do - let(:project) { create(:project, :repository, :private) } + let_it_be(:project) { create(:project, :repository, :private) } + + before_all do + project.add_guest(guest) + project.add_developer(author) + project.add_developer(assignee) + project.add_developer(member) + project.add_developer(john_doe) + project.add_developer(skipped) + end it 'creates a todo for each valid mentioned user' do expected_todo = base_commit_todo_attrs.merge( @@ -822,7 +841,17 @@ RSpec.describe TodoService do end describe '#new_note' do - let(:project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository) } + + before_all do + project.add_guest(guest) + project.add_developer(author) + project.add_developer(assignee) + project.add_developer(member) + project.add_developer(john_doe) + project.add_developer(skipped) + end + let(:mention) { john_doe.to_reference } let(:diff_note_on_merge_request) { create(:diff_note_on_merge_request, project: project, noteable: mr_unassigned, author: author, note: "Hey #{mention}") } let(:addressed_diff_note_on_merge_request) { create(:diff_note_on_merge_request, project: project, noteable: mr_unassigned, author: author, note: "#{mention}, hey!") } diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb index d101b092e7d..f4343b8b783 100644 --- a/spec/support/helpers/cycle_analytics_helpers.rb +++ b/spec/support/helpers/cycle_analytics_helpers.rb @@ -37,7 +37,7 @@ module CycleAnalyticsHelpers end def create_cycle(user, project, issue, mr, milestone, pipeline) - issue.update(milestone: milestone) + issue.update!(milestone: milestone) pipeline.run ci_build = create(:ci_build, pipeline: pipeline, status: :success, author: user) diff --git a/spec/support/helpers/design_management_test_helpers.rb b/spec/support/helpers/design_management_test_helpers.rb index 1daa92e8ad4..db217250b17 100644 --- a/spec/support/helpers/design_management_test_helpers.rb +++ b/spec/support/helpers/design_management_test_helpers.rb @@ -35,9 +35,9 @@ module DesignManagementTestHelpers def act_on_designs(designs, &block) issue = designs.first.issue - version = build(:design_version, :empty, issue: issue).tap { |v| v.save(validate: false) } + version = build(:design_version, :empty, issue: issue).tap { |v| v.save!(validate: false) } designs.each do |d| - yield.create(design: d, version: version) + yield.create!(design: d, version: version) end version end diff --git a/spec/support/helpers/jira_service_helper.rb b/spec/support/helpers/jira_service_helper.rb index 9072c41fe66..4895bc3ba15 100644 --- a/spec/support/helpers/jira_service_helper.rb +++ b/spec/support/helpers/jira_service_helper.rb @@ -10,7 +10,7 @@ module JiraServiceHelper password = 'my-secret-password' jira_issue_transition_id = '1' - jira_tracker.update( + jira_tracker.update!( url: url, username: username, password: password, jira_issue_transition_id: jira_issue_transition_id, active: true ) diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb index 92f6d673255..1118cfcf7ac 100644 --- a/spec/support/helpers/login_helpers.rb +++ b/spec/support/helpers/login_helpers.rb @@ -40,7 +40,7 @@ module LoginHelpers if user_or_role.is_a?(User) user_or_role else - create(user_or_role) + create(user_or_role) # rubocop:disable Rails/SaveBang end gitlab_sign_in_with(user, **kwargs) diff --git a/spec/support/helpers/notification_helpers.rb b/spec/support/helpers/notification_helpers.rb index 887d68de4e1..aee57b452fe 100644 --- a/spec/support/helpers/notification_helpers.rb +++ b/spec/support/helpers/notification_helpers.rb @@ -12,7 +12,7 @@ module NotificationHelpers def create_global_setting_for(user, level) setting = user.global_notification_setting setting.level = level - setting.save + setting.save! user end @@ -27,7 +27,7 @@ module NotificationHelpers def create_notification_setting(user, resource, level) setting = user.notification_settings_for(resource) setting.level = level - setting.save + setting.save! end # Create custom notifications diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb index bc31ee955c8..8a52a614821 100644 --- a/spec/support/helpers/stub_object_storage.rb +++ b/spec/support/helpers/stub_object_storage.rb @@ -37,7 +37,7 @@ module StubObjectStorage Fog.mock! ::Fog::Storage.new(connection_params).tap do |connection| - connection.directories.create(key: remote_directory) + connection.directories.create(key: remote_directory) # rubocop:disable Rails/SaveBang # Cleanup remaining files connection.directories.each do |directory| diff --git a/spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb b/spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb new file mode 100644 index 00000000000..397e22ace28 --- /dev/null +++ b/spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true +shared_examples 'N+1 query check' do + it 'prevents N+1 queries' do + execute_query # "warm up" to prevent undeterministic counts + + control_count = ActiveRecord::QueryRecorder.new { execute_query }.count + + search_params[:iids] << extra_iid_for_second_query + expect { execute_query }.not_to exceed_query_limit(control_count) + end +end diff --git a/spec/support/shared_examples/models/concerns/file_store_mounter_shared_examples.rb b/spec/support/shared_examples/models/concerns/file_store_mounter_shared_examples.rb new file mode 100644 index 00000000000..4cb087c47ad --- /dev/null +++ b/spec/support/shared_examples/models/concerns/file_store_mounter_shared_examples.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'mounted file in local store' do + it 'is stored locally' do + expect(subject.file_store).to be(ObjectStorage::Store::LOCAL) + expect(subject.file).to be_file_storage + expect(subject.file.object_store).to eq(ObjectStorage::Store::LOCAL) + end +end + +RSpec.shared_examples 'mounted file in object store' do + it 'is stored remotely' do + expect(subject.file_store).to eq(ObjectStorage::Store::REMOTE) + expect(subject.file).not_to be_file_storage + expect(subject.file.object_store).to eq(ObjectStorage::Store::REMOTE) + end +end diff --git a/yarn.lock b/yarn.lock index 732a84299c5..c05f247332a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4150,6 +4150,11 @@ domhandler@^3.0.0: dependencies: domelementtype "^2.0.1" +dompurify@^2.0.11: + version "2.0.11" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.0.11.tgz#cd47935774230c5e478b183a572e726300b3891d" + integrity sha512-qVoGPjIW9IqxRij7klDQQ2j6nSe4UNWANBhZNLnsS7ScTtLb+3YdxkRY8brNTpkUiTtcXsCJO+jS0UCDfenLuA== + domutils@^1.5.1: version "1.6.2" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.6.2.tgz#1958cc0b4c9426e9ed367fb1c8e854891b0fa3ff" |