diff options
84 files changed, 1512 insertions, 444 deletions
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 092afa15df4..84cc529467b 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -1.17.0 +1.18.0 diff --git a/app/assets/javascripts/alert_management/components/alert_details.vue b/app/assets/javascripts/alert_management/components/alert_details.vue index ef060713d3d..cee1988e3ec 100644 --- a/app/assets/javascripts/alert_management/components/alert_details.vue +++ b/app/assets/javascripts/alert_management/components/alert_details.vue @@ -1,5 +1,12 @@ <script> -import { GlNewDropdown, GlNewDropdownItem, GlTabs, GlTab, GlButton } from '@gitlab/ui'; +import { + GlLoadingIcon, + GlNewDropdown, + GlNewDropdownItem, + GlTabs, + GlTab, + GlButton, +} from '@gitlab/ui'; import { s__ } from '~/locale'; import query from '../graphql/queries/details.query.graphql'; import { fetchPolicies } from '~/lib/graphql'; @@ -16,6 +23,7 @@ export default { overviewTitle: s__('AlertManagement|Overview'), }, components: { + GlLoadingIcon, GlNewDropdown, GlNewDropdownItem, GlTab, @@ -55,10 +63,16 @@ export default { data() { return { alert: null }; }, + computed: { + loading() { + return this.$apollo.queries.alert.loading; + }, + }, }; </script> <template> <div> + <div v-if="loading"><gl-loading-icon size="lg" class="mt-3" /></div> <div v-if="alert" class="gl-display-flex justify-content-end gl-border-b-1 gl-border-b-gray-200 gl-border-b-solid gl-p-4" @@ -73,7 +87,7 @@ export default { {{ s__('AlertManagement|Create issue') }} </gl-button> </div> - <div class="gl-display-flex justify-content-end"> + <div v-if="alert" class="gl-display-flex justify-content-end"> <gl-new-dropdown right> <gl-new-dropdown-item v-for="(label, field) in $options.statuses" diff --git a/app/assets/javascripts/alert_management/components/alert_management_list.vue b/app/assets/javascripts/alert_management/components/alert_management_list.vue index 7fe74eb1da8..a2efa6f0e0c 100644 --- a/app/assets/javascripts/alert_management/components/alert_management_list.vue +++ b/app/assets/javascripts/alert_management/components/alert_management_list.vue @@ -15,7 +15,7 @@ import { import { s__ } from '~/locale'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import getAlerts from '../graphql/queries/getAlerts.query.graphql'; -import { ALERTS_STATUS, ALERTS_STATUS_TABS } from '../constants'; +import { ALERTS_STATUS, ALERTS_STATUS_TABS, ALERTS_SEVERITY_LABELS } from '../constants'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; const tdClass = 'table-col d-flex d-md-table-cell align-items-center'; @@ -68,6 +68,7 @@ export default { [ALERTS_STATUS.ACKNOWLEDGED]: s__('AlertManagement|Acknowledged'), [ALERTS_STATUS.RESOLVED]: s__('AlertManagement|Resolved'), }, + severityLabels: ALERTS_SEVERITY_LABELS, statusTabs: ALERTS_STATUS_TABS, components: { GlEmptyState, @@ -185,14 +186,17 @@ export default { stacked="md" > <template #cell(severity)="{ item }"> - <div class="d-inline-flex align-items-center justify-content-between"> + <div + class="d-inline-flex align-items-center justify-content-between" + data-testid="severityField" + > <gl-icon class="mr-2" :size="12" :name="`severity-${item.severity.toLowerCase()}`" :class="`icon-${item.severity.toLowerCase()}`" /> - {{ item.severity }} + {{ $options.severityLabels[item.severity] }} </div> </template> diff --git a/app/assets/javascripts/alert_management/constants.js b/app/assets/javascripts/alert_management/constants.js index c95a3c29f04..ddaf8242b68 100644 --- a/app/assets/javascripts/alert_management/constants.js +++ b/app/assets/javascripts/alert_management/constants.js @@ -1,5 +1,14 @@ import { s__ } from '~/locale'; +export const ALERTS_SEVERITY_LABELS = { + CRITICAL: s__('AlertManagement|Critical'), + HIGH: s__('AlertManagement|High'), + MEDIUM: s__('AlertManagement|Medium'), + LOW: s__('AlertManagement|Low'), + INFO: s__('AlertManagement|Info'), + UNKNOWN: s__('AlertManagement|Unknown'), +}; + export const ALERTS_STATUS = { OPEN: 'open', TRIGGERED: 'triggered', diff --git a/app/assets/javascripts/diffs/components/edit_button.vue b/app/assets/javascripts/diffs/components/edit_button.vue index 91e296f8572..21fdb19287d 100644 --- a/app/assets/javascripts/diffs/components/edit_button.vue +++ b/app/assets/javascripts/diffs/components/edit_button.vue @@ -1,5 +1,6 @@ <script> import { GlTooltipDirective, GlDeprecatedButton } from '@gitlab/ui'; +import { __ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; export default { @@ -13,7 +14,8 @@ export default { props: { editPath: { type: String, - required: true, + required: false, + default: '', }, canCurrentUserFork: { type: Boolean, @@ -25,6 +27,18 @@ export default { default: false, }, }, + computed: { + tooltipTitle() { + if (this.isDisabled) { + return __("Can't edit as source branch was deleted"); + } + + return __('Edit file'); + }, + isDisabled() { + return !this.editPath; + }, + }, methods: { handleEditClick(evt) { if (this.canCurrentUserFork && !this.canModifyBlob) { @@ -37,13 +51,15 @@ export default { </script> <template> - <gl-deprecated-button - v-gl-tooltip.top - :href="editPath" - :title="__('Edit file')" - class="js-edit-blob" - @click.native="handleEditClick" - > - <icon name="pencil" /> - </gl-deprecated-button> + <span v-gl-tooltip.top :title="tooltipTitle"> + <gl-deprecated-button + :href="editPath" + :disabled="isDisabled" + :class="{ 'cursor-not-allowed': isDisabled }" + class="rounded-0 js-edit-blob" + @click.native="handleEditClick" + > + <icon name="pencil" /> + </gl-deprecated-button> + </span> </template> diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js index 0134378868b..903105babeb 100644 --- a/app/assets/javascripts/monitoring/stores/actions.js +++ b/app/assets/javascripts/monitoring/stores/actions.js @@ -13,11 +13,7 @@ import trackDashboardLoad from '../monitoring_tracking_helper'; import getEnvironments from '../queries/getEnvironments.query.graphql'; import getAnnotations from '../queries/getAnnotations.query.graphql'; import statusCodes from '../../lib/utils/http_status'; -import { - backOff, - convertObjectPropsToCamelCase, - isFeatureFlagEnabled, -} from '../../lib/utils/common_utils'; +import { backOff, convertObjectPropsToCamelCase } from '../../lib/utils/common_utils'; import { s__, sprintf } from '../../locale'; import { @@ -116,14 +112,7 @@ export const clearExpandedPanel = ({ commit }) => { export const fetchData = ({ dispatch }) => { dispatch('fetchEnvironmentsData'); dispatch('fetchDashboard'); - /** - * Annotations data is not yet fetched. This will be - * ready after the BE piece is implemented. - * https://gitlab.com/gitlab-org/gitlab/-/issues/211330 - */ - if (isFeatureFlagEnabled('metricsDashboardAnnotations')) { - dispatch('fetchAnnotations'); - } + dispatch('fetchAnnotations'); }; // Metrics dashboard diff --git a/app/assets/stylesheets/components/related_items_list.scss b/app/assets/stylesheets/components/related_items_list.scss index e4466b44358..c9a8d5e5975 100644 --- a/app/assets/stylesheets/components/related_items_list.scss +++ b/app/assets/stylesheets/components/related_items_list.scss @@ -85,6 +85,10 @@ $item-weight-max-width: 48px; white-space: nowrap; } + .health-label-short { + display: none; + } + @include media-breakpoint-down(lg) { .issue-count-badge { padding: 0; @@ -96,7 +100,6 @@ $item-weight-max-width: 48px; .item-body, .card-header { .health-label-short { - display: initial; max-width: 0; } @@ -131,6 +134,12 @@ $item-weight-max-width: 48px; } } +.card-header { + .health-label-short { + display: initial; + } +} + .item-meta { flex-basis: 100%; font-size: $gl-font-size; @@ -265,7 +274,6 @@ $item-weight-max-width: 48px; max-width: 90%; } - .item-body, .card-header { .health-label-short { max-width: 30px; @@ -306,7 +314,6 @@ $item-weight-max-width: 48px; } } - .item-body, .card-header { .health-label-short { max-width: 60px; @@ -326,7 +333,6 @@ $item-weight-max-width: 48px; } } - .item-body, .card-header { .health-label-short { max-width: 100px; @@ -364,10 +370,6 @@ $item-weight-max-width: 48px; } } - .health-label-short { - display: initial; - } - .health-label-long { display: none; } @@ -411,8 +413,7 @@ $item-weight-max-width: 48px; } @media only screen and (min-width: 1500px) { - .card-header, - .item-body { + .card-header { .health-label-short { display: none; } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index ecf2097dc87..f47d0cab31f 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -507,6 +507,10 @@ opacity: 1 !important; cursor: default !important; + &.cursor-not-allowed { + cursor: not-allowed !important; + } + i { color: $gl-text-color-disabled !important; } diff --git a/app/assets/stylesheets/page_bundles/themes/_dark.scss b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss index 1d316ca2e3f..86dffb4d7df 100644 --- a/app/assets/stylesheets/page_bundles/themes/_dark.scss +++ b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss @@ -1,40 +1,6 @@ .ide.theme-dark { - $border-color: #1d1f21; - $highlight-accent: #fff; - $text-color: #ccc; - $background: #333; - $background-hover: #2d2d2d; - $highlight-background: #252526; - $link-color: #428fdc; - $footer-background: #060606; - - $input-border: #868686; - $input-background: transparent; - $input-color: $highlight-accent; - - $btn-default-background: transparent; - $btn-default-border: #bfbfbf; - $btn-default-hover-border: #d8d8d8; - - $btn-primary-background: #1068bf; - $btn-primary-border: #428fdc; - $btn-primary-hover-border: #63a6e9; - - $btn-success-background: #217645; - $btn-success-border: #108548; - $btn-success-hover-border: #2da160; - - $btn-disabled-border: rgba(223, 223, 223, 0.24); - $btn-disabled-color: rgba(145, 145, 145, 0.48); - - $dropdown-background: #404040; - $dropdown-hover-background: #525252; - - $diff-insert: rgba(155, 185, 85, 0.2); - $diff-remove: rgba(255, 0, 0, 0.2); - a:not(.btn) { - color: $link-color; + color: var(--ide-link-color); } h1, @@ -72,12 +38,12 @@ .ide-navigator-btn, .ide-pipeline .top-bar, .ide-pipeline .top-bar .controllers .controllers-buttons { - color: $text-color; + color: var(--ide-text-color); } .drag-handle:hover, .card-header .badge.badge-pill { - background-color: $dropdown-hover-background; + background-color: var(--ide-dropdown-hover-background); } .dropdown-menu-toggle svg, @@ -86,38 +52,34 @@ .file-row .file-row-icon svg, .file-row:hover .file-row-icon svg, .controllers-buttons svg { - fill: $text-color; + fill: var(--ide-text-color); } .ide-pipeline svg { - --svg-status-bg: $background; + --svg-status-bg: var(--ide-background); } .multi-file-tab-close:hover { - background-color: $input-border; + background-color: var(--ide-input-border); } .ide-review-sub-header:hover { - color: $input-border; + color: var(--ide-input-border); } .text-secondary { - color: $text-color !important; + color: var(--ide-text-color) !important; } input[type='search']::placeholder, input[type='text']::placeholder, textarea::placeholder, .dropdown-input .fa { - color: $input-border; + color: var(--ide-input-border); } .ide-nav-form .input-icon { - fill: $input-border; - } - - .ide-staged-action-btn { - background-color: transparent; + fill: var(--ide-input-border); } code, @@ -139,32 +101,28 @@ .bs-callout, .ide-pipeline .top-bar, .ide-terminal .top-bar { - background-color: $background; - } - - pre code { - background-color: inherit; + background-color: var(--ide-background); } .bs-callout { - border-color: $dropdown-background; + border-color: var(--ide-dropdown-background); code { - background-color: $dropdown-background; + background-color: var(--ide-dropdown-background); } } .nav-links:not(.quick-links) li:not(.md-header-toolbar) a:hover { - border-color: $dropdown-hover-background; + border-color: var(--ide-dropdown-hover-background); } .ide-sidebar-link:hover, .multi-file-tabs li { - background-color: $background-hover; + background-color: var(--ide-background-hover); } .common-note-form .md-area { - border-color: $input-border; + border-color: var(--ide-input-border); } &, @@ -180,7 +138,7 @@ .card, .multi-file-commit-panel-success-message, .ide-preview-header { - background-color: $highlight-background; + background-color: var(--ide-highlight-background); } .multi-file-commit-panel { @@ -204,7 +162,7 @@ .ide-job-item:not(:last-child), .ide-terminal .top-bar, .ide-pipeline .top-bar { - border-color: $border-color; + border-color: var(--ide-border-color); } .md h1, @@ -222,58 +180,58 @@ .multi-file-commit-form .nav-links:not(.quick-links), .ide-pipeline-list .nav-links:not(.quick-links), .ide-preview-header { - border-color: $background; + border-color: var(--ide-background); } .multi-file-tabs li.active { - border-bottom-color: $highlight-background; + border-bottom-color: var(--ide-highlight-background); } .multi-file-tabs, .ide-commit-editor-header { - box-shadow: inset 0 -1px $border-color; + box-shadow: inset 0 -1px var(--ide-border-color); } .ide-sidebar-link.active { - color: $highlight-accent; - box-shadow: inset 3px 0 $highlight-accent; + color: var(--ide-highlight-accent); + box-shadow: inset 3px 0 var(--ide-highlight-accent); &.is-right { - box-shadow: inset -3px 0 $highlight-accent; + box-shadow: inset -3px 0 var(--ide-highlight-accent); } } .nav-links li.active a, .nav-links li a.active { - border-color: $highlight-accent; - color: $text-color; + border-color: var(--ide-highlight-accent); + color: var(--ide-text-color); } .avatar-container { &, .avatar { - color: $text-color; - background-color: $highlight-background; - border-color: $highlight-background; + color: var(--ide-text-color); + background-color: var(--ide-highlight-background); + border-color: var(--ide-highlight-background); } } .ide-status-bar { - background-color: $footer-background; + background-color: var(--ide-footer-background); } input[type='text'], input[type='search'], .filtered-search-box { - border-color: $input-border; - background: $input-background !important; + border-color: var(--ide-input-border); + background: var(--ide-input-background) !important; } input[type='text'], input[type='search'], .filtered-search-box, textarea { - color: $input-color !important; + color: var(--ide-input-color) !important; } .filtered-search-box input[type='search'] { @@ -282,46 +240,49 @@ .filtered-search-token .value-container, .filtered-search-term .value-container { - background-color: $dropdown-hover-background; - - color: $text-color; + background-color: var(--ide-dropdown-hover-background); + color: var(--ide-text-color); &:hover { - background-color: $input-border; + background-color: var(--ide-input-border); } } .ide-entry-dropdown-toggle:hover { - background: $gray-800; + background: var(--ide-file-row-btn-hover-background); + } + + @function calc-btn-hover-padding($original-padding, $original-border: 1px) { + @return calc(#{$original-padding + $original-border} - var(--ide-btn-hover-border-width)); } .btn:not(.btn-link):not([disabled]):hover { - border-width: 2px; - padding: 5px 9px; + border-width: var(--ide-btn-hover-border-width); + padding: calc-btn-hover-padding(6px) calc-btn-hover-padding(10px); } .btn:not([disabled]).btn-sm:hover { - padding: 3px 9px; + padding: calc-btn-hover-padding(4px) calc-btn-hover-padding(10px); } .btn:not([disabled]).btn-block:hover { - padding: 5px 0; + padding: calc-btn-hover-padding(6px) 0; } .btn-inverted, .btn-default, .dropdown, .dropdown-menu-toggle { - background-color: $input-background !important; - color: $input-color !important; - border-color: $btn-default-border; + background-color: var(--ide-input-background) !important; + color: var(--ide-input-color) !important; + border-color: var(--ide-btn-default-border); } .btn-inverted, .btn-default { &:hover, &:focus { - border-color: $btn-default-hover-border !important; + border-color: var(--ide-btn-default-hover-border) !important; } } @@ -329,35 +290,35 @@ .dropdown-menu-toggle { &:hover, &:focus { - background-color: $gray-900 !important; - border-color: $gray-200 !important; + background-color: var(--ide-dropdown-btn-hover-background) !important; + border-color: var(--ide-dropdown-btn-hover-border) !important; } } .dropdown-menu { - color: $text-color; - border-color: $background; - background-color: $dropdown-background; + color: var(--ide-text-color); + border-color: var(--ide-background); + background-color: var(--ide-dropdown-background); .divider, .nav-links:not(.quick-links) { - background-color: $dropdown-hover-background; - border-color: $dropdown-hover-background; + background-color: var(--ide-dropdown-hover-background); + border-color: var(--ide-dropdown-hover-background); } .nav-links li a.active { - border-color: $highlight-accent; + border-color: var(--ide-highlight-accent); } .ide-nav-form .nav-links li a:not(.active) { - background-color: $dropdown-background; + background-color: var(--ide-dropdown-background); } .nav-links:not(.quick-links) li:not(.md-header-toolbar) a { - color: $text-color; + color: var(--ide-text-color); &.active { - color: $text-color; + color: var(--ide-text-color); } } @@ -366,73 +327,75 @@ li button:not(.disable-hover):hover, li button:not(.disable-hover):focus, li button.is-focused { - background-color: $dropdown-hover-background; - color: $text-color; + background-color: var(--ide-dropdown-hover-background); + color: var(--ide-text-color); } } .dropdown-title, .dropdown-input { - border-color: $dropdown-hover-background !important; + border-color: var(--ide-dropdown-hover-background) !important; } - .btn-primary { - background-color: $btn-primary-background; - border-color: $btn-primary-border !important; + .btn-primary, + .btn-info { + background-color: var(--ide-btn-primary-background); + border-color: var(--ide-btn-primary-border) !important; &:hover, &:focus { - border-color: $btn-primary-hover-border !important; + border-color: var(--ide-btn-primary-hover-border) !important; } } .btn-success { - background-color: $btn-success-background; - border-color: $btn-success-border !important; + background-color: var(--ide-btn-success-background); + border-color: var(--ide-btn-success-border) !important; &:hover, &:focus { - border-color: $btn-success-hover-border !important; + border-color: var(--ide-btn-success-hover-border) !important; } } .btn[disabled] { - background: $btn-default-background !important; - border: 1px solid $btn-disabled-border !important; - color: $btn-disabled-color !important; + background: var(--ide-btn-default-background) !important; + border: 1px solid var(--ide-btn-disabled-border) !important; + color: var(--ide-btn-disabled-color) !important; } .md-previewer, + pre code, .md table:not(.code) tbody, .ide-empty-state { - background-color: $border-color; + background-color: var(--ide-border-color); } .ide-tree-header svg:focus, .ide-tree-header svg:hover { - color: $blue-600; + color: var(--ide-link-color); } .animation-container { [class^='skeleton-line-'] { - background-color: $gray-800; + background-color: var(--ide-animation-gradient-1); &::after { background-image: linear-gradient(to right, - $gray-800 0%, - $gray-700 20%, - $gray-800 40%, - $gray-800 100%); + var(--ide-animation-gradient-1) 0%, + var(--ide-animation-gradient-2) 20%, + var(--ide-animation-gradient-1) 40%, + var(--ide-animation-gradient-1) 100%); } } } .idiff.addition { - background-color: $diff-insert; + background-color: var(--ide-diff-insert); } .idiff.deletion { - background-color: $diff-remove; + background-color: var(--ide-diff-remove); } } diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index d0660422f7e..cb41d960307 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -2,8 +2,9 @@ @import 'framework/mixins'; @import './ide_mixins'; @import './ide_monaco_overrides'; +@import './ide_theme_overrides'; -@import './themes/dark'; +@import './ide_themes/dark'; $search-list-icon-width: 18px; $ide-activity-bar-width: 60px; diff --git a/app/assets/stylesheets/page_bundles/ide_themes/_dark.scss b/app/assets/stylesheets/page_bundles/ide_themes/_dark.scss new file mode 100644 index 00000000000..809abc42a69 --- /dev/null +++ b/app/assets/stylesheets/page_bundles/ide_themes/_dark.scss @@ -0,0 +1,45 @@ +.ide.theme-dark { + --ide-border-color: #1d1f21; + --ide-highlight-accent: #fff; + --ide-text-color: #ccc; + --ide-background: #333; + --ide-background-hover: #2d2d2d; + --ide-highlight-background: #252526; + --ide-link-color: #428fdc; + --ide-footer-background: #060606; + + --ide-input-border: #868686; + --ide-input-background: transparent; + --ide-input-color: #fff; + + --ide-btn-default-background: transparent; + --ide-btn-default-border: #bfbfbf; + --ide-btn-default-hover-border: #d8d8d8; + + --ide-btn-primary-background: #1068bf; + --ide-btn-primary-border: #428fdc; + --ide-btn-primary-hover-border: #63a6e9; + + --ide-btn-success-background: #217645; + --ide-btn-success-border: #108548; + --ide-btn-success-hover-border: #2da160; + + --ide-btn-disabled-border: rgba(223, 223, 223, 0.24); + --ide-btn-disabled-color: rgba(145, 145, 145, 0.48); + + --ide-btn-hover-border-width: 2px; + + --ide-dropdown-background: #404040; + --ide-dropdown-hover-background: #525252; + + --ide-dropdown-btn-hover-border: #{$gray-200}; + --ide-dropdown-btn-hover-background: #{$gray-900}; + + --ide-file-row-btn-hover-background: #{$gray-800}; + + --ide-diff-insert: rgba(155, 185, 85, 0.2); + --ide-diff-remove: rgba(255, 0, 0, 0.2); + + --ide-animation-gradient-1: #{$gray-800}; + --ide-animation-gradient-2: #{$gray-700}; +} diff --git a/app/controllers/concerns/metrics_dashboard.rb b/app/controllers/concerns/metrics_dashboard.rb index fa79f3bc4e6..58715fda152 100644 --- a/app/controllers/concerns/metrics_dashboard.rb +++ b/app/controllers/concerns/metrics_dashboard.rb @@ -35,10 +35,10 @@ module MetricsDashboard private def all_dashboards - dashboards = dashboard_finder.find_all_paths(project_for_dashboard) - dashboards.map do |dashboard| - amend_dashboard(dashboard) - end + dashboard_finder + .find_all_paths(project_for_dashboard) + .map(&method(:amend_dashboard)) + .sort_by { |dashboard| [dashboard[:starred] ? 0 : 1, dashboard[:display_name].downcase] } end def amend_dashboard(dashboard) @@ -46,6 +46,8 @@ module MetricsDashboard dashboard[:can_edit] = project_dashboard ? can_edit?(dashboard) : false dashboard[:project_blob_path] = project_dashboard ? dashboard_project_blob_path(dashboard) : nil + dashboard[:starred] = starred_dashboards.include?(dashboard[:path]) + dashboard[:user_starred_path] = nil # placeholder attribute until API endpoint will be merged https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31316 dashboard end @@ -73,6 +75,20 @@ module MetricsDashboard ::Gitlab::Metrics::Dashboard::Finder end + def starred_dashboards + @starred_dashboards ||= begin + if project_for_dashboard.present? + ::Metrics::UsersStarredDashboardsFinder + .new(user: current_user, project: project_for_dashboard) + .execute + .map(&:dashboard_path) + .to_set + else + Set.new + end + end + end + # Project is not defined for group and admin level clusters. def project_for_dashboard defined?(project) ? project : nil diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 70724394ef5..5f4d88c57e9 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -9,7 +9,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController authorize_metrics_dashboard! push_frontend_feature_flag(:prometheus_computed_alerts) - push_frontend_feature_flag(:metrics_dashboard_annotations, project) end before_action :authorize_read_environment! before_action :authorize_create_environment!, only: [:new, :create] diff --git a/app/graphql/mutations/alert_management/update_alert_status.rb b/app/graphql/mutations/alert_management/update_alert_status.rb index ac6a5412956..e73a591378a 100644 --- a/app/graphql/mutations/alert_management/update_alert_status.rb +++ b/app/graphql/mutations/alert_management/update_alert_status.rb @@ -11,7 +11,6 @@ module Mutations def resolve(args) alert = authorized_find!(project_path: args[:project_path], iid: args[:iid]) - result = update_status(alert, args[:status]) prepare_response(result) @@ -20,7 +19,9 @@ module Mutations private def update_status(alert, status) - ::AlertManagement::UpdateAlertStatusService.new(alert, status).execute + ::AlertManagement::UpdateAlertStatusService + .new(alert, current_user, status) + .execute end def prepare_response(result) diff --git a/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb b/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb index 068323a3073..2dd224bb17b 100644 --- a/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb +++ b/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb @@ -18,7 +18,6 @@ module Resolvers def resolve(**args) return [] unless dashboard - return [] unless Feature.enabled?(:metrics_dashboard_annotations, dashboard.environment&.project) ::Metrics::Dashboards::AnnotationsFinder.new(dashboard: dashboard, params: args).execute end diff --git a/app/graphql/types/metrics/dashboard_type.rb b/app/graphql/types/metrics/dashboard_type.rb index e7d09866bb5..d684533ff94 100644 --- a/app/graphql/types/metrics/dashboard_type.rb +++ b/app/graphql/types/metrics/dashboard_type.rb @@ -11,8 +11,7 @@ module Types description: 'Path to a file with the dashboard definition' field :annotations, Types::Metrics::Dashboards::AnnotationType.connection_type, null: true, - description: 'Annotations added to the dashboard. Will always return `null` ' \ - 'if `metrics_dashboard_annotations` feature flag is disabled', + description: 'Annotations added to the dashboard', resolver: Resolvers::Metrics::Dashboards::AnnotationResolver end # rubocop: enable Graphql/AuthorizeTypes diff --git a/app/models/group.rb b/app/models/group.rb index bc57ab522da..37dfe27e834 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -335,6 +335,11 @@ class Group < Namespace .where(source_id: source_ids) end + def members_from_self_and_ancestors_with_effective_access_level + members_with_parents.select([:user_id, 'MAX(access_level) AS access_level']) + .group(:user_id) + end + def members_with_descendants GroupMember .active_without_invites_and_requests diff --git a/app/models/member.rb b/app/models/member.rb index 5b33333aa23..791073da095 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Member < ApplicationRecord + include EachBatch include AfterCommitQueue include Sortable include Importable diff --git a/app/serializers/diff_file_base_entity.rb b/app/serializers/diff_file_base_entity.rb index def98e23828..8c2b3a65d57 100644 --- a/app/serializers/diff_file_base_entity.rb +++ b/app/serializers/diff_file_base_entity.rb @@ -22,16 +22,16 @@ class DiffFileBaseEntity < Grape::Entity expose :edit_path, if: -> (_, options) { options[:merge_request] } do |diff_file| merge_request = options[:merge_request] - options = merge_request.persisted? ? { from_merge_request_iid: merge_request.iid } : {} + next unless merge_request.merged? || merge_request.source_branch_exists? - next unless merge_request.source_project + target_project, target_branch = edit_project_branch_options(merge_request) if Feature.enabled?(:web_ide_default) - ide_edit_path(merge_request.source_project, merge_request.source_branch, diff_file.new_path) + ide_edit_path(target_project, target_branch, diff_file.new_path) else - project_edit_blob_path(merge_request.source_project, - tree_join(merge_request.source_branch, diff_file.new_path), - options) + options = merge_request.persisted? && merge_request.source_branch_exists? && !merge_request.merged? ? { from_merge_request_iid: merge_request.iid } : {} + + project_edit_blob_path(target_project, tree_join(target_branch, diff_file.new_path), options) end end @@ -61,7 +61,7 @@ class DiffFileBaseEntity < Grape::Entity next unless diff_file.blob if merge_request&.source_project && current_user - can_modify_blob?(diff_file.blob, merge_request.source_project, merge_request.source_branch) + can_modify_blob?(diff_file.blob, merge_request.source_project, merge_request.source_branch_exists? ? merge_request.source_branch : merge_request.target_branch) else false end @@ -113,4 +113,12 @@ class DiffFileBaseEntity < Grape::Entity def current_user request.current_user end + + def edit_project_branch_options(merge_request) + if merge_request.source_branch_exists? && !merge_request.merged? + [merge_request.source_project, merge_request.source_branch] + else + [merge_request.target_project, merge_request.target_branch] + end + end end diff --git a/app/serializers/diffs_entity.rb b/app/serializers/diffs_entity.rb index 568d0f6aa8f..fb4fbe57130 100644 --- a/app/serializers/diffs_entity.rb +++ b/app/serializers/diffs_entity.rb @@ -11,6 +11,10 @@ class DiffsEntity < Grape::Entity merge_request&.source_branch end + expose :source_branch_exists do |diffs| + merge_request&.source_branch_exists? + end + expose :target_branch_name do |diffs| merge_request&.target_branch end diff --git a/app/serializers/issuable_sidebar_basic_entity.rb b/app/serializers/issuable_sidebar_basic_entity.rb index 498cfe5930d..bbec107544e 100644 --- a/app/serializers/issuable_sidebar_basic_entity.rb +++ b/app/serializers/issuable_sidebar_basic_entity.rb @@ -21,7 +21,7 @@ class IssuableSidebarBasicEntity < Grape::Entity expose :labels, using: LabelEntity expose :current_user, if: lambda { |_issuable| current_user } do - expose :current_user, merge: true, using: API::Entities::UserBasic + expose :current_user, merge: true, using: ::API::Entities::UserBasic expose :todo, using: IssuableSidebarTodoEntity do |issuable| current_user.pending_todo_for(issuable) diff --git a/app/serializers/issuable_sidebar_extras_entity.rb b/app/serializers/issuable_sidebar_extras_entity.rb index 0e1fcc58d7a..77f2e34fa5d 100644 --- a/app/serializers/issuable_sidebar_extras_entity.rb +++ b/app/serializers/issuable_sidebar_extras_entity.rb @@ -21,5 +21,5 @@ class IssuableSidebarExtrasEntity < Grape::Entity issuable.subscribed?(request.current_user, issuable.project) end - expose :assignees, using: API::Entities::UserBasic + expose :assignees, using: ::API::Entities::UserBasic end diff --git a/app/serializers/merge_request_assignee_entity.rb b/app/serializers/merge_request_assignee_entity.rb index 6849c62e759..b7ef7449270 100644 --- a/app/serializers/merge_request_assignee_entity.rb +++ b/app/serializers/merge_request_assignee_entity.rb @@ -5,3 +5,5 @@ class MergeRequestAssigneeEntity < ::API::Entities::UserBasic options[:merge_request]&.can_be_merged_by?(assignee) end end + +MergeRequestAssigneeEntity.prepend_if_ee('EE::MergeRequestAssigneeEntity') diff --git a/app/services/alert_management/update_alert_status_service.rb b/app/services/alert_management/update_alert_status_service.rb index 37960f510ef..a7ebddb82e0 100644 --- a/app/services/alert_management/update_alert_status_service.rb +++ b/app/services/alert_management/update_alert_status_service.rb @@ -5,14 +5,17 @@ module AlertManagement include Gitlab::Utils::StrongMemoize # @param alert [AlertManagement::Alert] + # @param user [User] # @param status [Integer] Must match a value from AlertManagement::Alert::STATUSES - def initialize(alert, status) + def initialize(alert, user, status) @alert = alert + @user = user @status = status end def execute - return error('Invalid status') unless status_key + return error_no_permissions unless allowed? + return error_invalid_status unless status_key if alert.update(status_event: status_event) success @@ -23,7 +26,13 @@ module AlertManagement private - attr_reader :alert, :status + attr_reader :alert, :user, :status + + delegate :project, to: :alert + + def allowed? + user.can?(:update_alert_management_alert, project) + end def status_key strong_memoize(:status_key) do @@ -39,6 +48,14 @@ module AlertManagement ServiceResponse.success(payload: { alert: alert }) end + def error_no_permissions + error(_('You have no permissions')) + end + + def error_invalid_status + error(_('Invalid status')) + end + def error(message) ServiceResponse.error(payload: { alert: alert }, message: message) end diff --git a/app/services/authorized_project_update/project_create_service.rb b/app/services/authorized_project_update/project_create_service.rb new file mode 100644 index 00000000000..c17c0a033fe --- /dev/null +++ b/app/services/authorized_project_update/project_create_service.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module AuthorizedProjectUpdate + class ProjectCreateService < BaseService + BATCH_SIZE = 1000 + + def initialize(project) + @project = project + end + + def execute + group = project.group + + unless group + return ServiceResponse.error(message: 'Project does not have a group') + end + + group.members_from_self_and_ancestors_with_effective_access_level + .each_batch(of: BATCH_SIZE, column: :user_id) do |members| + attributes = members.map do |member| + { user_id: member.user_id, project_id: project.id, access_level: member.access_level } + end + + ProjectAuthorization.insert_all(attributes) + end + + ServiceResponse.success + end + + private + + attr_reader :project + end +end diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 434f584ab33..33a20afc0ba 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -3,6 +3,13 @@ # # Do not edit it manually! --- +- :name: authorized_project_update:authorized_project_update_project_create + :feature_category: :authentication_and_authorization + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true - :name: auto_devops:auto_devops_disable :feature_category: :auto_devops :has_external_dependencies: diff --git a/app/workers/authorized_project_update/project_create_worker.rb b/app/workers/authorized_project_update/project_create_worker.rb new file mode 100644 index 00000000000..651849b57ec --- /dev/null +++ b/app/workers/authorized_project_update/project_create_worker.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module AuthorizedProjectUpdate + class ProjectCreateWorker + include ApplicationWorker + + feature_category :authentication_and_authorization + urgency :low + queue_namespace :authorized_project_update + + idempotent! + + def perform(project_id) + project = Project.find(project_id) + + AuthorizedProjectUpdate::ProjectCreateService.new(project).execute + end + end +end diff --git a/changelogs/unreleased/38080-scala-gitlab-ci-template-does-not-work.yml b/changelogs/unreleased/38080-scala-gitlab-ci-template-does-not-work.yml new file mode 100644 index 00000000000..843088f88a1 --- /dev/null +++ b/changelogs/unreleased/38080-scala-gitlab-ci-template-does-not-work.yml @@ -0,0 +1,5 @@ +--- +title: Fix GitLab CI/CD Scala template +merge_request: 30667 +author: +type: fixed diff --git a/changelogs/unreleased/move-build-template-to-rules-syntax.yml b/changelogs/unreleased/move-build-template-to-rules-syntax.yml new file mode 100644 index 00000000000..a6681e70e23 --- /dev/null +++ b/changelogs/unreleased/move-build-template-to-rules-syntax.yml @@ -0,0 +1,5 @@ +--- +title: Move Build.gitlab-ci.yml to `rules` syntax +merge_request: 30895 +author: +type: changed diff --git a/changelogs/unreleased/move-code-quality-template-to-rules-syntax.yml b/changelogs/unreleased/move-code-quality-template-to-rules-syntax.yml new file mode 100644 index 00000000000..dbc7e3c7e8f --- /dev/null +++ b/changelogs/unreleased/move-code-quality-template-to-rules-syntax.yml @@ -0,0 +1,5 @@ +--- +title: Move Code-Quality.gitlab-ci.yml to `rules` syntax +merge_request: 30896 +author: +type: changed diff --git a/changelogs/unreleased/move-deploy-template-to-rules-syntax.yml b/changelogs/unreleased/move-deploy-template-to-rules-syntax.yml new file mode 100644 index 00000000000..46ec2bcbcf5 --- /dev/null +++ b/changelogs/unreleased/move-deploy-template-to-rules-syntax.yml @@ -0,0 +1,5 @@ +--- +title: Move Deploy.gitlab-ci.yml to `rules` syntax +merge_request: 31290 +author: +type: changed diff --git a/changelogs/unreleased/mwam-214582-add-starred-dashboard-to-metrics-dashboard-endpoint.yml b/changelogs/unreleased/mwam-214582-add-starred-dashboard-to-metrics-dashboard-endpoint.yml new file mode 100644 index 00000000000..db9635aaac7 --- /dev/null +++ b/changelogs/unreleased/mwam-214582-add-starred-dashboard-to-metrics-dashboard-endpoint.yml @@ -0,0 +1,5 @@ +--- +title: Display metrics dashboards starred by user at the top of dashboard select field. +merge_request: 31059 +author: +type: added diff --git a/changelogs/unreleased/mwaw-215224-make-dashboard-annotations-generally-available.yml b/changelogs/unreleased/mwaw-215224-make-dashboard-annotations-generally-available.yml new file mode 100644 index 00000000000..84f8dfb0798 --- /dev/null +++ b/changelogs/unreleased/mwaw-215224-make-dashboard-annotations-generally-available.yml @@ -0,0 +1,6 @@ +--- +title: Add metrics dashboard annotations feature, which enables marking interesting + events over metrics dashboard charts +merge_request: 30371 +author: +type: added diff --git a/changelogs/unreleased/pages-1-18.yml b/changelogs/unreleased/pages-1-18.yml new file mode 100644 index 00000000000..84b45bc8ad4 --- /dev/null +++ b/changelogs/unreleased/pages-1-18.yml @@ -0,0 +1,5 @@ +--- +title: Update GitLab Pages to 1.18.0 +merge_request: +author: +type: added diff --git a/changelogs/unreleased/remove-deprecated-container-scanning-report-format.yml b/changelogs/unreleased/remove-deprecated-container-scanning-report-format.yml new file mode 100644 index 00000000000..a4e754fee1d --- /dev/null +++ b/changelogs/unreleased/remove-deprecated-container-scanning-report-format.yml @@ -0,0 +1,5 @@ +--- +title: Remove deprecated container scanning report parser +merge_request: 31294 +author: +type: removed diff --git a/changelogs/unreleased/revert-0ddee28a.yml b/changelogs/unreleased/revert-0ddee28a.yml new file mode 100644 index 00000000000..909a8673a08 --- /dev/null +++ b/changelogs/unreleased/revert-0ddee28a.yml @@ -0,0 +1,5 @@ +--- +title: Move Auto DevOps Test.gitlab-ci.yml template to rules syntax instead of only/except +merge_request: 30876 +author: +type: changed diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index d5a048f15aa..960a2eeae10 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -32,6 +32,8 @@ - 1 - - authorized_keys - 2 +- - authorized_project_update + - 1 - - authorized_project_update_user_refresh_with_low_urgency - 1 - - authorized_projects diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 317cd05850d..857de2f7fb6 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -6027,7 +6027,7 @@ type Metadata { type MetricsDashboard { """ - Annotations added to the dashboard. Will always return `null` if `metrics_dashboard_annotations` feature flag is disabled + Annotations added to the dashboard """ annotations( """ @@ -8504,7 +8504,7 @@ input RemoveProjectFromSecurityDashboardInput { """ ID of the project to remove from the Instance Security Dashboard """ - projectId: ID! + id: ID! } """ diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 776dd968273..e51f44db2ff 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -16917,7 +16917,7 @@ "fields": [ { "name": "annotations", - "description": "Annotations added to the dashboard. Will always return `null` if `metrics_dashboard_annotations` feature flag is disabled", + "description": "Annotations added to the dashboard", "args": [ { "name": "from", @@ -24943,7 +24943,7 @@ "fields": null, "inputFields": [ { - "name": "projectId", + "name": "id", "description": "ID of the project to remove from the Instance Security Dashboard", "type": { "kind": "NON_NULL", diff --git a/doc/ci/examples/test-scala-application.md b/doc/ci/examples/test-scala-application.md index 4da62c95a69..1831417e48e 100644 --- a/doc/ci/examples/test-scala-application.md +++ b/doc/ci/examples/test-scala-application.md @@ -14,7 +14,7 @@ The following `.gitlab-ci.yml` should be added in the root of your repository to trigger CI: ``` yaml -image: java:8 +image: openjdk:8 stages: - test diff --git a/doc/user/application_security/security_dashboard/img/instance_security_dashboard_export_csv_v13_0.png b/doc/user/application_security/security_dashboard/img/instance_security_dashboard_export_csv_v13_0.png Binary files differnew file mode 100644 index 00000000000..d767c159e8d --- /dev/null +++ b/doc/user/application_security/security_dashboard/img/instance_security_dashboard_export_csv_v13_0.png diff --git a/doc/user/application_security/security_dashboard/img/project_security_dashboard_export_csv_v12.10.png b/doc/user/application_security/security_dashboard/img/project_security_dashboard_export_csv_v12_10.png Binary files differindex 07b41b471d4..07b41b471d4 100644 --- a/doc/user/application_security/security_dashboard/img/project_security_dashboard_export_csv_v12.10.png +++ b/doc/user/application_security/security_dashboard/img/project_security_dashboard_export_csv_v12_10.png diff --git a/doc/user/application_security/security_dashboard/index.md b/doc/user/application_security/security_dashboard/index.md index 59aeba9d655..8776b626bec 100644 --- a/doc/user/application_security/security_dashboard/index.md +++ b/doc/user/application_security/security_dashboard/index.md @@ -67,7 +67,7 @@ NOTE: **Note:** It may take several minutes for the download to start if your project consists of thousands of vulnerabilities. Do not close the page until the download finishes. -![CSV Export Button](img/project_security_dashboard_export_csv_v12.10.png) +![CSV Export Button](img/project_security_dashboard_export_csv_v12_10.png) ## Group Security Dashboard @@ -152,6 +152,22 @@ projects. ![Instance Security Dashboard with projects](img/instance_security_dashboard_with_projects_v12_8.png) +### Export vulnerabilities + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/213014) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.0. + +You can export all your vulnerabilities as CSV by clicking the **{upload}** **Export** +button located at top right of the **Instance Security Dashboard**. After the report +is built, the CSV report downloads to your local machine. The report contains all +vulnerabilities for the projects defined in the **Instance Security Dashboard**, +as filters don't apply to the export function. + +NOTE: **Note:** +It may take several minutes for the download to start if your project contains +thousands of vulnerabilities. Do not close the page until the download finishes. + +![CSV Export Button](img/instance_security_dashboard_export_csv_v13_0.png) + ## Keeping the dashboards up to date The Security Dashboard displays information from the results of the most recent diff --git a/lib/api/entities/user_basic.rb b/lib/api/entities/user_basic.rb index e063aa42855..80f3ee7b502 100644 --- a/lib/api/entities/user_basic.rb +++ b/lib/api/entities/user_basic.rb @@ -18,3 +18,5 @@ module API end end end + +API::Entities::UserBasic.prepend_if_ee('EE::API::Entities::UserBasic') diff --git a/lib/api/entities/user_path.rb b/lib/api/entities/user_path.rb index 7d922b39654..3f007659813 100644 --- a/lib/api/entities/user_path.rb +++ b/lib/api/entities/user_path.rb @@ -12,3 +12,5 @@ module API end end end + +API::Entities::UserPath.prepend_if_ee('EE::API::Entities::UserPath') diff --git a/lib/api/metrics/dashboard/annotations.rb b/lib/api/metrics/dashboard/annotations.rb index 432fa3ac0c9..81fee166175 100644 --- a/lib/api/metrics/dashboard/annotations.rb +++ b/lib/api/metrics/dashboard/annotations.rb @@ -28,8 +28,6 @@ module API post ':id/metrics_dashboard/annotations' do annotations_source_object = annotations_source[:class].find(params[:id]) - not_found! unless Feature.enabled?(:metrics_dashboard_annotations, annotations_source_object.project) - forbidden! unless can?(current_user, :create_metrics_dashboard_annotation, annotations_source_object) create_service_params = declared(params).merge(annotations_source[:create_service_param_key] => annotations_source_object) diff --git a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml index 3949b87bbda..787f07521e0 100644 --- a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml @@ -15,6 +15,5 @@ build: export CI_APPLICATION_TAG=${CI_APPLICATION_TAG:-$CI_COMMIT_TAG} fi - /build/build.sh - only: - - branches - - tags + rules: + - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' diff --git a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml index 9c4699f1f44..24e75c56a75 100644 --- a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml @@ -26,10 +26,7 @@ code_quality: codequality: gl-code-quality-report.json expire_in: 1 week dependencies: [] - only: - refs: - - branches - - tags - except: - variables: - - $CODE_QUALITY_DISABLED + rules: + - if: '$CODE_QUALITY_DISABLED' + when: never + - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml index 9bf0d31409a..7f98f0074d8 100644 --- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml @@ -18,16 +18,14 @@ review: on_stop: stop_review artifacts: paths: [environment_url.txt] - only: - refs: - - branches - - tags - kubernetes: active - except: - refs: - - master - variables: - - $REVIEW_DISABLED + rules: + - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""' + when: never + - if: '$CI_COMMIT_BRANCH == "master"' + when: never + - if: '$REVIEW_DISABLED' + when: never + - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' stop_review: extends: .auto-deploy @@ -41,18 +39,16 @@ stop_review: name: review/$CI_COMMIT_REF_NAME action: stop dependencies: [] - when: manual allow_failure: true - only: - refs: - - branches - - tags - kubernetes: active - except: - refs: - - master - variables: - - $REVIEW_DISABLED + rules: + - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""' + when: never + - if: '$CI_COMMIT_BRANCH == "master"' + when: never + - if: '$REVIEW_DISABLED' + when: never + - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' + when: manual # Staging deploys are disabled by default since # continuous deployment to production is enabled by default @@ -73,12 +69,12 @@ staging: environment: name: staging url: http://$CI_PROJECT_PATH_SLUG-staging.$KUBE_INGRESS_BASE_DOMAIN - only: - refs: - - master - kubernetes: active - variables: - - $STAGING_ENABLED + rules: + - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""' + when: never + - if: '$CI_COMMIT_BRANCH != "master"' + when: never + - if: '$STAGING_ENABLED' # Canaries are disabled by default, but if you want them, # and know what the downsides are, you can enable this by setting @@ -97,13 +93,13 @@ canary: environment: name: production url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN - when: manual - only: - refs: - - master - kubernetes: active - variables: - - $CANARY_ENABLED + rules: + - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""' + when: never + - if: '$CI_COMMIT_BRANCH != "master"' + when: never + - if: '$CANARY_ENABLED' + when: manual .production: &production_template extends: .auto-deploy @@ -126,32 +122,33 @@ canary: production: <<: *production_template - only: - refs: - - master - kubernetes: active - except: - variables: - - $STAGING_ENABLED - - $CANARY_ENABLED - - $INCREMENTAL_ROLLOUT_ENABLED - - $INCREMENTAL_ROLLOUT_MODE + rules: + - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""' + when: never + - if: '$STAGING_ENABLED' + when: never + - if: '$CANARY_ENABLED' + when: never + - if: '$INCREMENTAL_ROLLOUT_ENABLED' + when: never + - if: '$INCREMENTAL_ROLLOUT_MODE' + when: never + - if: '$CI_COMMIT_BRANCH == "master"' production_manual: <<: *production_template - when: manual allow_failure: false - only: - refs: - - master - kubernetes: active - variables: - - $STAGING_ENABLED - - $CANARY_ENABLED - except: - variables: - - $INCREMENTAL_ROLLOUT_ENABLED - - $INCREMENTAL_ROLLOUT_MODE + rules: + - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""' + when: never + - if: '$INCREMENTAL_ROLLOUT_ENABLED' + when: never + - if: '$INCREMENTAL_ROLLOUT_MODE' + when: never + - if: '$CI_COMMIT_BRANCH == "master" && $STAGING_ENABLED' + when: manual + - if: '$CI_COMMIT_BRANCH == "master" && $CANARY_ENABLED' + when: manual # This job implements incremental rollout on for every push to `master`. @@ -176,29 +173,29 @@ production_manual: .manual_rollout_template: &manual_rollout_template <<: *rollout_template stage: production - when: manual - # This selectors are backward compatible mode with $INCREMENTAL_ROLLOUT_ENABLED (before 11.4) - only: - refs: - - master - kubernetes: active - variables: - - $INCREMENTAL_ROLLOUT_MODE == "manual" - - $INCREMENTAL_ROLLOUT_ENABLED - except: - variables: - - $INCREMENTAL_ROLLOUT_MODE == "timed" + rules: + - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""' + when: never + - if: '$INCREMENTAL_ROLLOUT_MODE == "timed"' + when: never + - if: '$CI_COMMIT_BRANCH != "master"' + when: never + # $INCREMENTAL_ROLLOUT_ENABLED is for compamtibilty with pre-GitLab 11.4 syntax + - if: '$INCREMENTAL_ROLLOUT_MODE == "manual" || $INCREMENTAL_ROLLOUT_ENABLED' + when: manual .timed_rollout_template: &timed_rollout_template <<: *rollout_template - when: delayed - start_in: 5 minutes - only: - refs: - - master - kubernetes: active - variables: - - $INCREMENTAL_ROLLOUT_MODE == "timed" + rules: + - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""' + when: never + - if: '$INCREMENTAL_ROLLOUT_MODE == "manual"' + when: never + - if: '$CI_COMMIT_BRANCH != "master"' + when: never + - if: '$INCREMENTAL_ROLLOUT_MODE == "timed"' + when: delayed + start_in: 5 minutes timed rollout 10%: <<: *timed_rollout_template diff --git a/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml index c6b0de44207..61808eae142 100644 --- a/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml @@ -16,9 +16,7 @@ test: - export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DB_HOST}:5432/${POSTGRES_DB}" - cp -R . /tmp/app - /bin/herokuish buildpack test - only: - - branches - - tags - except: - variables: - - $TEST_DISABLED + rules: + - if: '$TEST_DISABLED' + when: never + - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH' diff --git a/lib/gitlab/ci/templates/Scala.gitlab-ci.yml b/lib/gitlab/ci/templates/Scala.gitlab-ci.yml index b4208ed9d7d..e081e20564a 100644 --- a/lib/gitlab/ci/templates/Scala.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Scala.gitlab-ci.yml @@ -1,7 +1,7 @@ -# Official Java image. Look for the different tagged releases at -# https://hub.docker.com/r/library/java/tags/ . A Java image is not required +# Official OpenJDK Java image. Look for the different tagged releases at +# https://hub.docker.com/_/openjdk/ . A Java image is not required # but an image with a JVM speeds up the build a bit. -image: java:8 +image: openjdk:8 before_script: # Enable the usage of sources over https @@ -14,7 +14,7 @@ before_script: - apt-get update -yqq - apt-get install sbt -yqq # Log the sbt version - - sbt sbt-version + - sbt sbtVersion test: script: diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 133c5a3659d..50a78658c28 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1721,6 +1721,9 @@ msgstr "" msgid "AlertManagement|Create issue" msgstr "" +msgid "AlertManagement|Critical" +msgstr "" + msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents." msgstr "" @@ -1739,6 +1742,18 @@ msgstr "" msgid "AlertManagement|Full Alert Details" msgstr "" +msgid "AlertManagement|High" +msgstr "" + +msgid "AlertManagement|Info" +msgstr "" + +msgid "AlertManagement|Low" +msgstr "" + +msgid "AlertManagement|Medium" +msgstr "" + msgid "AlertManagement|More information" msgstr "" @@ -1778,6 +1793,9 @@ msgstr "" msgid "AlertManagement|Triggered" msgstr "" +msgid "AlertManagement|Unknown" +msgstr "" + msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts." msgstr "" @@ -3545,6 +3563,9 @@ msgstr "" msgid "Can override approvers and approvals required per merge request" msgstr "" +msgid "Can't edit as source branch was deleted" +msgstr "" + msgid "Can't find HEAD commit for this branch" msgstr "" @@ -11615,6 +11636,9 @@ msgstr "" msgid "Invalid start or end time format" msgstr "" +msgid "Invalid status" +msgstr "" + msgid "Invalid two-factor code." msgstr "" diff --git a/spec/controllers/concerns/metrics_dashboard_spec.rb b/spec/controllers/concerns/metrics_dashboard_spec.rb index 4e42171e3d3..3a6a037ac9a 100644 --- a/spec/controllers/concerns/metrics_dashboard_spec.rb +++ b/spec/controllers/concerns/metrics_dashboard_spec.rb @@ -114,6 +114,35 @@ describe MetricsDashboard do end end end + + context 'starred dashboards' do + let_it_be(:dashboard_yml) { fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') } + let_it_be(:dashboards) do + { + '.gitlab/dashboards/test.yml' => dashboard_yml, + '.gitlab/dashboards/anomaly.yml' => dashboard_yml, + '.gitlab/dashboards/errors.yml' => dashboard_yml + } + end + let_it_be(:project) { create(:project, :custom_repo, files: dashboards) } + + before do + create(:metrics_users_starred_dashboard, user: user, project: project, dashboard_path: '.gitlab/dashboards/errors.yml') + create(:metrics_users_starred_dashboard, user: user, project: project, dashboard_path: '.gitlab/dashboards/test.yml') + end + + it 'adds starred dashboard information and sorts the list' do + all_dashboards = json_response['all_dashboards'].map { |dashboard| dashboard.slice('display_name', 'starred', 'user_starred_path') } + expected_response = [ + { "display_name" => "errors.yml", "starred" => true, 'user_starred_path' => nil }, + { "display_name" => "test.yml", "starred" => true, 'user_starred_path' => nil }, + { "display_name" => "anomaly.yml", "starred" => false, 'user_starred_path' => nil }, + { "display_name" => "Default", "starred" => false, 'user_starred_path' => nil } + ] + + expect(all_dashboards).to eql expected_response + end + end end end end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index a22dc77997b..17d7e710614 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -825,7 +825,7 @@ describe Projects::IssuesController do update_issue(issue_params: { assignee_ids: [assignee.id] }) expect(json_response['assignees'].first.keys) - .to match_array(%w(id name username avatar_url state web_url)) + .to include(*%w(id name username avatar_url state web_url)) end end @@ -1408,6 +1408,7 @@ describe Projects::IssuesController do it 'render merge request as json' do create_merge_request + expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('merge_request') end diff --git a/spec/factories/alert_management/alerts.rb b/spec/factories/alert_management/alerts.rb index 28cfe5d6b29..d98fed4a6b1 100644 --- a/spec/factories/alert_management/alerts.rb +++ b/spec/factories/alert_management/alerts.rb @@ -3,6 +3,7 @@ require 'ffaker' FactoryBot.define do factory :alert_management_alert, class: 'AlertManagement::Alert' do + triggered project title { FFaker::Lorem.sentence } started_at { Time.current } @@ -35,6 +36,11 @@ FactoryBot.define do ended_at { nil } end + trait :triggered do + status { AlertManagement::Alert::STATUSES[:triggered] } + without_ended_at + end + trait :acknowledged do status { AlertManagement::Alert::STATUSES[:acknowledged] } without_ended_at diff --git a/spec/fixtures/api/schemas/entities/discussion.json b/spec/fixtures/api/schemas/entities/discussion.json index 9d7ca62435e..21d8efe0b2b 100644 --- a/spec/fixtures/api/schemas/entities/discussion.json +++ b/spec/fixtures/api/schemas/entities/discussion.json @@ -29,8 +29,15 @@ "web_url": { "type": "uri" }, "status_tooltip_html": { "type": ["string", "null"] }, "path": { "type": "string" } - }, - "additionalProperties": false + }, + "required": [ + "id", + "state", + "avatar_url", + "path", + "name", + "username" + ] }, "created_at": { "type": "date" }, "updated_at": { "type": "date" }, diff --git a/spec/fixtures/api/schemas/entities/note_user_entity.json b/spec/fixtures/api/schemas/entities/note_user_entity.json index 9b838054563..4a27d885cdc 100644 --- a/spec/fixtures/api/schemas/entities/note_user_entity.json +++ b/spec/fixtures/api/schemas/entities/note_user_entity.json @@ -16,6 +16,5 @@ "name": { "type": "string" }, "username": { "type": "string" }, "status_tooltip_html": { "$ref": "../types/nullable_string.json" } - }, - "additionalProperties": false + } } diff --git a/spec/fixtures/api/schemas/entities/user.json b/spec/fixtures/api/schemas/entities/user.json index 82d80b75cef..3252a37c82a 100644 --- a/spec/fixtures/api/schemas/entities/user.json +++ b/spec/fixtures/api/schemas/entities/user.json @@ -18,6 +18,5 @@ "name": { "type": "string" }, "username": { "type": "string" }, "status_tooltip_html": { "$ref": "../types/nullable_string.json" } - }, - "additionalProperties": false + } } diff --git a/spec/fixtures/api/schemas/pipeline_schedule.json b/spec/fixtures/api/schemas/pipeline_schedule.json index 690c4a7d4e8..d01801a15fa 100644 --- a/spec/fixtures/api/schemas/pipeline_schedule.json +++ b/spec/fixtures/api/schemas/pipeline_schedule.json @@ -30,7 +30,9 @@ "avatar_url": { "type": "uri" }, "web_url": { "type": "uri" } }, - "additionalProperties": false + "required": [ + "id", "name", "username", "state", "avatar_url", "web_url" + ] }, "variables": { "type": "array", diff --git a/spec/fixtures/api/schemas/public_api/v4/issue.json b/spec/fixtures/api/schemas/public_api/v4/issue.json index bf1b4a06f0b..69ecba8b6f3 100644 --- a/spec/fixtures/api/schemas/public_api/v4/issue.json +++ b/spec/fixtures/api/schemas/public_api/v4/issue.json @@ -71,7 +71,14 @@ "avatar_url": { "type": "uri" }, "web_url": { "type": "uri" } }, - "additionalProperties": false + "required": [ + "id", + "state", + "avatar_url", + "name", + "username", + "web_url" + ] }, "user_notes_count": { "type": "integer" }, "upvotes": { "type": "integer" }, diff --git a/spec/fixtures/api/schemas/public_api/v4/members.json b/spec/fixtures/api/schemas/public_api/v4/members.json index 38ad64ad061..695f00b0040 100644 --- a/spec/fixtures/api/schemas/public_api/v4/members.json +++ b/spec/fixtures/api/schemas/public_api/v4/members.json @@ -15,8 +15,7 @@ }, "required": [ "id", "name", "username", "state", - "web_url", "access_level", "expires_at" - ], - "additionalProperties": false + "web_url", "access_level", "expires_at", "avatar_url" + ] } } diff --git a/spec/fixtures/api/schemas/public_api/v4/notes.json b/spec/fixtures/api/schemas/public_api/v4/notes.json index d15d2e90b05..683dcb19836 100644 --- a/spec/fixtures/api/schemas/public_api/v4/notes.json +++ b/spec/fixtures/api/schemas/public_api/v4/notes.json @@ -17,7 +17,9 @@ "avatar_url": { "type": "uri" }, "web_url": { "type": "uri" } }, - "additionalProperties": false + "required" : [ + "id", "name", "username", "state", "avatar_url", "web_url" + ] }, "commands_changes": { "type": "object", "additionalProperties": true }, "created_at": { "type": "date" }, diff --git a/spec/fixtures/api/schemas/public_api/v4/snippets.json b/spec/fixtures/api/schemas/public_api/v4/snippets.json index ddddd46f5c4..7baa24a6f1f 100644 --- a/spec/fixtures/api/schemas/public_api/v4/snippets.json +++ b/spec/fixtures/api/schemas/public_api/v4/snippets.json @@ -23,7 +23,9 @@ "avatar_url": { "type": "uri" }, "web_url": { "type": "uri" } }, - "additionalProperties": false + "required" : [ + "id", "name", "username", "state", "avatar_url", "web_url" + ] } }, "required": [ diff --git a/spec/frontend/alert_management/components/alert_management_detail_spec.js b/spec/frontend/alert_management/components/alert_management_detail_spec.js index 55ac62eccdf..fa32707ead5 100644 --- a/spec/frontend/alert_management/components/alert_management_detail_spec.js +++ b/spec/frontend/alert_management/components/alert_management_detail_spec.js @@ -1,11 +1,16 @@ import { shallowMount } from '@vue/test-utils'; +import { GlLoadingIcon } from '@gitlab/ui'; import AlertDetails from '~/alert_management/components/alert_details.vue'; describe('AlertDetails', () => { let wrapper; const newIssuePath = 'root/alerts/-/issues/new'; - function mountComponent(alert = {}, createIssueFromAlertEnabled = false) { + function mountComponent({ + alert = {}, + createIssueFromAlertEnabled = false, + loading = false, + } = {}) { wrapper = shallowMount(AlertDetails, { propsData: { alertId: 'alertId', @@ -18,6 +23,15 @@ describe('AlertDetails', () => { provide: { glFeatures: { createIssueFromAlertEnabled }, }, + mocks: { + $apollo: { + queries: { + alert: { + loading, + }, + }, + }, + }, }); } @@ -32,17 +46,11 @@ describe('AlertDetails', () => { describe('Alert details', () => { describe('when alert is null', () => { beforeEach(() => { - mountComponent(null); + mountComponent({ alert: null }); }); - describe('when alert is null', () => { - beforeEach(() => { - mountComponent(null); - }); - - it('shows an empty state', () => { - expect(wrapper.find('[data-testid="alertDetailsTabs"]').exists()).toBe(false); - }); + it('shows an empty state', () => { + expect(wrapper.find('[data-testid="alertDetailsTabs"]').exists()).toBe(false); }); }); @@ -71,7 +79,7 @@ describe('AlertDetails', () => { describe('Create issue from alert', () => { describe('createIssueFromAlertEnabled feature flag enabled', () => { it('should display a button that links to new issue page', () => { - mountComponent({}, true); + mountComponent({ createIssueFromAlertEnabled: true }); expect(findCreatedIssueBtn().exists()).toBe(true); expect(findCreatedIssueBtn().attributes('href')).toBe(newIssuePath); }); @@ -79,10 +87,20 @@ describe('AlertDetails', () => { describe('createIssueFromAlertEnabled feature flag disabled', () => { it('should display a button that links to a new issue page', () => { - mountComponent({}, false); + mountComponent({ createIssueFromAlertEnabled: false }); expect(findCreatedIssueBtn().exists()).toBe(false); }); }); }); + + describe('loading state', () => { + beforeEach(() => { + mountComponent({ loading: true }); + }); + + it('displays a loading state when loading', () => { + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + }); + }); }); }); diff --git a/spec/frontend/alert_management/components/alert_management_list_spec.js b/spec/frontend/alert_management/components/alert_management_list_spec.js index d7170e71a96..81d966a42b5 100644 --- a/spec/frontend/alert_management/components/alert_management_list_spec.js +++ b/spec/frontend/alert_management/components/alert_management_list_spec.js @@ -26,6 +26,7 @@ describe('AlertManagementList', () => { const findStatusFilterTabs = () => wrapper.findAll(GlTab); const findNumberOfAlertsBadge = () => wrapper.findAll(GlBadge); const findDateFields = () => wrapper.findAll(TimeAgo); + const findSeverityFields = () => wrapper.findAll('[data-testid="severityField"]'); function mountComponent({ props = { @@ -201,6 +202,20 @@ describe('AlertManagementList', () => { }); }); + it('Internationalizes severity text', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: mockAlerts, errored: false }, + loading: false, + }); + + expect( + findSeverityFields() + .at(0) + .text(), + ).toBe('Critical'); + }); + describe('handle date fields', () => { it('should display time ago dates when values provided', () => { mountComponent({ diff --git a/spec/frontend/alert_management/mocks/alerts.json b/spec/frontend/alert_management/mocks/alerts.json index d4667eb21f8..e0b8fa55507 100644 --- a/spec/frontend/alert_management/mocks/alerts.json +++ b/spec/frontend/alert_management/mocks/alerts.json @@ -2,7 +2,7 @@ { "iid": "1527542", "title": "SyntaxError: Invalid or unexpected token", - "severity": "Critical", + "severity": "CRITICAL", "eventCount": 7, "startedAt": "2020-04-17T23:18:14.996Z", "endedAt": "2020-04-17T23:18:14.996Z", @@ -11,7 +11,7 @@ { "iid": "1527542", "title": "Some otherr alert Some otherr alert Some otherr alert Some otherr alert Some otherr alert Some otherr alert", - "severity": "Medium", + "severity": "MEDIUM", "eventCount": 1, "startedAt": "2020-04-17T23:18:14.996Z", "endedAt": "2020-04-17T23:18:14.996Z", @@ -20,7 +20,7 @@ { "iid": "1527542", "title": "SyntaxError: Invalid or unexpected token", - "severity": "Low", + "severity": "LOW", "eventCount": 4, "startedAt": "2020-04-17T23:18:14.996Z", "endedAt": "2020-04-17T23:18:14.996Z", diff --git a/spec/frontend/diffs/components/edit_button_spec.js b/spec/frontend/diffs/components/edit_button_spec.js index f9a1d4a84a8..71512c1c4af 100644 --- a/spec/frontend/diffs/components/edit_button_spec.js +++ b/spec/frontend/diffs/components/edit_button_spec.js @@ -1,4 +1,5 @@ import { shallowMount } from '@vue/test-utils'; +import { GlDeprecatedButton } from '@gitlab/ui'; import EditButton from '~/diffs/components/edit_button.vue'; const editPath = 'test-path'; @@ -22,7 +23,7 @@ describe('EditButton', () => { canCurrentUserFork: false, }); - expect(wrapper.attributes('href')).toBe(editPath); + expect(wrapper.find(GlDeprecatedButton).attributes('href')).toBe(editPath); }); it('emits a show fork message event if current user can fork', () => { @@ -30,7 +31,7 @@ describe('EditButton', () => { editPath, canCurrentUserFork: true, }); - wrapper.trigger('click'); + wrapper.find(GlDeprecatedButton).trigger('click'); return wrapper.vm.$nextTick().then(() => { expect(wrapper.emitted('showForkMessage')).toBeTruthy(); @@ -42,7 +43,7 @@ describe('EditButton', () => { editPath, canCurrentUserFork: false, }); - wrapper.trigger('click'); + wrapper.find(GlDeprecatedButton).trigger('click'); return wrapper.vm.$nextTick().then(() => { expect(wrapper.emitted('showForkMessage')).toBeFalsy(); @@ -55,10 +56,20 @@ describe('EditButton', () => { canCurrentUserFork: true, canModifyBlob: true, }); - wrapper.trigger('click'); + wrapper.find(GlDeprecatedButton).trigger('click'); return wrapper.vm.$nextTick().then(() => { expect(wrapper.emitted('showForkMessage')).toBeFalsy(); }); }); + + it('disables button if editPath is empty', () => { + createComponent({ + editPath: '', + canCurrentUserFork: true, + canModifyBlob: true, + }); + + expect(wrapper.find(GlDeprecatedButton).attributes('disabled')).toBe('true'); + }); }); diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js index d9794c34b3b..901b698b703 100644 --- a/spec/frontend/monitoring/store/actions_spec.js +++ b/spec/frontend/monitoring/store/actions_spec.js @@ -97,7 +97,11 @@ describe('Monitoring store actions', () => { null, state, [], - [{ type: 'fetchEnvironmentsData' }, { type: 'fetchDashboard' }], + [ + { type: 'fetchEnvironmentsData' }, + { type: 'fetchDashboard' }, + { type: 'fetchAnnotations' }, + ], ); }); diff --git a/spec/graphql/mutations/alert_management/update_alert_status_spec.rb b/spec/graphql/mutations/alert_management/update_alert_status_spec.rb index 126362d024a..8b9abd9497d 100644 --- a/spec/graphql/mutations/alert_management/update_alert_status_spec.rb +++ b/spec/graphql/mutations/alert_management/update_alert_status_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' describe Mutations::AlertManagement::UpdateAlertStatus do let_it_be(:current_user) { create(:user) } - let_it_be(:alert) { create(:alert_management_alert, status: 'triggered') } + let_it_be(:alert) { create(:alert_management_alert, :triggered) } let_it_be(:project) { alert.project } let(:new_status) { Types::AlertManagement::StatusEnum.values['ACKNOWLEDGED'].value } let(:args) { { status: new_status, project_path: project.full_path, iid: alert.iid } } @@ -53,7 +53,7 @@ describe Mutations::AlertManagement::UpdateAlertStatus do it 'returns the alert with errors' do expect(resolve).to eq( alert: alert, - errors: ['Invalid status'] + errors: [_('Invalid status')] ) end end diff --git a/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb new file mode 100644 index 00000000000..b2a9e3f5cf4 --- /dev/null +++ b/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Jobs/Build.gitlab-ci.yml' do + subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Jobs/Build') } + + describe 'the created pipeline' do + let_it_be(:user) { create(:admin) } + let_it_be(:project) { create(:project, :repository) } + + let(:default_branch) { 'master' } + let(:pipeline_ref) { default_branch } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) } + let(:pipeline) { service.execute!(:push) } + let(:build_names) { pipeline.builds.pluck(:name) } + + before do + stub_ci_pipeline_yaml_file(template.content) + allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true) + allow(project).to receive(:default_branch).and_return(default_branch) + end + + context 'on master' do + it 'creates the build job' do + expect(build_names).to contain_exactly('build') + end + end + + context 'on another branch' do + let(:pipeline_ref) { 'feature' } + + it 'creates the build job' do + expect(build_names).to contain_exactly('build') + end + end + + context 'on tag' do + let(:pipeline_ref) { 'v1.0.0' } + + it 'creates the build job' do + expect(pipeline).to be_tag + expect(build_names).to contain_exactly('build') + end + end + + context 'on merge request' do + let(:service) { MergeRequests::CreatePipelineService.new(project, user) } + let(:merge_request) { create(:merge_request, :simple, source_project: project) } + let(:pipeline) { service.execute(merge_request) } + + it 'has no jobs' do + expect(pipeline).to be_merge_request_event + expect(build_names).to be_empty + end + end + end +end diff --git a/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb new file mode 100644 index 00000000000..9c5b2fd5099 --- /dev/null +++ b/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Jobs/Code-Quality.gitlab-ci.yml' do + subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Jobs/Code-Quality') } + + describe 'the created pipeline' do + let_it_be(:user) { create(:admin) } + let_it_be(:project) { create(:project, :repository) } + + let(:default_branch) { 'master' } + let(:pipeline_ref) { default_branch } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) } + let(:pipeline) { service.execute!(:push) } + let(:build_names) { pipeline.builds.pluck(:name) } + + before do + stub_ci_pipeline_yaml_file(template.content) + allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true) + allow(project).to receive(:default_branch).and_return(default_branch) + end + + context 'on master' do + it 'creates the code_quality job' do + expect(build_names).to contain_exactly('code_quality') + end + end + + context 'on another branch' do + let(:pipeline_ref) { 'feature' } + + it 'creates the code_quality job' do + expect(build_names).to contain_exactly('code_quality') + end + end + + context 'on tag' do + let(:pipeline_ref) { 'v1.0.0' } + + it 'creates the code_quality job' do + expect(pipeline).to be_tag + expect(build_names).to contain_exactly('code_quality') + end + end + + context 'on merge request' do + let(:service) { MergeRequests::CreatePipelineService.new(project, user) } + let(:merge_request) { create(:merge_request, :simple, source_project: project) } + let(:pipeline) { service.execute(merge_request) } + + it 'has no jobs' do + expect(pipeline).to be_merge_request_event + expect(build_names).to be_empty + end + end + + context 'CODE_QUALITY_DISABLED is set' do + before do + create(:ci_variable, key: 'CODE_QUALITY_DISABLED', value: 'true', project: project) + end + + context 'on master' do + it 'has no jobs' do + expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError) + end + end + + context 'on another branch' do + let(:pipeline_ref) { 'feature' } + + it 'has no jobs' do + expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError) + end + end + + context 'on tag' do + let(:pipeline_ref) { 'v1.0.0' } + + it 'has no jobs' do + expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError) + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb new file mode 100644 index 00000000000..a6ae23c85d3 --- /dev/null +++ b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb @@ -0,0 +1,222 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Jobs/Deploy.gitlab-ci.yml' do + subject(:template) do + <<~YAML + stages: + - test + - review + - staging + - canary + - production + - incremental rollout 10% + - incremental rollout 25% + - incremental rollout 50% + - incremental rollout 100% + - cleanup + + include: + - template: Jobs/Deploy.gitlab-ci.yml + + placeholder: + script: + - echo "Ensure at least one job to keep pipeline validator happy" + YAML + end + + describe 'the created pipeline' do + let(:user) { create(:admin) } + let(:project) { create(:project, :repository) } + + let(:default_branch) { 'master' } + let(:pipeline_ref) { default_branch } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) } + let(:pipeline) { service.execute!(:push) } + let(:build_names) { pipeline.builds.pluck(:name) } + + before do + stub_ci_pipeline_yaml_file(template) + + allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true) + allow(project).to receive(:default_branch).and_return(default_branch) + end + + context 'with no cluster' do + it 'does not create any kubernetes deployment jobs' do + expect(build_names).to eq %w(placeholder) + end + end + + context 'with only a disabled cluster' do + let!(:cluster) { create(:cluster, :project, :provided_by_gcp, enabled: false, projects: [project]) } + + it 'does not create any kubernetes deployment jobs' do + expect(build_names).to eq %w(placeholder) + end + end + + context 'with an active cluster' do + let!(:cluster) { create(:cluster, :project, :provided_by_gcp, projects: [project]) } + + context 'on master' do + it 'by default' do + expect(build_names).to include('production') + expect(build_names).not_to include('review') + end + + it 'when CANARY_ENABLED' do + create(:ci_variable, project: project, key: 'CANARY_ENABLED', value: 'true') + + expect(build_names).to include('production_manual') + expect(build_names).to include('canary') + expect(build_names).not_to include('production') + end + + it 'when STAGING_ENABLED' do + create(:ci_variable, project: project, key: 'STAGING_ENABLED', value: 'true') + + expect(build_names).to include('production_manual') + expect(build_names).to include('staging') + expect(build_names).not_to include('production') + end + + it 'when INCREMENTAL_ROLLOUT_MODE == timed' do + create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_ENABLED', value: 'true') + create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_MODE', value: 'timed') + + expect(build_names).not_to include('production_manual') + expect(build_names).not_to include('production') + expect(build_names).not_to include( + 'rollout 10%', + 'rollout 25%', + 'rollout 50%', + 'rollout 100%' + ) + expect(build_names).to include( + 'timed rollout 10%', + 'timed rollout 25%', + 'timed rollout 50%', + 'timed rollout 100%' + ) + end + + it 'when INCREMENTAL_ROLLOUT_ENABLED' do + create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_ENABLED', value: 'true') + + expect(build_names).not_to include('production_manual') + expect(build_names).not_to include('production') + expect(build_names).not_to include( + 'timed rollout 10%', + 'timed rollout 25%', + 'timed rollout 50%', + 'timed rollout 100%' + ) + expect(build_names).to include( + 'rollout 10%', + 'rollout 25%', + 'rollout 50%', + 'rollout 100%' + ) + end + + it 'when INCREMENTAL_ROLLOUT_MODE == manual' do + create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_MODE', value: 'manual') + + expect(build_names).not_to include('production_manual') + expect(build_names).not_to include('production') + expect(build_names).not_to include( + 'timed rollout 10%', + 'timed rollout 25%', + 'timed rollout 50%', + 'timed rollout 100%' + ) + expect(build_names).to include( + 'rollout 10%', + 'rollout 25%', + 'rollout 50%', + 'rollout 100%' + ) + end + end + + shared_examples_for 'review app deployment' do + it 'creates the review and stop_review jobs but no production jobs' do + expect(build_names).to include('review') + expect(build_names).to include('stop_review') + expect(build_names).not_to include('production') + expect(build_names).not_to include('production_manual') + expect(build_names).not_to include('staging') + expect(build_names).not_to include('canary') + expect(build_names).not_to include('timed rollout 10%') + expect(build_names).not_to include('timed rollout 25%') + expect(build_names).not_to include('timed rollout 50%') + expect(build_names).not_to include('timed rollout 100%') + expect(build_names).not_to include('rollout 10%') + expect(build_names).not_to include('rollout 25%') + expect(build_names).not_to include('rollout 50%') + expect(build_names).not_to include('rollout 100%') + end + + it 'does not include review when REVIEW_DISABLED' do + create(:ci_variable, project: project, key: 'REVIEW_DISABLED', value: 'true') + + expect(build_names).not_to include('review') + expect(build_names).not_to include('stop_review') + end + end + + context 'on branch' do + let(:pipeline_ref) { 'feature' } + + before do + allow_any_instance_of(Gitlab::Ci::Pipeline::Chain::Validate::Repository).to receive(:perform!).and_return(true) + end + + it_behaves_like 'review app deployment' + + context 'when INCREMENTAL_ROLLOUT_ENABLED' do + before do + create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_ENABLED', value: 'true') + end + + it_behaves_like 'review app deployment' + end + + context 'when INCREMENTAL_ROLLOUT_MODE == "timed"' do + before do + create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_MODE', value: 'timed') + end + + it_behaves_like 'review app deployment' + end + + context 'when INCREMENTAL_ROLLOUT_MODE == "manual"' do + before do + create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_MODE', value: 'manual') + end + + it_behaves_like 'review app deployment' + end + end + + context 'on tag' do + let(:pipeline_ref) { 'v1.0.0' } + + it_behaves_like 'review app deployment' + end + + context 'on merge request' do + let(:service) { MergeRequests::CreatePipelineService.new(project, user) } + let(:merge_request) { create(:merge_request, :simple, source_project: project) } + let(:pipeline) { service.execute(merge_request) } + + it 'has no jobs' do + expect(pipeline).to be_merge_request_event + expect(build_names).to be_empty + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb new file mode 100644 index 00000000000..2186bf038eb --- /dev/null +++ b/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Jobs/Test.gitlab-ci.yml' do + subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Jobs/Test') } + + describe 'the created pipeline' do + let_it_be(:user) { create(:admin) } + let_it_be(:project) { create(:project, :repository) } + + let(:default_branch) { 'master' } + let(:pipeline_ref) { default_branch } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) } + let(:pipeline) { service.execute!(:push) } + let(:build_names) { pipeline.builds.pluck(:name) } + + before do + stub_ci_pipeline_yaml_file(template.content) + allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true) + allow(project).to receive(:default_branch).and_return(default_branch) + end + + context 'on master' do + it 'creates the test job' do + expect(build_names).to contain_exactly('test') + end + end + + context 'on another branch' do + let(:pipeline_ref) { 'feature' } + + it 'creates the test job' do + expect(build_names).to contain_exactly('test') + end + end + + context 'on tag' do + let(:pipeline_ref) { 'v1.0.0' } + + it 'creates the test job' do + expect(pipeline).to be_tag + expect(build_names).to contain_exactly('test') + end + end + + context 'on merge request' do + let(:service) { MergeRequests::CreatePipelineService.new(project, user) } + let(:merge_request) { create(:merge_request, :simple, source_project: project) } + let(:pipeline) { service.execute(merge_request) } + + it 'has no jobs' do + expect(pipeline).to be_merge_request_event + expect(build_names).to be_empty + end + end + + context 'TEST_DISABLED is set' do + before do + create(:ci_variable, key: 'TEST_DISABLED', value: 'true', project: project) + end + + context 'on master' do + it 'has no jobs' do + expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError) + end + end + + context 'on another branch' do + let(:pipeline_ref) { 'feature' } + + it 'has no jobs' do + expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError) + end + end + + context 'on tag' do + let(:pipeline_ref) { 'v1.0.0' } + + it 'has no jobs' do + expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError) + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb index 0c5d172f17c..e98f9fe1b04 100644 --- a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb @@ -40,11 +40,7 @@ describe 'Auto-DevOps.gitlab-ci.yml' do end context 'when the project has an active cluster' do - let(:cluster) { create(:cluster, :project, :provided_by_gcp, projects: [project]) } - - before do - allow(cluster).to receive(:active?).and_return(true) - end + let!(:cluster) { create(:cluster, :project, :provided_by_gcp, projects: [project]) } describe 'deployment-related builds' do context 'on default branch' do diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 5ac79807c78..8fa17026934 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -659,6 +659,42 @@ describe Group do end end + describe '#members_from_self_and_ancestors_with_effective_access_level' do + let!(:group_parent) { create(:group, :private) } + let!(:group) { create(:group, :private, parent: group_parent) } + let!(:group_child) { create(:group, :private, parent: group) } + + let!(:user) { create(:user) } + + let(:parent_group_access_level) { Gitlab::Access::REPORTER } + let(:group_access_level) { Gitlab::Access::DEVELOPER } + let(:child_group_access_level) { Gitlab::Access::MAINTAINER } + + before do + create(:group_member, user: user, group: group_parent, access_level: parent_group_access_level) + create(:group_member, user: user, group: group, access_level: group_access_level) + create(:group_member, user: user, group: group_child, access_level: child_group_access_level) + end + + it 'returns effective access level for user' do + expect(group_parent.members_from_self_and_ancestors_with_effective_access_level.as_json).to( + contain_exactly( + hash_including('user_id' => user.id, 'access_level' => parent_group_access_level) + ) + ) + expect(group.members_from_self_and_ancestors_with_effective_access_level.as_json).to( + contain_exactly( + hash_including('user_id' => user.id, 'access_level' => group_access_level) + ) + ) + expect(group_child.members_from_self_and_ancestors_with_effective_access_level.as_json).to( + contain_exactly( + hash_including('user_id' => user.id, 'access_level' => child_group_access_level) + ) + ) + end + end + describe '#direct_and_indirect_members' do let!(:group) { create(:group, :nested) } let!(:sub_group) { create(:group, parent: group) } diff --git a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb index ceeaa12a2bf..cb35411b7a5 100644 --- a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb +++ b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb @@ -21,6 +21,7 @@ describe 'Getting Metrics Dashboard Annotations' do create(:metrics_dashboard_annotation, environment: environment, starting_at: to.advance(minutes: 5), dashboard_path: path) end + let(:args) { "from: \"#{from}\", to: \"#{to}\"" } let(:fields) do <<~QUERY #{all_graphql_fields_for('MetricsDashboardAnnotation'.classify)} @@ -47,63 +48,40 @@ describe 'Getting Metrics Dashboard Annotations' do ) end - context 'feature flag metrics_dashboard_annotations' do - let(:args) { "from: \"#{from}\", to: \"#{to}\"" } + before do + project.add_developer(current_user) + post_graphql(query, current_user: current_user) + end - before do - project.add_developer(current_user) - end + it_behaves_like 'a working graphql query' - context 'is off' do - before do - stub_feature_flags(metrics_dashboard_annotations: false) - post_graphql(query, current_user: current_user) - end + it 'returns annotations' do + annotations = graphql_data.dig('project', 'environments', 'nodes')[0].dig('metricsDashboard', 'annotations', 'nodes') - it 'returns empty nodes array' do - annotations = graphql_data.dig('project', 'environments', 'nodes')[0].dig('metricsDashboard', 'annotations', 'nodes') + expect(annotations).to match_array [{ + "description" => annotation.description, + "id" => annotation.to_global_id.to_s, + "panelId" => annotation.panel_xid, + "startingAt" => annotation.starting_at.iso8601, + "endingAt" => nil + }] + end - expect(annotations).to be_empty - end - end + context 'arguments' do + context 'from is missing' do + let(:args) { "to: \"#{from}\"" } - context 'is on' do - before do - stub_feature_flags(metrics_dashboard_annotations: true) + it 'returns error' do post_graphql(query, current_user: current_user) - end - it_behaves_like 'a working graphql query' - - it 'returns annotations' do - annotations = graphql_data.dig('project', 'environments', 'nodes')[0].dig('metricsDashboard', 'annotations', 'nodes') - - expect(annotations).to match_array [{ - "description" => annotation.description, - "id" => annotation.to_global_id.to_s, - "panelId" => annotation.panel_xid, - "startingAt" => annotation.starting_at.iso8601, - "endingAt" => nil - }] + expect(graphql_errors[0]).to include("message" => "Field 'annotations' is missing required arguments: from") end + end - context 'arguments' do - context 'from is missing' do - let(:args) { "to: \"#{from}\"" } - - it 'returns error' do - post_graphql(query, current_user: current_user) - - expect(graphql_errors[0]).to include("message" => "Field 'annotations' is missing required arguments: from") - end - end - - context 'to is missing' do - let(:args) { "from: \"#{from}\"" } + context 'to is missing' do + let(:args) { "from: \"#{from}\"" } - it_behaves_like 'a working graphql query' - end - end + it_behaves_like 'a working graphql query' end end end diff --git a/spec/requests/api/metrics/dashboard/annotations_spec.rb b/spec/requests/api/metrics/dashboard/annotations_spec.rb index c6a41ee6444..ec88b4db256 100644 --- a/spec/requests/api/metrics/dashboard/annotations_spec.rb +++ b/spec/requests/api/metrics/dashboard/annotations_spec.rb @@ -19,75 +19,55 @@ describe API::Metrics::Dashboard::Annotations do end context "with :source_type == #{source_type.pluralize}" do - context 'feature flag metrics_dashboard_annotations' do - context 'is on' do - before do - stub_feature_flags(metrics_dashboard_annotations: { enabled: true, thing: project }) - end + context 'with correct permissions' do + context 'with valid parameters' do + it 'creates a new annotation', :aggregate_failures do + post api(url, user), params: params - context 'with correct permissions' do - context 'with valid parameters' do - it 'creates a new annotation', :aggregate_failures do - post api(url, user), params: params - - expect(response).to have_gitlab_http_status(:created) - expect(json_response["#{source_type}_id"]).to eq(source.id) - expect(json_response['starting_at'].to_time).to eq(starting_at.to_time) - expect(json_response['ending_at'].to_time).to eq(ending_at.to_time) - expect(json_response['description']).to eq(params[:description]) - expect(json_response['dashboard_path']).to eq(dashboard) - end - end - - context 'with invalid parameters' do - it 'returns error messsage' do - post api(url, user), params: { dashboard_path: nil, starting_at: nil, description: nil } - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']).to include({ "starting_at" => ["can't be blank"], "description" => ["can't be blank"], "dashboard_path" => ["can't be blank"] }) - end - end - - context 'with undeclared params' do - before do - params[:undeclared_param] = 'xyz' - end - - it 'filters out undeclared params' do - expect(::Metrics::Dashboard::Annotations::CreateService).to receive(:new).with(user, hash_excluding(:undeclared_param)) - - post api(url, user), params: params - end - end + expect(response).to have_gitlab_http_status(:created) + expect(json_response["#{source_type}_id"]).to eq(source.id) + expect(json_response['starting_at'].to_time).to eq(starting_at.to_time) + expect(json_response['ending_at'].to_time).to eq(ending_at.to_time) + expect(json_response['description']).to eq(params[:description]) + expect(json_response['dashboard_path']).to eq(dashboard) end + end - context 'without correct permissions' do - let_it_be(:guest) { create(:user) } - - before do - project.add_guest(guest) - end - - it 'returns error messsage' do - post api(url, guest), params: params + context 'with invalid parameters' do + it 'returns error messsage' do + post api(url, user), params: { dashboard_path: nil, starting_at: nil, description: nil } - expect(response).to have_gitlab_http_status(:forbidden) - end + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to include({ "starting_at" => ["can't be blank"], "description" => ["can't be blank"], "dashboard_path" => ["can't be blank"] }) end end - context 'is off' do + context 'with undeclared params' do before do - stub_feature_flags(metrics_dashboard_annotations: { enabled: false }) + params[:undeclared_param] = 'xyz' end - it 'returns error messsage' do - post api(url, user), params: params + it 'filters out undeclared params' do + expect(::Metrics::Dashboard::Annotations::CreateService).to receive(:new).with(user, hash_excluding(:undeclared_param)) - expect(response).to have_gitlab_http_status(:not_found) + post api(url, user), params: params end end end + + context 'without correct permissions' do + let_it_be(:guest) { create(:user) } + + before do + project.add_guest(guest) + end + + it 'returns error message' do + post api(url, guest), params: params + + expect(response).to have_gitlab_http_status(:forbidden) + end + end end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index f776faf6458..0deff138e2e 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1806,7 +1806,7 @@ describe API::Projects do first_user = json_response.first expect(first_user['username']).to eq(user.username) expect(first_user['name']).to eq(user.name) - expect(first_user.keys).to contain_exactly(*%w[name username id state avatar_url web_url]) + expect(first_user.keys).to include(*%w[name username id state avatar_url web_url]) end end diff --git a/spec/serializers/diff_file_base_entity_spec.rb b/spec/serializers/diff_file_base_entity_spec.rb index 80f5bc8f159..1fd697970de 100644 --- a/spec/serializers/diff_file_base_entity_spec.rb +++ b/spec/serializers/diff_file_base_entity_spec.rb @@ -34,4 +34,62 @@ describe DiffFileBaseEntity do expect(entity[:new_size]).to eq(132) end end + + context 'edit_path' do + let(:diff_file) { merge_request.diffs.diff_files.to_a.last } + let(:options) { { request: EntityRequest.new(current_user: create(:user)), merge_request: merge_request } } + let(:params) { {} } + + before do + stub_feature_flags(web_ide_default: false) + end + + shared_examples 'a diff file edit path to the source branch' do + it do + expect(entity[:edit_path]).to eq(Gitlab::Routing.url_helpers.project_edit_blob_path(project, File.join(merge_request.source_branch, diff_file.new_path), params)) + end + end + + context 'open' do + let(:merge_request) { create(:merge_request, source_project: project, target_branch: 'master', source_branch: 'feature') } + let(:params) { { from_merge_request_iid: merge_request.iid } } + + it_behaves_like 'a diff file edit path to the source branch' + + context 'removed source branch' do + before do + allow(merge_request).to receive(:source_branch_exists?).and_return(false) + end + + it do + expect(entity[:edit_path]).to eq(nil) + end + end + end + + context 'closed' do + let(:merge_request) { create(:merge_request, source_project: project, state: :closed, target_branch: 'master', source_branch: 'feature') } + let(:params) { { from_merge_request_iid: merge_request.iid } } + + it_behaves_like 'a diff file edit path to the source branch' + + context 'removed source branch' do + before do + allow(merge_request).to receive(:source_branch_exists?).and_return(false) + end + + it do + expect(entity[:edit_path]).to eq(nil) + end + end + end + + context 'merged' do + let(:merge_request) { create(:merge_request, source_project: project, state: :merged) } + + it do + expect(entity[:edit_path]).to eq(Gitlab::Routing.url_helpers.project_edit_blob_path(project, File.join(merge_request.target_branch, diff_file.new_path), {})) + end + end + end end diff --git a/spec/serializers/diffs_metadata_entity_spec.rb b/spec/serializers/diffs_metadata_entity_spec.rb index a6bf9a7700e..3ed2b7c9452 100644 --- a/spec/serializers/diffs_metadata_entity_spec.rb +++ b/spec/serializers/diffs_metadata_entity_spec.rb @@ -29,7 +29,7 @@ describe DiffsMetadataEntity do :added_lines, :removed_lines, :render_overflow_warning, :email_patch_path, :plain_diff_path, :merge_request_diffs, :context_commits, - :definition_path_prefix, + :definition_path_prefix, :source_branch_exists, # Attributes :diff_files ) diff --git a/spec/serializers/merge_request_sidebar_basic_entity_spec.rb b/spec/serializers/merge_request_sidebar_basic_entity_spec.rb index b364b1a3306..b2db57801ea 100644 --- a/spec/serializers/merge_request_sidebar_basic_entity_spec.rb +++ b/spec/serializers/merge_request_sidebar_basic_entity_spec.rb @@ -13,7 +13,7 @@ describe MergeRequestSidebarBasicEntity do describe '#current_user' do it 'contains attributes related to the current user' do - expect(entity[:current_user].keys).to contain_exactly( + expect(entity[:current_user].keys).to include( :id, :name, :username, :state, :avatar_url, :web_url, :todo, :can_edit, :can_move, :can_admin_label, :can_merge ) diff --git a/spec/services/alert_management/update_alert_status_service_spec.rb b/spec/services/alert_management/update_alert_status_service_spec.rb index 44083128453..b287d0d1614 100644 --- a/spec/services/alert_management/update_alert_status_service_spec.rb +++ b/spec/services/alert_management/update_alert_status_service_spec.rb @@ -3,27 +3,64 @@ require 'spec_helper' describe AlertManagement::UpdateAlertStatusService do - let_it_be(:alert) { create(:alert_management_alert, status: 'triggered') } + let(:project) { alert.project } + let_it_be(:user) { build(:user) } + + let_it_be(:alert, reload: true) do + create(:alert_management_alert, :triggered) + end + + let(:service) { described_class.new(alert, user, new_status) } describe '#execute' do - subject(:execute) { described_class.new(alert, new_status).execute } + shared_examples 'update failure' do |error_message| + it 'returns an error' do + expect(response).to be_error + expect(response.message).to eq(error_message) + expect(response.payload[:alert]).to eq(alert) + end + + it 'does not update the status' do + expect { response }.not_to change { alert.status } + end + end let(:new_status) { Types::AlertManagement::StatusEnum.values['ACKNOWLEDGED'].value } + let(:can_update) { true } + + subject(:response) { service.execute } + + before do + allow(user).to receive(:can?) + .with(:update_alert_management_alert, project) + .and_return(can_update) + end + + it 'returns success' do + expect(response).to be_success + expect(response.payload[:alert]).to eq(alert) + end it 'updates the status' do - expect { execute }.to change { alert.acknowledged? }.to(true) + expect { response }.to change { alert.acknowledged? }.to(true) end - context 'with unknown status' do - let(:new_status) { 'random_status' } + context 'when user has no permissions' do + let(:can_update) { false } - it 'returns an error' do - expect(execute.status).to eq(:error) - end + include_examples 'update failure', _('You have no permissions') + end - it 'does not update the status' do - expect { execute }.not_to change { alert.status } - end + context 'with no status' do + let(:new_status) { nil } + + include_examples 'update failure', _('Invalid status') + end + + context 'with unknown status' do + let(:new_status) { -1 } + + include_examples 'update failure', _('Invalid status') end end end diff --git a/spec/services/authorized_project_update/project_create_service_spec.rb b/spec/services/authorized_project_update/project_create_service_spec.rb new file mode 100644 index 00000000000..49ea538d909 --- /dev/null +++ b/spec/services/authorized_project_update/project_create_service_spec.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe AuthorizedProjectUpdate::ProjectCreateService do + let_it_be(:group_parent) { create(:group, :private) } + let_it_be(:group) { create(:group, :private, parent: group_parent) } + let_it_be(:group_child) { create(:group, :private, parent: group) } + + let_it_be(:group_project) { create(:project, group: group) } + + let_it_be(:parent_group_user) { create(:user) } + let_it_be(:group_user) { create(:user) } + let_it_be(:child_group_user) { create(:user) } + + let(:access_level) { Gitlab::Access::MAINTAINER } + + subject(:service) { described_class.new(group_project) } + + describe '#perform' do + context 'direct group members' do + before do + create(:group_member, access_level: access_level, group: group, user: group_user) + ProjectAuthorization.delete_all + end + + it 'creates project authorization' do + expect { service.execute }.to( + change { ProjectAuthorization.count }.from(0).to(1)) + + project_authorization = ProjectAuthorization.where( + project_id: group_project.id, + user_id: group_user.id, + access_level: access_level) + + expect(project_authorization).to exist + end + end + + context 'inherited group members' do + before do + create(:group_member, access_level: access_level, group: group_parent, user: parent_group_user) + ProjectAuthorization.delete_all + end + + it 'creates project authorization' do + expect { service.execute }.to( + change { ProjectAuthorization.count }.from(0).to(1)) + + project_authorization = ProjectAuthorization.where( + project_id: group_project.id, + user_id: parent_group_user.id, + access_level: access_level) + expect(project_authorization).to exist + end + end + + context 'membership overrides' do + before do + create(:group_member, access_level: Gitlab::Access::REPORTER, group: group_parent, user: group_user) + create(:group_member, access_level: Gitlab::Access::DEVELOPER, group: group, user: group_user) + ProjectAuthorization.delete_all + end + + it 'creates project authorization' do + expect { service.execute }.to( + change { ProjectAuthorization.count }.from(0).to(1)) + + project_authorization = ProjectAuthorization.where( + project_id: group_project.id, + user_id: group_user.id, + access_level: Gitlab::Access::DEVELOPER) + expect(project_authorization).to exist + end + end + + context 'no group member' do + it 'does not create project authorization' do + expect { service.execute }.not_to( + change { ProjectAuthorization.count }.from(0)) + end + end + + context 'unapproved access requests' do + before do + create(:group_member, :guest, :access_request, user: group_user, group: group) + end + + it 'does not create project authorization' do + expect { service.execute }.not_to( + change { ProjectAuthorization.count }.from(0)) + end + end + + context 'project has more user than BATCH_SIZE' do + let(:batch_size) { 2 } + let(:users) { create_list(:user, batch_size + 1 ) } + + before do + stub_const("#{described_class.name}::BATCH_SIZE", batch_size) + + users.each do |user| + create(:group_member, access_level: access_level, group: group_parent, user: user) + end + + ProjectAuthorization.delete_all + end + + it 'bulk creates project authorizations in batches' do + users.each_slice(batch_size) do |batch| + attributes = batch.map do |user| + { user_id: user.id, project_id: group_project.id, access_level: access_level } + end + + expect(ProjectAuthorization).to( + receive(:insert_all).with(array_including(attributes)).and_call_original) + end + + expect { service.execute }.to( + change { ProjectAuthorization.count }.from(0).to(batch_size + 1)) + end + end + + context 'ignores existing project authorizations' do + before do + # ProjectAuthorizations is also created because of an after_commit + # callback on Member model + create(:group_member, access_level: access_level, group: group, user: group_user) + end + + it 'does not create project authorization' do + project_authorization = ProjectAuthorization.where( + project_id: group_project.id, + user_id: group_user.id, + access_level: access_level) + + expect { service.execute }.not_to( + change { project_authorization.reload.exists? }.from(true)) + end + end + end +end diff --git a/spec/workers/authorized_project_update/project_create_worker_spec.rb b/spec/workers/authorized_project_update/project_create_worker_spec.rb new file mode 100644 index 00000000000..5ebfb60bc79 --- /dev/null +++ b/spec/workers/authorized_project_update/project_create_worker_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe AuthorizedProjectUpdate::ProjectCreateWorker do + let_it_be(:group) { create(:group, :private) } + let_it_be(:group_project) { create(:project, group: group) } + let_it_be(:group_user) { create(:user) } + + let(:access_level) { Gitlab::Access::MAINTAINER } + + subject(:worker) { described_class.new } + + it 'calls AuthorizedProjectUpdate::ProjectCreateService' do + expect_next_instance_of(AuthorizedProjectUpdate::ProjectCreateService) do |service| + expect(service).to(receive(:execute)) + end + + worker.perform(group_project.id) + end + + it 'returns ServiceResponse.success' do + result = worker.perform(group_project.id) + + expect(result.success?).to be_truthy + end + + context 'idempotence' do + before do + create(:group_member, access_level: Gitlab::Access::MAINTAINER, group: group, user: group_user) + ProjectAuthorization.delete_all + end + + include_examples 'an idempotent worker' do + let(:job_args) { group_project.id } + + it 'creates project authorization' do + subject + + project_authorization = ProjectAuthorization.where( + project_id: group_project.id, + user_id: group_user.id, + access_level: access_level) + + expect(project_authorization).to exist + expect(ProjectAuthorization.count).to eq(1) + end + end + end +end |