diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-08 15:09:24 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-08 15:09:24 +0000 |
commit | 833d57e60da633435d845a7867e46e6092c46520 (patch) | |
tree | 35676d3a0da36ef28b67cadb06af474b6c8f5b85 | |
parent | c52b72f5772d52e9fc85bd9f4e8b8497a6278c37 (diff) | |
download | gitlab-ce-833d57e60da633435d845a7867e46e6092c46520.tar.gz |
Add latest changes from gitlab-org/gitlab@master
97 files changed, 1043 insertions, 1152 deletions
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml index 72ef88cfeee..9cce7ffac84 100644 --- a/.gitlab/ci/frontend.gitlab-ci.yml +++ b/.gitlab/ci/frontend.gitlab-ci.yml @@ -222,6 +222,7 @@ coverage-frontend: - run_timed_command "retry yarn install --frozen-lockfile" script: - run_timed_command "yarn node scripts/frontend/merge_coverage_frontend.js" + coverage: '/^Statements\s*:\s*?(\d+(?:\.\d+)?)%/' artifacts: name: coverage-frontend expire_in: 31d @@ -164,6 +164,8 @@ gem 'diff_match_patch', '~> 0.1.0' # Application server gem 'rack', '~> 2.0.9' +# https://github.com/sharpstone/rack-timeout/blob/master/README.md#rails-apps-manually +gem 'rack-timeout', '~> 0.5.1', require: 'rack/timeout/base' group :unicorn do gem 'unicorn', '~> 5.5' @@ -173,7 +175,6 @@ end group :puma do gem 'gitlab-puma', '~> 4.3.3.gitlab.2', require: false gem 'gitlab-puma_worker_killer', '~> 0.1.1.gitlab.1', require: false - gem 'rack-timeout', require: false end # State machine diff --git a/Gemfile.lock b/Gemfile.lock index 70f2dfafe3a..f68fd455352 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -817,7 +817,7 @@ GEM rack rack-test (1.1.0) rack (>= 1.0, < 3) - rack-timeout (0.5.1) + rack-timeout (0.5.2) rails (6.0.3.1) actioncable (= 6.0.3.1) actionmailbox (= 6.0.3.1) @@ -1350,7 +1350,7 @@ DEPENDENCIES rack-cors (~> 1.0.6) rack-oauth2 (~> 1.9.3) rack-proxy (~> 0.6.0) - rack-timeout + rack-timeout (~> 0.5.1) rails (~> 6.0.3.1) rails-controller-testing rails-i18n (~> 6.0) diff --git a/app/assets/javascripts/alert_management/components/system_notes/system_note.vue b/app/assets/javascripts/alert_management/components/system_notes/system_note.vue index 9042d51aecf..39717ab609f 100644 --- a/app/assets/javascripts/alert_management/components/system_notes/system_note.vue +++ b/app/assets/javascripts/alert_management/components/system_notes/system_note.vue @@ -24,7 +24,7 @@ export default { return { ...author, id: id?.split('/').pop() }; }, iconHtml() { - return spriteIcon('user'); + return spriteIcon(this.note?.systemNoteIconName); }, }, }; diff --git a/app/assets/javascripts/alert_management/graphql/fragments/alert_note.fragment.graphql b/app/assets/javascripts/alert_management/graphql/fragments/alert_note.fragment.graphql index c72300e9757..74b425717a0 100644 --- a/app/assets/javascripts/alert_management/graphql/fragments/alert_note.fragment.graphql +++ b/app/assets/javascripts/alert_management/graphql/fragments/alert_note.fragment.graphql @@ -1,16 +1,17 @@ #import "~/graphql_shared/fragments/author.fragment.graphql" fragment AlertNote on Note { + id + author { id - author { - id - state - ...Author - } - body - bodyHtml - createdAt - discussion { - id - } + state + ...Author + } + body + bodyHtml + createdAt + discussion { + id + } + systemNoteIconName } diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index 9269dacd582..e3dd882f3dc 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -1,3 +1,7 @@ +// The backend actually uses "hide_whitespace" while the frontend +// uses "show whitspace" so these values are opposite what you might expect +export const NO_SHOW_WHITESPACE = '1'; +export const SHOW_WHITESPACE = '0'; export const INLINE_DIFF_VIEW_TYPE = 'inline'; export const PARALLEL_DIFF_VIEW_TYPE = 'parallel'; export const MATCH_LINE_TYPE = 'match'; @@ -20,6 +24,7 @@ export const LINE_SIDE_LEFT = 'left-side'; export const LINE_SIDE_RIGHT = 'right-side'; export const DIFF_VIEW_COOKIE_NAME = 'diff_view'; +export const DIFF_WHITESPACE_COOKIE_NAME = 'diff_whitespace'; export const LINE_HOVER_CLASS_NAME = 'is-over'; export const LINE_UNFOLD_CLASS_NAME = 'unfold js-unfold'; export const CONTEXT_LINE_CLASS_NAME = 'diff-expanded'; @@ -35,7 +40,6 @@ export const MR_TREE_SHOW_KEY = 'mr_tree_show'; export const TREE_TYPE = 'tree'; export const TREE_LIST_STORAGE_KEY = 'mr_diff_tree_list'; -export const WHITESPACE_STORAGE_KEY = 'mr_show_whitespace'; export const TREE_LIST_WIDTH_STORAGE_KEY = 'mr_tree_list_width'; export const INITIAL_TREE_WIDTH = 320; diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js index ce48e36bfd7..028f04095a9 100644 --- a/app/assets/javascripts/diffs/index.js +++ b/app/assets/javascripts/diffs/index.js @@ -1,11 +1,11 @@ import Vue from 'vue'; import { mapActions, mapState, mapGetters } from 'vuex'; import { parseBoolean } from '~/lib/utils/common_utils'; -import { getParameterValues } from '~/lib/utils/url_utility'; import FindFile from '~/vue_shared/components/file_finder/index.vue'; import eventHub from '../notes/event_hub'; import diffsApp from './components/app.vue'; -import { TREE_LIST_STORAGE_KEY } from './constants'; +import { TREE_LIST_STORAGE_KEY, DIFF_WHITESPACE_COOKIE_NAME } from './constants'; +import Cookies from 'js-cookie'; export default function initDiffsApp(store) { const fileFinderEl = document.getElementById('js-diff-file-finder'); @@ -86,15 +86,16 @@ export default function initDiffsApp(store) { }), }, created() { - let hideWhitespace = getParameterValues('w')[0]; const treeListStored = localStorage.getItem(TREE_LIST_STORAGE_KEY); const renderTreeList = treeListStored !== null ? parseBoolean(treeListStored) : true; this.setRenderTreeList(renderTreeList); - if (!hideWhitespace) { - hideWhitespace = this.showWhitespaceDefault ? '0' : '1'; + + // Set whitespace default as per user preferences unless cookie is already set + if (!Cookies.get(DIFF_WHITESPACE_COOKIE_NAME)) { + const hideWhitespace = this.showWhitespaceDefault ? '0' : '1'; + this.setShowWhitespace({ showWhitespace: hideWhitespace !== '1' }); } - this.setShowWhitespace({ showWhitespace: hideWhitespace !== '1' }); }, methods: { ...mapActions('diffs', ['setRenderTreeList', 'setShowWhitespace']), @@ -114,7 +115,6 @@ export default function initDiffsApp(store) { isFluidLayout: this.isFluidLayout, dismissEndpoint: this.dismissEndpoint, showSuggestPopover: this.showSuggestPopover, - showWhitespaceDefault: this.showWhitespaceDefault, }, }); }, diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index a8d348e1836..6f903bd09f0 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -25,7 +25,6 @@ import { DIFF_VIEW_COOKIE_NAME, MR_TREE_SHOW_KEY, TREE_LIST_STORAGE_KEY, - WHITESPACE_STORAGE_KEY, TREE_LIST_WIDTH_STORAGE_KEY, OLD_LINE_KEY, NEW_LINE_KEY, @@ -38,6 +37,9 @@ import { INLINE_DIFF_LINES_KEY, PARALLEL_DIFF_LINES_KEY, DIFFS_PER_PAGE, + DIFF_WHITESPACE_COOKIE_NAME, + SHOW_WHITESPACE, + NO_SHOW_WHITESPACE, } from '../constants'; import { diffViewerModes } from '~/ide/constants'; @@ -484,11 +486,12 @@ export const setRenderTreeList = ({ commit }, renderTreeList) => { export const setShowWhitespace = ({ commit }, { showWhitespace, pushState = false }) => { commit(types.SET_SHOW_WHITESPACE, showWhitespace); + const w = showWhitespace ? SHOW_WHITESPACE : NO_SHOW_WHITESPACE; - localStorage.setItem(WHITESPACE_STORAGE_KEY, showWhitespace); + Cookies.set(DIFF_WHITESPACE_COOKIE_NAME, w); if (pushState) { - historyPushState(mergeUrlParams({ w: showWhitespace ? '0' : '1' }, window.location.href)); + historyPushState(mergeUrlParams({ w }, window.location.href)); } eventHub.$emit('refetchDiffData'); diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js index 87938ababed..1f165dd4971 100644 --- a/app/assets/javascripts/diffs/store/modules/diff_state.js +++ b/app/assets/javascripts/diffs/store/modules/diff_state.js @@ -1,10 +1,17 @@ import Cookies from 'js-cookie'; import { getParameterValues } from '~/lib/utils/url_utility'; -import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME } from '../../constants'; +import { + INLINE_DIFF_VIEW_TYPE, + DIFF_VIEW_COOKIE_NAME, + DIFF_WHITESPACE_COOKIE_NAME, +} from '../../constants'; +import { getDefaultWhitespace } from '../utils'; const viewTypeFromQueryString = getParameterValues('view')[0]; const viewTypeFromCookie = Cookies.get(DIFF_VIEW_COOKIE_NAME); const defaultViewType = INLINE_DIFF_VIEW_TYPE; +const whiteSpaceFromQueryString = getParameterValues('w')[0]; +const whiteSpaceFromCookie = Cookies.get(DIFF_WHITESPACE_COOKIE_NAME); export default () => ({ isLoading: true, @@ -29,7 +36,7 @@ export default () => ({ commentForms: [], highlightedRow: null, renderTreeList: true, - showWhitespace: true, + showWhitespace: getDefaultWhitespace(whiteSpaceFromQueryString, whiteSpaceFromCookie), fileFinderVisible: false, dismissEndpoint: '', showSuggestPopover: true, diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index d261be1b550..bc85dd0a1d4 100644 --- a/app/assets/javascripts/diffs/store/utils.js +++ b/app/assets/javascripts/diffs/store/utils.js @@ -15,6 +15,8 @@ import { TREE_TYPE, INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE, + SHOW_WHITESPACE, + NO_SHOW_WHITESPACE, } from '../constants'; export function findDiffFile(files, match, matchKey = 'file_hash') { @@ -701,3 +703,10 @@ export const allDiscussionWrappersExpanded = diff => { return discussionsExpanded; }; + +export const getDefaultWhitespace = (queryString, cookie) => { + // Querystring should override stored cookie value + if (queryString) return queryString === SHOW_WHITESPACE; + if (cookie === NO_SHOW_WHITESPACE) return false; + return true; +}; diff --git a/app/assets/javascripts/monitoring/components/create_dashboard_modal.vue b/app/assets/javascripts/monitoring/components/create_dashboard_modal.vue new file mode 100644 index 00000000000..74799002b17 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/create_dashboard_modal.vue @@ -0,0 +1,66 @@ +<script> +import { GlButton, GlModal, GlSprintf } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import { isSafeURL } from '~/lib/utils/url_utility'; + +export default { + components: { GlButton, GlModal, GlSprintf }, + props: { + modalId: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: true, + validator: isSafeURL, + }, + addDashboardDocumentationPath: { + type: String, + required: true, + }, + }, + methods: { + cancelHandler() { + this.$refs.modal.hide(); + }, + }, + i18n: { + titleText: s__('Metrics|Create your dashboard configuration file'), + mainText: s__( + 'Metrics|To create a new dashboard, add a new YAML file to %{codeStart}.gitlab/dashboards%{codeEnd} at the root of this project.', + ), + }, +}; +</script> + +<template> + <gl-modal ref="modal" :modal-id="modalId" :title="$options.i18n.titleText"> + <p> + <gl-sprintf :message="$options.i18n.mainText"> + <template #code="{ content }"> + <code>{{ content }}</code> + </template> + </gl-sprintf> + </p> + <template #modal-footer> + <gl-button category="secondary" @click="cancelHandler">{{ s__('Metrics|Cancel') }}</gl-button> + <gl-button + category="secondary" + variant="info" + target="_blank" + :href="addDashboardDocumentationPath" + data-testid="create-dashboard-modal-docs-button" + > + {{ s__('Metrics|View documentation') }} + </gl-button> + <gl-button + variant="success" + data-testid="create-dashboard-modal-repo-button" + :href="projectPath" + > + {{ s__('Metrics|Open repository') }} + </gl-button> + </template> + </gl-modal> +</template> diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 28c588de468..f685d67751c 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -72,6 +72,10 @@ export default { type: String, required: true, }, + addDashboardDocumentationPath: { + type: String, + required: true, + }, settingsPath: { type: String, required: true, @@ -395,6 +399,7 @@ export default { v-if="showHeader" ref="prometheusGraphsHeader" class="prometheus-graphs-header d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 border-bottom bg-gray-light" + :add-dashboard-documentation-path="addDashboardDocumentationPath" :default-branch="defaultBranch" :rearrange-panels-available="rearrangePanelsAvailable" :custom-metrics-available="customMetricsAvailable" diff --git a/app/assets/javascripts/monitoring/components/dashboard_header.vue b/app/assets/javascripts/monitoring/components/dashboard_header.vue index b47adc9db11..4c973b6bb67 100644 --- a/app/assets/javascripts/monitoring/components/dashboard_header.vue +++ b/app/assets/javascripts/monitoring/components/dashboard_header.vue @@ -8,6 +8,9 @@ import { GlDropdownItem, GlDropdownHeader, GlDropdownDivider, + GlNewDropdown, + GlNewDropdownDivider, + GlNewDropdownItem, GlModal, GlLoadingIcon, GlSearchBoxByType, @@ -23,6 +26,8 @@ import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_p import DashboardsDropdown from './dashboards_dropdown.vue'; import RefreshButton from './refresh_button.vue'; +import CreateDashboardModal from './create_dashboard_modal.vue'; +import DuplicateDashboardModal from './duplicate_dashboard_modal.vue'; import TrackEventDirective from '~/vue_shared/directives/track_event'; import { getAddMetricTrackingOptions, timeRangeToUrl } from '../utils'; @@ -39,6 +44,9 @@ export default { GlDropdownItem, GlDropdownHeader, GlDropdownDivider, + GlNewDropdown, + GlNewDropdownDivider, + GlNewDropdownItem, GlSearchBoxByType, GlModal, CustomMetricsFormFields, @@ -46,6 +54,8 @@ export default { DateTimePicker, DashboardsDropdown, RefreshButton, + DuplicateDashboardModal, + CreateDashboardModal, }, directives: { GlModal: GlModalDirective, @@ -95,6 +105,10 @@ export default { type: Object, required: true, }, + addDashboardDocumentationPath: { + type: String, + required: true, + }, }, data() { return { @@ -108,8 +122,12 @@ export default { 'isUpdatingStarredValue', 'showEmptyState', 'dashboardTimezone', + 'projectPath', ]), ...mapGetters('monitoringDashboard', ['selectedDashboard', 'filteredEnvironments']), + isSystemDashboard() { + return this.selectedDashboard?.system_dashboard; + }, shouldShowEnvironmentsDropdownNoMatchedMsg() { return !this.environmentsLoading && this.filteredEnvironments.length === 0; }, @@ -129,6 +147,9 @@ export default { displayUtc() { return this.dashboardTimezone === timezones.UTC; }, + shouldShowActionsMenu() { + return Boolean(this.projectPath); + }, }, methods: { ...mapActions('monitoringDashboard', ['filterEnvironments', 'toggleStarredValue']), @@ -136,6 +157,7 @@ export default { const params = { dashboard: encodeURIComponent(dashboard.path), }; + redirectTo(mergeUrlParams(params, window.location.href)); }, debouncedEnvironmentsSearch: debounce(function environmentsSearchOnInput(searchTerm) { @@ -162,13 +184,15 @@ export default { this.$refs.customMetricsForm.submit(); }, }, - addMetric: { - title: s__('Metrics|Add metric'), - modalId: 'add-metric', + modalIds: { + addMetric: 'addMetric', + createDashboard: 'createDashboard', + duplicateDashboard: 'duplicateDashboard', }, i18n: { starDashboard: s__('Metrics|Star dashboard'), unstarDashboard: s__('Metrics|Unstar dashboard'), + addMetric: s__('Metrics|Add metric'), }, timeRanges, }; @@ -176,17 +200,20 @@ export default { <template> <div ref="prometheusGraphsHeader"> - <div class="mb-2 pr-2 d-flex d-sm-block"> + <div class="mb-2 mr-2 d-flex d-sm-block"> <dashboards-dropdown id="monitor-dashboards-dropdown" data-qa-selector="dashboards_filter_dropdown" class="flex-grow-1" toggle-class="dropdown-menu-toggle" :default-branch="defaultBranch" + :modal-id="$options.modalIds.duplicateDashboard" @selectDashboard="selectDashboard" /> </div> + <span aria-hidden="true" class="gl-pl-3 border-left gl-mb-3 d-none d-sm-block"></span> + <div class="mb-2 pr-2 d-flex d-sm-block"> <gl-dropdown id="monitor-environments-dropdown" @@ -290,17 +317,17 @@ export default { <div v-if="addingMetricsAvailable" class="mb-2 mr-2 d-flex d-sm-block"> <gl-deprecated-button ref="addMetricBtn" - v-gl-modal="$options.addMetric.modalId" + v-gl-modal="$options.modalIds.addMetric" variant="outline-success" data-qa-selector="add_metric_button" class="flex-grow-1" > - {{ $options.addMetric.title }} + {{ $options.i18n.addMetric }} </gl-deprecated-button> <gl-modal ref="addMetricModal" - :modal-id="$options.addMetric.modalId" - :title="$options.addMetric.title" + :modal-id="$options.modalIds.addMetric" + :title="$options.i18n.addMetric" > <form ref="customMetricsForm" :action="customMetricsPath" method="post"> <custom-metrics-form-fields @@ -353,6 +380,50 @@ export default { {{ __('View full dashboard') }} <icon name="external-link" /> </gl-deprecated-button> </div> + + <template v-if="shouldShowActionsMenu"> + <span aria-hidden="true" class="gl-pl-3 border-left gl-mb-3 d-none d-sm-block"></span> + + <div class="gl-mb-3 gl-mr-3 d-flex d-sm-block"> + <gl-new-dropdown + v-gl-tooltip + right + class="gl-flex-grow-1" + data-testid="actions-menu" + :title="s__('Metrics|Create dashboard')" + :icon="'plus-square'" + > + <gl-new-dropdown-item + v-gl-modal="$options.modalIds.createDashboard" + data-testid="action-create-dashboard" + >{{ s__('Metrics|Create new dashboard') }}</gl-new-dropdown-item + > + + <create-dashboard-modal + data-testid="create-dashboard-modal" + :add-dashboard-documentation-path="addDashboardDocumentationPath" + :modal-id="$options.modalIds.createDashboard" + :project-path="projectPath" + /> + + <template v-if="isSystemDashboard"> + <gl-new-dropdown-divider /> + <gl-new-dropdown-item + ref="duplicateDashboardItem" + v-gl-modal="$options.modalIds.duplicateDashboard" + data-testid="action-duplicate-dashboard" + > + {{ s__('Metrics|Duplicate current dashboard') }} + </gl-new-dropdown-item> + </template> + </gl-new-dropdown> + </div> + </template> </div> + <duplicate-dashboard-modal + :default-branch="defaultBranch" + :modal-id="$options.modalIds.duplicateDashboard" + @dashboardDuplicated="selectDashboard" + /> </div> </template> diff --git a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue index 8b86890715f..0e96d57b8a1 100644 --- a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue +++ b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue @@ -1,19 +1,14 @@ <script> import { mapState, mapActions, mapGetters } from 'vuex'; import { - GlAlert, GlIcon, GlDropdown, GlDropdownItem, GlDropdownHeader, GlDropdownDivider, GlSearchBoxByType, - GlModal, - GlLoadingIcon, GlModalDirective, } from '@gitlab/ui'; -import { s__ } from '~/locale'; -import DuplicateDashboardForm from './duplicate_dashboard_form.vue'; const events = { selectDashboard: 'selectDashboard', @@ -21,16 +16,12 @@ const events = { export default { components: { - GlAlert, GlIcon, GlDropdown, GlDropdownItem, GlDropdownHeader, GlDropdownDivider, GlSearchBoxByType, - GlModal, - GlLoadingIcon, - DuplicateDashboardForm, }, directives: { GlModal: GlModalDirective, @@ -40,12 +31,13 @@ export default { type: String, required: true, }, + modalId: { + type: String, + required: true, + }, }, data() { return { - alert: null, - loading: false, - form: {}, searchTerm: '', }; }, @@ -76,10 +68,6 @@ export default { nonStarredDashboards() { return this.filteredDashboards.filter(({ starred }) => !starred); }, - - okButtonText() { - return this.loading ? s__('Metrics|Duplicating...') : s__('Metrics|Duplicate'); - }, }, methods: { ...mapActions('monitoringDashboard', ['duplicateSystemDashboard']), @@ -89,37 +77,6 @@ export default { selectDashboard(dashboard) { this.$emit(events.selectDashboard, dashboard); }, - ok(bvModalEvt) { - // Prevent modal from hiding in case submit fails - bvModalEvt.preventDefault(); - - this.loading = true; - this.alert = null; - this.duplicateSystemDashboard(this.form) - .then(createdDashboard => { - this.loading = false; - this.alert = null; - - // Trigger hide modal as submit is successful - this.$refs.duplicateDashboardModal.hide(); - - // Dashboards in the default branch become available immediately. - // Not so in other branches, so we refresh the current dashboard - const dashboard = - this.form.branch === this.defaultBranch ? createdDashboard : this.selectedDashboard; - this.$emit(events.selectDashboard, dashboard); - }) - .catch(error => { - this.loading = false; - this.alert = error; - }); - }, - hide() { - this.alert = null; - }, - formChange(form) { - this.form = form; - }, }, }; </script> @@ -178,32 +135,14 @@ export default { {{ __('No matching results') }} </div> + <!-- + This Duplicate Dashboard item will be removed from the dashboards dropdown + in https://gitlab.com/gitlab-org/gitlab/-/issues/223223 + --> <template v-if="isSystemDashboard"> <gl-dropdown-divider /> - <gl-modal - ref="duplicateDashboardModal" - modal-id="duplicateDashboardModal" - :title="s__('Metrics|Duplicate dashboard')" - ok-variant="success" - @ok="ok" - @hide="hide" - > - <gl-alert v-if="alert" class="mb-3" variant="danger" @dismiss="alert = null"> - {{ alert }} - </gl-alert> - <duplicate-dashboard-form - :dashboard="selectedDashboard" - :default-branch="defaultBranch" - @change="formChange" - /> - <template #modal-ok> - <gl-loading-icon v-if="loading" inline color="light" /> - {{ okButtonText }} - </template> - </gl-modal> - - <gl-dropdown-item ref="duplicateDashboardItem" v-gl-modal="'duplicateDashboardModal'"> + <gl-dropdown-item v-gl-modal="modalId" data-testid="duplicateDashboardItem"> {{ s__('Metrics|Duplicate dashboard') }} </gl-dropdown-item> </template> diff --git a/app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue b/app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue new file mode 100644 index 00000000000..e64afc01fd9 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue @@ -0,0 +1,95 @@ +<script> +import { mapActions, mapGetters } from 'vuex'; +import { GlAlert, GlLoadingIcon, GlModal } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import DuplicateDashboardForm from './duplicate_dashboard_form.vue'; + +const events = { + dashboardDuplicated: 'dashboardDuplicated', +}; + +export default { + components: { GlAlert, GlLoadingIcon, GlModal, DuplicateDashboardForm }, + props: { + defaultBranch: { + type: String, + required: true, + }, + modalId: { + type: String, + required: true, + }, + }, + data() { + return { + alert: null, + loading: false, + form: {}, + }; + }, + computed: { + ...mapGetters('monitoringDashboard', ['selectedDashboard']), + okButtonText() { + return this.loading ? s__('Metrics|Duplicating...') : s__('Metrics|Duplicate'); + }, + }, + methods: { + ...mapActions('monitoringDashboard', ['duplicateSystemDashboard']), + ok(bvModalEvt) { + // Prevent modal from hiding in case submit fails + bvModalEvt.preventDefault(); + + this.loading = true; + this.alert = null; + this.duplicateSystemDashboard(this.form) + .then(createdDashboard => { + this.loading = false; + this.alert = null; + + // Trigger hide modal as submit is successful + this.$refs.duplicateDashboardModal.hide(); + + // Dashboards in the default branch become available immediately. + // Not so in other branches, so we refresh the current dashboard + const dashboard = + this.form.branch === this.defaultBranch ? createdDashboard : this.selectedDashboard; + this.$emit(events.dashboardDuplicated, dashboard); + }) + .catch(error => { + this.loading = false; + this.alert = error; + }); + }, + hide() { + this.alert = null; + }, + formChange(form) { + this.form = form; + }, + }, +}; +</script> + +<template> + <gl-modal + ref="duplicateDashboardModal" + :modal-id="modalId" + :title="s__('Metrics|Duplicate dashboard')" + ok-variant="success" + @ok="ok" + @hide="hide" + > + <gl-alert v-if="alert" class="mb-3" variant="danger" @dismiss="alert = null"> + {{ alert }} + </gl-alert> + <duplicate-dashboard-form + :dashboard="selectedDashboard" + :default-branch="defaultBranch" + @change="formChange" + /> + <template #modal-ok> + <gl-loading-icon v-if="loading" inline color="light" /> + {{ okButtonText }} + </template> + </gl-modal> +</template> diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue index 8a11c3dcafe..45c343c3f7f 100644 --- a/app/assets/javascripts/repository/components/breadcrumbs.vue +++ b/app/assets/javascripts/repository/components/breadcrumbs.vue @@ -4,9 +4,9 @@ import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility'; import { __ } from '../../locale'; import Icon from '../../vue_shared/components/icon.vue'; import getRefMixin from '../mixins/get_ref'; -import projectShortPathQuery from '../queries/project_short_path.query.graphql'; -import projetPathQuery from '../queries/project_path.query.graphql'; -import permissionsQuery from '../queries/permissions.query.graphql'; +import getProjectShortPath from '../queries/getProjectShortPath.query.graphql'; +import getProjectPath from '../queries/getProjectPath.query.graphql'; +import getPermissions from '../queries/getPermissions.query.graphql'; const ROW_TYPES = { header: 'header', @@ -23,13 +23,13 @@ export default { }, apollo: { projectShortPath: { - query: projectShortPathQuery, + query: getProjectShortPath, }, projectPath: { - query: projetPathQuery, + query: getProjectPath, }, userPermissions: { - query: permissionsQuery, + query: getPermissions, variables() { return { projectPath: this.projectPath, diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue index 0206a61487c..c5c99d56e2a 100644 --- a/app/assets/javascripts/repository/components/last_commit.vue +++ b/app/assets/javascripts/repository/components/last_commit.vue @@ -8,8 +8,8 @@ import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; import CiIcon from '../../vue_shared/components/ci_icon.vue'; import ClipboardButton from '../../vue_shared/components/clipboard_button.vue'; import getRefMixin from '../mixins/get_ref'; -import projectPathQuery from '../queries/project_path.query.graphql'; -import pathLastCommitQuery from '../queries/path_last_commit.query.graphql'; +import getProjectPath from '../queries/getProjectPath.query.graphql'; +import pathLastCommit from '../queries/pathLastCommit.query.graphql'; export default { components: { @@ -28,10 +28,10 @@ export default { mixins: [getRefMixin], apollo: { projectPath: { - query: projectPathQuery, + query: getProjectPath, }, commit: { - query: pathLastCommitQuery, + query: pathLastCommit, variables() { return { projectPath: this.projectPath, @@ -102,7 +102,7 @@ export default { <template v-else-if="commit"> <user-avatar-link v-if="commit.author" - :link-href="commit.author.webPath" + :link-href="commit.author.webUrl" :img-src="commit.author.avatarUrl" :img-size="40" class="avatar-cell" @@ -118,7 +118,7 @@ export default { <div class="commit-detail flex-list"> <div class="commit-content qa-commit-content"> <gl-link - :href="commit.webPath" + :href="commit.webUrl" :class="{ 'font-italic': !commit.message }" class="commit-row-message item-title" v-html="commit.titleHtml" @@ -135,7 +135,7 @@ export default { <div class="committer"> <gl-link v-if="commit.author" - :href="commit.author.webPath" + :href="commit.author.webUrl" class="commit-author-link js-user-link" > {{ commit.author.name }} diff --git a/app/assets/javascripts/repository/components/preview/index.vue b/app/assets/javascripts/repository/components/preview/index.vue index ecf98ebe7db..f96523bb497 100644 --- a/app/assets/javascripts/repository/components/preview/index.vue +++ b/app/assets/javascripts/repository/components/preview/index.vue @@ -3,15 +3,15 @@ import $ from 'jquery'; import '~/behaviors/markdown/render_gfm'; import { GlLink, GlLoadingIcon } from '@gitlab/ui'; import { handleLocationHash } from '~/lib/utils/common_utils'; -import readmeQery from '../../queries/readme.query.graphql'; +import getReadmeQuery from '../../queries/getReadme.query.graphql'; export default { apollo: { readme: { - query: readmeQery, + query: getReadmeQuery, variables() { return { - url: this.blob.webPath, + url: this.blob.webUrl, }; }, loadingKey: 'loading', @@ -51,7 +51,7 @@ export default { <div class="js-file-title file-title-flex-parent"> <div class="file-header-content"> <i aria-hidden="true" class="fa fa-file-text-o fa-fw"></i> - <gl-link :href="blob.webPath"> + <gl-link :href="blob.webUrl"> <strong>{{ blob.name }}</strong> </gl-link> </div> diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue index 4077e3f8cdc..c1f350dccd6 100644 --- a/app/assets/javascripts/repository/components/table/index.vue +++ b/app/assets/javascripts/repository/components/table/index.vue @@ -2,7 +2,7 @@ import { GlSkeletonLoading } from '@gitlab/ui'; import { sprintf, __ } from '../../../locale'; import getRefMixin from '../../mixins/get_ref'; -import projectPathQuery from '../../queries/project_path.query.graphql'; +import getProjectPath from '../../queries/getProjectPath.query.graphql'; import TableHeader from './header.vue'; import TableRow from './row.vue'; import ParentRow from './parent_row.vue'; @@ -17,7 +17,7 @@ export default { mixins: [getRefMixin], apollo: { projectPath: { - query: projectPathQuery, + query: getProjectPath, }, }, props: { @@ -96,7 +96,7 @@ export default { :name="entry.name" :path="entry.flatPath" :type="entry.type" - :url="entry.webUrl || entry.webPath" + :url="entry.webUrl" :submodule-tree-url="entry.treeUrl" :lfs-oid="entry.lfsOid" :loading-path="loadingPath" diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue index 95e34280665..d5363016335 100644 --- a/app/assets/javascripts/repository/components/table/row.vue +++ b/app/assets/javascripts/repository/components/table/row.vue @@ -12,7 +12,7 @@ import { escapeFileUrl } from '~/lib/utils/url_utility'; import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue'; import getRefMixin from '../../mixins/get_ref'; -import commitQuery from '../../queries/commit.query.graphql'; +import getCommit from '../../queries/getCommit.query.graphql'; export default { components: { @@ -29,7 +29,7 @@ export default { }, apollo: { commit: { - query: commitQuery, + query: getCommit, variables() { return { fileName: this.name, diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue index 721cc6787dc..59ba1caa8c9 100644 --- a/app/assets/javascripts/repository/components/tree_content.vue +++ b/app/assets/javascripts/repository/components/tree_content.vue @@ -3,8 +3,8 @@ import createFlash from '~/flash'; import { __ } from '../../locale'; import FileTable from './table/index.vue'; import getRefMixin from '../mixins/get_ref'; -import filesQuery from '../queries/files.query.graphql'; -import projectPathQuery from '../queries/project_path.query.graphql'; +import getFiles from '../queries/getFiles.query.graphql'; +import getProjectPath from '../queries/getProjectPath.query.graphql'; import FilePreview from './preview/index.vue'; import { readmeFile } from '../utils/readme'; @@ -18,7 +18,7 @@ export default { mixins: [getRefMixin], apollo: { projectPath: { - query: projectPathQuery, + query: getProjectPath, }, }, props: { @@ -70,7 +70,7 @@ export default { return this.$apollo .query({ - query: filesQuery, + query: getFiles, variables: { projectPath: this.projectPath, ref: this.ref, diff --git a/app/assets/javascripts/repository/log_tree.js b/app/assets/javascripts/repository/log_tree.js index 704dd88aabe..cef17bf7acb 100644 --- a/app/assets/javascripts/repository/log_tree.js +++ b/app/assets/javascripts/repository/log_tree.js @@ -1,8 +1,8 @@ import { normalizeData } from 'ee_else_ce/repository/utils/commit'; import axios from '~/lib/utils/axios_utils'; -import commitsQuery from './queries/commits.query.graphql'; -import projectPathQuery from './queries/project_path.query.graphql'; -import refQuery from './queries/ref.query.graphql'; +import getCommits from './queries/getCommits.query.graphql'; +import getProjectPath from './queries/getProjectPath.query.graphql'; +import getRef from './queries/getRef.query.graphql'; let fetchpromise; let resolvers = []; @@ -22,8 +22,8 @@ export function fetchLogsTree(client, path, offset, resolver = null) { if (fetchpromise) return fetchpromise; - const { projectPath } = client.readQuery({ query: projectPathQuery }); - const { escapedRef } = client.readQuery({ query: refQuery }); + const { projectPath } = client.readQuery({ query: getProjectPath }); + const { escapedRef } = client.readQuery({ query: getRef }); fetchpromise = axios .get( @@ -36,10 +36,10 @@ export function fetchLogsTree(client, path, offset, resolver = null) { ) .then(({ data, headers }) => { const headerLogsOffset = headers['more-logs-offset']; - const { commits } = client.readQuery({ query: commitsQuery }); + const { commits } = client.readQuery({ query: getCommits }); const newCommitData = [...commits, ...normalizeData(data, path)]; client.writeQuery({ - query: commitsQuery, + query: getCommits, data: { commits: newCommitData }, }); diff --git a/app/assets/javascripts/repository/mixins/get_ref.js b/app/assets/javascripts/repository/mixins/get_ref.js index 1f1880a48c7..99d19b77c35 100644 --- a/app/assets/javascripts/repository/mixins/get_ref.js +++ b/app/assets/javascripts/repository/mixins/get_ref.js @@ -1,9 +1,9 @@ -import refQuery from '../queries/ref.query.graphql'; +import getRef from '../queries/getRef.query.graphql'; export default { apollo: { ref: { - query: refQuery, + query: getRef, manual: true, result({ data, loading }) { if (!loading) { diff --git a/app/assets/javascripts/repository/mixins/preload.js b/app/assets/javascripts/repository/mixins/preload.js index cb1d7f3aac9..cb6c2294679 100644 --- a/app/assets/javascripts/repository/mixins/preload.js +++ b/app/assets/javascripts/repository/mixins/preload.js @@ -1,12 +1,12 @@ -import filesQuery from '../queries/files.query.graphql'; +import getFiles from '../queries/getFiles.query.graphql'; import getRefMixin from './get_ref'; -import projectPathQuery from '../queries/project_path.query.graphql'; +import getProjectPath from '../queries/getProjectPath.query.graphql'; export default { mixins: [getRefMixin], apollo: { projectPath: { - query: projectPathQuery, + query: getProjectPath, }, }, data() { @@ -21,7 +21,7 @@ export default { return this.$apollo .query({ - query: filesQuery, + query: getFiles, variables: { projectPath: this.projectPath, ref: this.ref, diff --git a/app/assets/javascripts/repository/queries/commit.query.graphql b/app/assets/javascripts/repository/queries/getCommit.query.graphql index d2c94733bb9..e4aeaaff8fe 100644 --- a/app/assets/javascripts/repository/queries/commit.query.graphql +++ b/app/assets/javascripts/repository/queries/getCommit.query.graphql @@ -1,6 +1,6 @@ #import "ee_else_ce/repository/queries/commit.fragment.graphql" -query Commit($fileName: String!, $type: String!, $path: String!) { +query getCommit($fileName: String!, $type: String!, $path: String!) { commit(path: $path, fileName: $fileName, type: $type) @client { ...TreeEntryCommit } diff --git a/app/assets/javascripts/repository/queries/commits.query.graphql b/app/assets/javascripts/repository/queries/getCommits.query.graphql index 8747649f56f..0976b8f32d7 100644 --- a/app/assets/javascripts/repository/queries/commits.query.graphql +++ b/app/assets/javascripts/repository/queries/getCommits.query.graphql @@ -1,6 +1,6 @@ #import "ee_else_ce/repository/queries/commit.fragment.graphql" -query Commits { +query getCommits { commits @client { ...TreeEntryCommit } diff --git a/app/assets/javascripts/repository/queries/files.query.graphql b/app/assets/javascripts/repository/queries/getFiles.query.graphql index 10843b0450f..2aaf5066b4a 100644 --- a/app/assets/javascripts/repository/queries/files.query.graphql +++ b/app/assets/javascripts/repository/queries/getFiles.query.graphql @@ -8,7 +8,7 @@ fragment TreeEntry on Entry { type } -query Files( +query getFiles( $projectPath: ID! $path: String $ref: String! @@ -22,7 +22,7 @@ query Files( edges { node { ...TreeEntry - webPath + webUrl } } pageInfo { @@ -45,7 +45,7 @@ query Files( edges { node { ...TreeEntry - webPath + webUrl lfsOid } } diff --git a/app/assets/javascripts/repository/queries/permissions.query.graphql b/app/assets/javascripts/repository/queries/getPermissions.query.graphql index 57976ca2689..092fa44e2d0 100644 --- a/app/assets/javascripts/repository/queries/permissions.query.graphql +++ b/app/assets/javascripts/repository/queries/getPermissions.query.graphql @@ -1,4 +1,4 @@ -query Permissions($projectPath: ID!) { +query getPermissions($projectPath: ID!) { project(fullPath: $projectPath) { userPermissions { pushCode diff --git a/app/assets/javascripts/repository/queries/getProjectPath.query.graphql b/app/assets/javascripts/repository/queries/getProjectPath.query.graphql new file mode 100644 index 00000000000..74e73e07577 --- /dev/null +++ b/app/assets/javascripts/repository/queries/getProjectPath.query.graphql @@ -0,0 +1,3 @@ +query getProjectPath { + projectPath +} diff --git a/app/assets/javascripts/repository/queries/project_short_path.query.graphql b/app/assets/javascripts/repository/queries/getProjectShortPath.query.graphql index e6abe9d78cd..34eb26598c2 100644 --- a/app/assets/javascripts/repository/queries/project_short_path.query.graphql +++ b/app/assets/javascripts/repository/queries/getProjectShortPath.query.graphql @@ -1,3 +1,3 @@ -query ProjectShortPath { +query getProjectShortPath { projectShortPath @client } diff --git a/app/assets/javascripts/repository/queries/readme.query.graphql b/app/assets/javascripts/repository/queries/getReadme.query.graphql index 38ecc2e1bb2..cf056330133 100644 --- a/app/assets/javascripts/repository/queries/readme.query.graphql +++ b/app/assets/javascripts/repository/queries/getReadme.query.graphql @@ -1,4 +1,4 @@ -query Readme($url: String!) { +query getReadme($url: String!) { readme(url: $url) @client { html } diff --git a/app/assets/javascripts/repository/queries/ref.query.graphql b/app/assets/javascripts/repository/queries/getRef.query.graphql index e3498acd688..91afb751626 100644 --- a/app/assets/javascripts/repository/queries/ref.query.graphql +++ b/app/assets/javascripts/repository/queries/getRef.query.graphql @@ -1,4 +1,4 @@ -query Ref { +query getRef { ref @client escapedRef @client } diff --git a/app/assets/javascripts/repository/queries/path_last_commit.query.graphql b/app/assets/javascripts/repository/queries/pathLastCommit.query.graphql index 8dc00fd9c7b..f54f09fd647 100644 --- a/app/assets/javascripts/repository/queries/path_last_commit.query.graphql +++ b/app/assets/javascripts/repository/queries/pathLastCommit.query.graphql @@ -1,4 +1,4 @@ -query PathLastCommit($projectPath: ID!, $path: String, $ref: String!) { +query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) { project(fullPath: $projectPath) { repository { tree(path: $path, ref: $ref) { @@ -8,14 +8,14 @@ query PathLastCommit($projectPath: ID!, $path: String, $ref: String!) { titleHtml description message - webPath + webUrl authoredDate authorName authorGravatar author { name avatarUrl - webPath + webUrl } signatureHtml pipelines(ref: $ref, first: 1) { diff --git a/app/assets/javascripts/repository/queries/project_path.query.graphql b/app/assets/javascripts/repository/queries/project_path.query.graphql deleted file mode 100644 index 462ae5382a1..00000000000 --- a/app/assets/javascripts/repository/queries/project_path.query.graphql +++ /dev/null @@ -1,3 +0,0 @@ -query ProjectPath { - projectPath -} diff --git a/app/graphql/types/commit_type.rb b/app/graphql/types/commit_type.rb index b4ec1d5cb6c..be5165da545 100644 --- a/app/graphql/types/commit_type.rb +++ b/app/graphql/types/commit_type.rb @@ -23,8 +23,6 @@ module Types description: 'Timestamp of when the commit was authored' field :web_url, type: GraphQL::STRING_TYPE, null: false, description: 'Web URL of the commit' - field :web_path, type: GraphQL::STRING_TYPE, null: false, - description: 'Web path of the commit' field :signature_html, type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true, description: 'Rendered HTML of the commit signature' field :author_name, type: GraphQL::STRING_TYPE, null: true, diff --git a/app/graphql/types/notes/note_type.rb b/app/graphql/types/notes/note_type.rb index 8755b4ccad5..5d41f0032bd 100644 --- a/app/graphql/types/notes/note_type.rb +++ b/app/graphql/types/notes/note_type.rb @@ -27,6 +27,8 @@ module Types field :system, GraphQL::BOOLEAN_TYPE, null: false, description: 'Indicates whether this note was created by the system or by a user' + field :system_note_icon_name, GraphQL::STRING_TYPE, null: true, + description: 'Name of the icon corresponding to a system note' field :body, GraphQL::STRING_TYPE, null: false, @@ -46,6 +48,10 @@ module Types field :confidential, GraphQL::BOOLEAN_TYPE, null: true, description: 'Indicates if this note is confidential', method: :confidential? + + def system_note_icon_name + SystemNoteHelper.system_note_icon_name(object) if object.system? + end end end end diff --git a/app/graphql/types/tree/blob_type.rb b/app/graphql/types/tree/blob_type.rb index 4fdb4570b7d..22349203519 100644 --- a/app/graphql/types/tree/blob_type.rb +++ b/app/graphql/types/tree/blob_type.rb @@ -12,8 +12,6 @@ module Types field :web_url, GraphQL::STRING_TYPE, null: true, description: 'Web URL of the blob' - field :web_path, GraphQL::STRING_TYPE, null: true, - description: 'Web path of the blob' field :lfs_oid, GraphQL::STRING_TYPE, null: true, description: 'LFS ID of the blob', resolve: -> (blob, args, ctx) do diff --git a/app/graphql/types/tree/tree_entry_type.rb b/app/graphql/types/tree/tree_entry_type.rb index aff2e025761..81a7a7e66ae 100644 --- a/app/graphql/types/tree/tree_entry_type.rb +++ b/app/graphql/types/tree/tree_entry_type.rb @@ -13,8 +13,6 @@ module Types field :web_url, GraphQL::STRING_TYPE, null: true, description: 'Web URL for the tree entry (directory)' - field :web_path, GraphQL::STRING_TYPE, null: true, - description: 'Web path for the tree entry (directory)' end # rubocop: enable Graphql/AuthorizeTypes end diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb index f636f1b02b2..ab3c84ea539 100644 --- a/app/graphql/types/user_type.rb +++ b/app/graphql/types/user_type.rb @@ -22,8 +22,6 @@ module Types description: "URL of the user's avatar" field :web_url, GraphQL::STRING_TYPE, null: false, description: 'Web URL of the user' - field :web_path, GraphQL::STRING_TYPE, null: false, - description: 'Web path of the user' field :todos, Types::TodoType.connection_type, null: false, resolver: Resolvers::TodoResolver, description: 'Todos of the user' diff --git a/app/helpers/ci/pipeline_schedules_helper.rb b/app/helpers/ci/pipeline_schedules_helper.rb new file mode 100644 index 00000000000..20e5c90a60e --- /dev/null +++ b/app/helpers/ci/pipeline_schedules_helper.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Ci + module PipelineSchedulesHelper + def timezone_data + ActiveSupport::TimeZone.all.map do |timezone| + { + name: timezone.name, + offset: timezone.now.utc_offset, + identifier: timezone.tzinfo.identifier + } + end + end + end +end diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb index 41a255434af..4e5d12de8d3 100644 --- a/app/helpers/environments_helper.rb +++ b/app/helpers/environments_helper.rb @@ -92,6 +92,7 @@ module EnvironmentsHelper def static_metrics_data { 'documentation-path' => help_page_path('administration/monitoring/prometheus/index.md'), + 'add-dashboard-documentation-path' => help_page_path('user/project/integrations/prometheus.md', anchor: 'adding-a-new-dashboard-to-your-project'), 'empty-getting-started-svg-path' => image_path('illustrations/monitoring/getting_started.svg'), 'empty-loading-svg-path' => image_path('illustrations/monitoring/loading.svg'), 'empty-no-data-svg-path' => image_path('illustrations/monitoring/no_data.svg'), diff --git a/app/helpers/pipeline_schedules_helper.rb b/app/helpers/pipeline_schedules_helper.rb deleted file mode 100644 index 0e166106b32..00000000000 --- a/app/helpers/pipeline_schedules_helper.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module PipelineSchedulesHelper - def timezone_data - ActiveSupport::TimeZone.all.map do |timezone| - { - name: timezone.name, - offset: timezone.now.utc_offset, - identifier: timezone.tzinfo.identifier - } - end - end -end diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb index cff935d51b5..e0077db8d5c 100644 --- a/app/presenters/blob_presenter.rb +++ b/app/presenters/blob_presenter.rb @@ -18,10 +18,6 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated Gitlab::Routing.url_helpers.project_blob_url(blob.repository.project, File.join(blob.commit_id, blob.path)) end - def web_path - Gitlab::Routing.url_helpers.project_blob_path(blob.repository.project, File.join(blob.commit_id, blob.path)) - end - private def load_all_blob_data diff --git a/app/presenters/commit_presenter.rb b/app/presenters/commit_presenter.rb index 998e5160f8b..9ded00fcb7a 100644 --- a/app/presenters/commit_presenter.rb +++ b/app/presenters/commit_presenter.rb @@ -21,10 +21,6 @@ class CommitPresenter < Gitlab::View::Presenter::Delegated url_builder.build(commit) end - def web_path - url_builder.build(commit, only_path: true) - end - def signature_html return unless commit.has_signature? diff --git a/app/presenters/tree_entry_presenter.rb b/app/presenters/tree_entry_presenter.rb index 216b3b0d4c9..7bb10cd1455 100644 --- a/app/presenters/tree_entry_presenter.rb +++ b/app/presenters/tree_entry_presenter.rb @@ -6,8 +6,4 @@ class TreeEntryPresenter < Gitlab::View::Presenter::Delegated def web_url Gitlab::Routing.url_helpers.project_tree_url(tree.repository.project, File.join(tree.commit_id, tree.path)) end - - def web_path - Gitlab::Routing.url_helpers.project_tree_path(tree.repository.project, File.join(tree.commit_id, tree.path)) - end end diff --git a/app/presenters/user_presenter.rb b/app/presenters/user_presenter.rb index 2a4d6a070f8..14ef53e9ec8 100644 --- a/app/presenters/user_presenter.rb +++ b/app/presenters/user_presenter.rb @@ -6,8 +6,4 @@ class UserPresenter < Gitlab::View::Presenter::Delegated def web_url Gitlab::Routing.url_helpers.user_url(user) end - - def web_path - Gitlab::Routing.url_helpers.user_path(user) - end end diff --git a/app/services/projects/prometheus/alerts/create_events_service.rb b/app/services/projects/prometheus/alerts/create_events_service.rb deleted file mode 100644 index 4fcf841314b..00000000000 --- a/app/services/projects/prometheus/alerts/create_events_service.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -module Projects - module Prometheus - module Alerts - # Persists a series of Prometheus alert events as list of PrometheusAlertEvent. - class CreateEventsService < BaseService - def execute - create_events_from(alerts) - end - - private - - def create_events_from(alerts) - Array.wrap(alerts).map { |alert| create_event(alert) }.compact - end - - def create_event(payload) - parsed_alert = Gitlab::Alerting::Alert.new(project: project, payload: payload) - - return unless parsed_alert.valid? - - if parsed_alert.gitlab_managed? - create_managed_prometheus_alert_event(parsed_alert) - else - create_self_managed_prometheus_alert_event(parsed_alert) - end - end - - def alerts - params['alerts'] - end - - def find_alert(metric) - Projects::Prometheus::AlertsFinder - .new(project: project, metric: metric) - .execute - .first - end - - def create_managed_prometheus_alert_event(parsed_alert) - alert = find_alert(parsed_alert.metric_id) - event = PrometheusAlertEvent.find_or_initialize_by_payload_key(parsed_alert.project, alert, parsed_alert.gitlab_fingerprint) - - set_status(parsed_alert, event) - end - - def create_self_managed_prometheus_alert_event(parsed_alert) - event = SelfManagedPrometheusAlertEvent.find_or_initialize_by_payload_key(parsed_alert.project, parsed_alert.gitlab_fingerprint) do |event| - event.environment = parsed_alert.environment - event.title = parsed_alert.title - event.query_expression = parsed_alert.full_query - end - - set_status(parsed_alert, event) - end - - def set_status(parsed_alert, event) - persisted = case parsed_alert.status - when 'firing' - event.fire(parsed_alert.starts_at) - when 'resolved' - event.resolve(parsed_alert.ends_at) - end - - event if persisted - end - end - end - end -end diff --git a/app/services/projects/prometheus/alerts/notify_service.rb b/app/services/projects/prometheus/alerts/notify_service.rb index 877a4f99a94..4b3aed2d1d9 100644 --- a/app/services/projects/prometheus/alerts/notify_service.rb +++ b/app/services/projects/prometheus/alerts/notify_service.rb @@ -23,9 +23,7 @@ module Projects return unauthorized unless valid_alert_manager_token?(token) process_prometheus_alerts - persist_events send_alert_email if send_email? - process_incident_issues if process_issues? ServiceResponse.success end @@ -132,13 +130,6 @@ module Projects .prometheus_alerts_fired(project, firings) end - def process_incident_issues - alerts.each do |alert| - IncidentManagement::ProcessPrometheusAlertWorker - .perform_async(project.id, alert.to_h) - end - end - def process_prometheus_alerts alerts.each do |alert| AlertManagement::ProcessPrometheusAlertService @@ -147,10 +138,6 @@ module Projects end end - def persist_events - CreateEventsService.new(project, nil, params).execute - end - def bad_request ServiceResponse.error(message: 'Bad Request', http_status: :bad_request) end diff --git a/app/workers/incident_management/process_prometheus_alert_worker.rb b/app/workers/incident_management/process_prometheus_alert_worker.rb index e405bc2c2d2..4b778f6a621 100644 --- a/app/workers/incident_management/process_prometheus_alert_worker.rb +++ b/app/workers/incident_management/process_prometheus_alert_worker.rb @@ -9,68 +9,13 @@ module IncidentManagement worker_resource_boundary :cpu def perform(project_id, alert_hash) - project = find_project(project_id) - return unless project - - parsed_alert = Gitlab::Alerting::Alert.new(project: project, payload: alert_hash) - event = find_prometheus_alert_event(parsed_alert) - - if event&.resolved? - issue = event.related_issues.order_created_at_desc.detect(&:opened?) - - close_issue(project, issue) - else - issue = create_issue(project, alert_hash) - - relate_issue_to_event(event, issue) - end - end - - private - - def find_project(project_id) - Project.find_by_id(project_id) - end - - def find_prometheus_alert_event(alert) - if alert.gitlab_managed? - find_gitlab_managed_event(alert) - else - find_self_managed_event(alert) - end - end - - def find_gitlab_managed_event(alert) - PrometheusAlertEvent.find_by_payload_key(alert.gitlab_fingerprint) - end - - def find_self_managed_event(alert) - SelfManagedPrometheusAlertEvent.find_by_payload_key(alert.gitlab_fingerprint) - end - - def create_issue(project, alert) - IncidentManagement::CreateIssueService - .new(project, alert) - .execute - .dig(:issue) - end - - def close_issue(project, issue) - return if issue.blank? || issue.closed? - - processed_issue = Issues::CloseService - .new(project, User.alert_bot) - .execute(issue, system_note: false) - - SystemNoteService.auto_resolve_prometheus_alert(issue, project, User.alert_bot) if processed_issue.reset.closed? - end - - def relate_issue_to_event(event, issue) - return unless event && issue - - if event.related_issues.exclude?(issue) - event.related_issues << issue - end + # no-op + # + # This worker is not scheduled anymore since + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35943 + # and will be removed completely via + # https://gitlab.com/gitlab-org/gitlab/-/issues/227146 + # in 14.0. end end end diff --git a/changelogs/unreleased/astoicescu-addMetricsDashboardActionsMenu.yml b/changelogs/unreleased/astoicescu-addMetricsDashboardActionsMenu.yml new file mode 100644 index 00000000000..71d6a5f34a9 --- /dev/null +++ b/changelogs/unreleased/astoicescu-addMetricsDashboardActionsMenu.yml @@ -0,0 +1,5 @@ +--- +title: Add metrics settings menu to dashboard header +merge_request: 35028 +author: +type: added diff --git a/changelogs/unreleased/jdb-save-whitespace-setting.yml b/changelogs/unreleased/jdb-save-whitespace-setting.yml new file mode 100644 index 00000000000..1785accf764 --- /dev/null +++ b/changelogs/unreleased/jdb-save-whitespace-setting.yml @@ -0,0 +1,5 @@ +--- +title: Save show whitespace changes +merge_request: 35806 +author: +type: fixed diff --git a/changelogs/unreleased/update-rack-timeout.yml b/changelogs/unreleased/update-rack-timeout.yml new file mode 100644 index 00000000000..a7cafe7255f --- /dev/null +++ b/changelogs/unreleased/update-rack-timeout.yml @@ -0,0 +1,5 @@ +--- +title: Update `rack-timeout` to `0.5.2` +merge_request: 36289 +author: +type: changed diff --git a/changelogs/unreleased/update-secure-smau-metric.yml b/changelogs/unreleased/update-secure-smau-metric.yml new file mode 100644 index 00000000000..b07ec45a9f3 --- /dev/null +++ b/changelogs/unreleased/update-secure-smau-metric.yml @@ -0,0 +1,5 @@ +--- +title: Report all unique users for Secure scanners +merge_request: 33881 +author: +type: changed diff --git a/config/initializers/rack_timeout.rb b/config/initializers/rack_timeout.rb index 5d5a5fcf980..e217398ee7d 100644 --- a/config/initializers/rack_timeout.rb +++ b/config/initializers/rack_timeout.rb @@ -10,8 +10,6 @@ # logged and we should fix the potential timeout issue in the code itself. if Gitlab::Runtime.puma? && !Rails.env.test? - require 'rack/timeout/base' - Rack::Timeout::Logger.level = Logger::ERROR Gitlab::Application.configure do |config| diff --git a/config/settings.rb b/config/settings.rb index 99f1b85202e..c681fa32491 100644 --- a/config/settings.rb +++ b/config/settings.rb @@ -184,14 +184,15 @@ class Settings < Settingslogic URI.parse(url_without_path).host end - # Runs at a random time of day on a consistent day of the week based on + # Runs at a consistent random time of day on a day of the week based on # the instance UUID. This is to balance the load on the service receiving # these pings. The sidekiq job handles temporary http failures. def cron_for_usage_ping - hour = rand(24) - minute = rand(60) # Set a default UUID for the case when the UUID hasn't been initialized. uuid = Gitlab::CurrentSettings.uuid || 'uuid-not-set' + + minute = Digest::MD5.hexdigest(uuid + 'minute').to_i(16) % 60 + hour = Digest::MD5.hexdigest(uuid + 'hour').to_i(16) % 24 day_of_week = Digest::MD5.hexdigest(uuid).to_i(16) % 7 "#{minute} #{hour} * * #{day_of_week}" diff --git a/db/post_migrate/20200618152212_update_secure_smau_index.rb b/db/post_migrate/20200618152212_update_secure_smau_index.rb new file mode 100644 index 00000000000..ba989c279be --- /dev/null +++ b/db/post_migrate/20200618152212_update_secure_smau_index.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class UpdateSecureSmauIndex < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + INDEX_NAME = 'index_secure_ci_builds_on_user_id_created_at' + + disable_ddl_transaction! + + def up + add_concurrent_index( + :ci_builds, + [:user_id, :created_at], + where: "(((type)::text = 'Ci::Build'::text) AND ((name)::text = ANY (ARRAY[('container_scanning'::character varying)::text, ('dast'::character varying)::text, ('dependency_scanning'::character varying)::text, ('license_management'::character varying)::text, ('license_scanning'::character varying)::text, ('sast'::character varying)::text, ('secret_detection'::character varying)::text])))", + name: INDEX_NAME + ) + end + + def down + remove_concurrent_index_by_name :ci_builds, INDEX_NAME + end +end diff --git a/db/structure.sql b/db/structure.sql index 20e27b4350d..796ece6bc93 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -20079,6 +20079,8 @@ CREATE UNIQUE INDEX index_scim_identities_on_user_id_and_group_id ON public.scim CREATE UNIQUE INDEX index_scim_oauth_access_tokens_on_group_id_and_token_encrypted ON public.scim_oauth_access_tokens USING btree (group_id, token_encrypted); +CREATE INDEX index_secure_ci_builds_on_user_id_created_at ON public.ci_builds USING btree (user_id, created_at) WHERE (((type)::text = 'Ci::Build'::text) AND ((name)::text = ANY (ARRAY[('container_scanning'::character varying)::text, ('dast'::character varying)::text, ('dependency_scanning'::character varying)::text, ('license_management'::character varying)::text, ('license_scanning'::character varying)::text, ('sast'::character varying)::text, ('secret_detection'::character varying)::text]))); + CREATE INDEX index_security_ci_builds_on_name_and_id ON public.ci_builds USING btree (name, id) WHERE (((name)::text = ANY (ARRAY[('container_scanning'::character varying)::text, ('dast'::character varying)::text, ('dependency_scanning'::character varying)::text, ('license_management'::character varying)::text, ('sast'::character varying)::text, ('secret_detection'::character varying)::text, ('license_scanning'::character varying)::text])) AND ((type)::text = 'Ci::Build'::text)); CREATE INDEX index_self_managed_prometheus_alert_events_on_environment_id ON public.self_managed_prometheus_alert_events USING btree (environment_id); @@ -23560,6 +23562,7 @@ COPY "schema_migrations" (version) FROM STDIN; 20200618105638 20200618134223 20200618134723 +20200618152212 20200619000316 20200619154527 20200619154528 diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 1c792172ddb..e27988b64ab 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -814,11 +814,6 @@ type Blob implements Entry { type: EntryType! """ - Web path of the blob - """ - webPath: String - - """ Web URL of the blob """ webUrl: String @@ -1222,11 +1217,6 @@ type Commit { titleHtml: String """ - Web path of the commit - """ - webPath: String! - - """ Web URL of the commit """ webUrl: String! @@ -8266,6 +8256,11 @@ type Note implements ResolvableInterface { system: Boolean! """ + Name of the icon corresponding to a system note + """ + systemNoteIconName: String + + """ Timestamp of the note's last activity """ updatedAt: Time! @@ -13388,11 +13383,6 @@ type TreeEntry implements Entry { type: EntryType! """ - Web path for the tree entry (directory) - """ - webPath: String - - """ Web URL for the tree entry (directory) """ webUrl: String @@ -14294,11 +14284,6 @@ type User { username: String! """ - Web path of the user - """ - webPath: String! - - """ Web URL of the user """ webUrl: String! diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 0d282c49aad..aa886021e15 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -2129,20 +2129,6 @@ "deprecationReason": null }, { - "name": "webPath", - "description": "Web path of the blob", - "args": [ - - ], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { "name": "webUrl", "description": "Web URL of the blob", "args": [ @@ -3305,24 +3291,6 @@ "deprecationReason": null }, { - "name": "webPath", - "description": "Web path of the commit", - "args": [ - - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { "name": "webUrl", "description": "Web URL of the commit", "args": [ @@ -24634,6 +24602,20 @@ "deprecationReason": null }, { + "name": "systemNoteIconName", + "description": "Name of the icon corresponding to a system note", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "updatedAt", "description": "Timestamp of the note's last activity", "args": [ @@ -39551,20 +39533,6 @@ "deprecationReason": null }, { - "name": "webPath", - "description": "Web path for the tree entry (directory)", - "args": [ - - ], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { "name": "webUrl", "description": "Web URL for the tree entry (directory)", "args": [ @@ -41982,24 +41950,6 @@ "deprecationReason": null }, { - "name": "webPath", - "description": "Web path of the user", - "args": [ - - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { "name": "webUrl", "description": "Web URL of the user", "args": [ diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 937df0f92b1..94afeb727ae 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -164,7 +164,6 @@ Autogenerated return type of AwardEmojiToggle | `path` | String! | Path of the entry | | `sha` | String! | Last commit sha for the entry | | `type` | EntryType! | Type of tree entry | -| `webPath` | String | Web path of the blob | | `webUrl` | String | Web URL of the blob | ## Board @@ -228,7 +227,6 @@ Autogenerated return type of BoardListUpdateLimitMetrics | `signatureHtml` | String | Rendered HTML of the commit signature | | `title` | String | Title of the commit message | | `titleHtml` | String | The GitLab Flavored Markdown rendering of `title` | -| `webPath` | String! | Web path of the commit | | `webUrl` | String! | Web URL of the commit | ## CommitCreatePayload @@ -1264,6 +1262,7 @@ Contains statistics about a milestone | `resolvedAt` | Time | Timestamp of when the object was resolved | | `resolvedBy` | User | User who resolved the object | | `system` | Boolean! | Indicates whether this note was created by the system or by a user | +| `systemNoteIconName` | String | Name of the icon corresponding to a system note | | `updatedAt` | Time! | Timestamp of the note's last activity | | `userPermissions` | NotePermissions! | Permissions for the current user on the resource | @@ -2021,7 +2020,6 @@ Represents a directory | `path` | String! | Path of the entry | | `sha` | String! | Last commit sha for the entry | | `type` | EntryType! | Type of tree entry | -| `webPath` | String | Web path for the tree entry (directory) | | `webUrl` | String | Web URL for the tree entry (directory) | ## UpdateAlertStatusPayload @@ -2125,7 +2123,6 @@ Autogenerated return type of UpdateSnippet | `state` | UserState! | State of the user | | `userPermissions` | UserPermissions! | Permissions for the current user on the resource | | `username` | String! | Username of the user. Unique within this instance of GitLab | -| `webPath` | String! | Web path of the user | | `webUrl` | String! | Web URL of the user | ## UserPermissions diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 569ee48e282..c10dac2750c 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -458,7 +458,13 @@ For Omnibus GitLab packages: gitlab_rails['backup_upload_connection'] = { 'provider' => 'Google', 'google_storage_access_key_id' => 'Access Key', - 'google_storage_secret_access_key' => 'Secret' + 'google_storage_secret_access_key' => 'Secret', + + ## If you have CNAME buckets (foo.example.com), you might run into SSL issues + ## when uploading backups ("hostname foo.example.com.storage.googleapis.com + ## does not match the server certificate"). In that case, uncomnent the following + ## setting. See: https://github.com/fog/fog/issues/2834 + #'path_style' => true } gitlab_rails['backup_upload_remote_directory'] = 'my.google.bucket' ``` diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 7b29ccf0439..4b8d61f625d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3240,18 +3240,6 @@ msgstr "" msgid "Audit Log" msgstr "" -msgid "AuditEvents|(removed)" -msgstr "" - -msgid "AuditEvents|Action" -msgstr "" - -msgid "AuditEvents|At" -msgstr "" - -msgid "AuditEvents|Target" -msgstr "" - msgid "AuditLogs|(removed)" msgstr "" @@ -6833,6 +6821,9 @@ msgstr "" msgid "Create new label" msgstr "" +msgid "Create new value stream" +msgstr "" + msgid "Create new..." msgstr "" @@ -14506,15 +14497,27 @@ msgstr "" msgid "Metrics|Avg" msgstr "" +msgid "Metrics|Cancel" +msgstr "" + msgid "Metrics|Check out the CI/CD documentation on deploying to an environment" msgstr "" msgid "Metrics|Create custom dashboard %{fileName}" msgstr "" +msgid "Metrics|Create dashboard" +msgstr "" + msgid "Metrics|Create metric" msgstr "" +msgid "Metrics|Create new dashboard" +msgstr "" + +msgid "Metrics|Create your dashboard configuration file" +msgstr "" + msgid "Metrics|Current" msgstr "" @@ -14527,6 +14530,9 @@ msgstr "" msgid "Metrics|Duplicate" msgstr "" +msgid "Metrics|Duplicate current dashboard" +msgstr "" + msgid "Metrics|Duplicate dashboard" msgstr "" @@ -14577,6 +14583,9 @@ msgstr "" msgid "Metrics|New metric" msgstr "" +msgid "Metrics|Open repository" +msgstr "" + msgid "Metrics|PromQL query is valid" msgstr "" @@ -14631,6 +14640,9 @@ msgstr "" msgid "Metrics|There was an error while retrieving metrics. %{message}" msgstr "" +msgid "Metrics|To create a new dashboard, add a new YAML file to %{codeStart}.gitlab/dashboards%{codeEnd} at the root of this project." +msgstr "" + msgid "Metrics|Unexpected deployment data response from prometheus endpoint" msgstr "" @@ -14652,6 +14664,9 @@ msgstr "" msgid "Metrics|Values" msgstr "" +msgid "Metrics|View documentation" +msgstr "" + msgid "Metrics|View logs" msgstr "" diff --git a/package.json b/package.json index fd06989ea27..7cee19793c9 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@babel/preset-env": "^7.10.1", "@gitlab/at.js": "1.5.5", "@gitlab/svgs": "1.151.0", - "@gitlab/ui": "17.18.1", + "@gitlab/ui": "17.19.0", "@gitlab/visual-review-tools": "1.6.1", "@rails/actioncable": "^6.0.3-1", "@sentry/browser": "^5.10.2", diff --git a/qa/bin/rubymine b/qa/bin/rubymine new file mode 100755 index 00000000000..0be0cf0ec33 --- /dev/null +++ b/qa/bin/rubymine @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby + +require_relative '../qa' + +ARGV.unshift('Test::Instance::All', ENV['GITLAB_URL'], '--') + +QA::Scenario + .const_get(ARGV.shift) + .launch!(ARGV) diff --git a/spec/config/settings_spec.rb b/spec/config/settings_spec.rb index 9db3d35cbe5..ed873478fc9 100644 --- a/spec/config/settings_spec.rb +++ b/spec/config/settings_spec.rb @@ -112,4 +112,26 @@ RSpec.describe Settings do end end end + + describe '.cron_for_usage_ping' do + it 'returns correct crontab for some manually calculated example' do + allow(Gitlab::CurrentSettings) + .to receive(:uuid) { 'd9e2f4e8-db1f-4e51-b03d-f427e1965c4a'} + + expect(described_class.send(:cron_for_usage_ping)).to eq('21 18 * * 4') + end + + it 'returns min, hour, day in the valid range' do + allow(Gitlab::CurrentSettings) + .to receive(:uuid) { SecureRandom.uuid } + + 10.times do + cron = described_class.send(:cron_for_usage_ping).split(/\s/) + + expect(cron[0].to_i).to be_between(0, 59) + expect(cron[1].to_i).to be_between(0, 23) + expect(cron[4].to_i).to be_between(0, 6) + end + end + end end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 52e91f31ec1..4b1f3194ce5 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -159,6 +159,11 @@ FactoryBot.define do system { true } end + trait :with_system_note_metadata do + system + system_note_metadata + end + trait :downvote do note { "thumbsdown" } end diff --git a/spec/features/clusters/installing_applications_shared_examples.rb b/spec/features/clusters/installing_applications_shared_examples.rb index 7528bcaa8b7..74150c42519 100644 --- a/spec/features/clusters/installing_applications_shared_examples.rb +++ b/spec/features/clusters/installing_applications_shared_examples.rb @@ -168,32 +168,54 @@ RSpec.shared_examples "installing applications for a cluster" do |managed_apps_l allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async) create(:clusters_applications_helm, :installed, cluster: cluster) unless managed_apps_local_tiller + end + it 'shows status transition' do page.within('.js-cluster-application-row-cert_manager') do click_button 'Install' + wait_for_requests + + expect(page).to have_field('Issuer Email', with: cluster.user.email) + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing') + + Clusters::Cluster.last.application_cert_manager.make_installing! + + expect(page).to have_field('Issuer Email', with: cluster.user.email) + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing') + + Clusters::Cluster.last.application_cert_manager.make_installed! + + expect(page).to have_field('Issuer Email', with: cluster.user.email) + expect(page).to have_css('.js-cluster-application-uninstall-button', exact_text: 'Uninstall') end - wait_for_requests + expect(page).to have_content('Cert-Manager was successfully installed on your Kubernetes cluster') end - it 'shows status transition' do + it 'installs with custom email' do + custom_email = 'new_email@example.org' + page.within('.js-cluster-application-row-cert_manager') do - expect(page).to have_field('Issuer Email', with: cluster.user.email) + # Wait for the polling to finish + wait_for_requests + + page.find('.js-email').set(custom_email) + click_button 'Install' + wait_for_requests + + expect(page).to have_field('Issuer Email', with: custom_email) expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing') - page.find('.js-email').set("new_email@example.org") Clusters::Cluster.last.application_cert_manager.make_installing! - expect(page).to have_field('Issuer Email', with: 'new_email@example.org') + expect(page).to have_field('Issuer Email', with: custom_email) expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing') Clusters::Cluster.last.application_cert_manager.make_installed! - expect(page).to have_field('Issuer Email', with: 'new_email@example.org') + expect(page).to have_field('Issuer Email', with: custom_email) expect(page).to have_css('.js-cluster-application-uninstall-button', exact_text: 'Uninstall') end - - expect(page).to have_content('Cert-Manager was successfully installed on your Kubernetes cluster') end end diff --git a/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb b/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb index fab500f47bf..05f4c16ef60 100644 --- a/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb +++ b/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb @@ -27,7 +27,7 @@ RSpec.describe 'Merge request > User toggles whitespace changes', :js do find('.js-show-diff-settings').click - expect(find('#show-whitespace')).to be_checked + expect(find('#show-whitespace')).not_to be_checked end end end diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb index 921bbbfbe7d..1e2cd3c0a69 100644 --- a/spec/features/projects/pipeline_schedules_spec.rb +++ b/spec/features/projects/pipeline_schedules_spec.rb @@ -3,8 +3,6 @@ require 'spec_helper' RSpec.describe 'Pipeline Schedules', :js do - include PipelineSchedulesHelper - let!(:project) { create(:project, :repository) } let!(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly, project: project ) } let!(:pipeline) { create(:ci_pipeline, pipeline_schedule: pipeline_schedule) } diff --git a/spec/frontend/alert_management/components/system_notes/alert_management_system_note_spec.js b/spec/frontend/alert_management/components/system_notes/alert_management_system_note_spec.js index 652cbc4b838..8dd663e55d9 100644 --- a/spec/frontend/alert_management/components/system_notes/alert_management_system_note_spec.js +++ b/spec/frontend/alert_management/components/system_notes/alert_management_system_note_spec.js @@ -28,7 +28,11 @@ describe('Alert Details System Note', () => { }); it('renders the correct system note', () => { - expect(wrapper.find('.note-wrapper').attributes('id')).toBe('note_1628'); + const noteId = wrapper.find('.note-wrapper').attributes('id'); + const iconRoute = wrapper.find('use').attributes('href'); + + expect(noteId).toBe('note_1628'); + expect(iconRoute.includes('user')).toBe(true); }); }); }); diff --git a/spec/frontend/alert_management/mocks/alerts.json b/spec/frontend/alert_management/mocks/alerts.json index 34eed7ae024..f63019d1e5c 100644 --- a/spec/frontend/alert_management/mocks/alerts.json +++ b/spec/frontend/alert_management/mocks/alerts.json @@ -33,7 +33,8 @@ "name": "Administrator", "username": "root", "webUrl": "http://192.168.1.4:3000/root" - } + }, + "systemNoteIconName": "user" } ] } diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js index 7d79dcfbfe3..ee55669cd9b 100644 --- a/spec/frontend/diffs/store/actions_spec.js +++ b/spec/frontend/diffs/store/actions_spec.js @@ -6,6 +6,8 @@ import { INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE, DIFFS_PER_PAGE, + DIFF_WHITESPACE_COOKIE_NAME, + SHOW_WHITESPACE, } from '~/diffs/constants'; import { setBaseConfig, @@ -1187,10 +1189,10 @@ describe('DiffsStoreActions', () => { ); }); - it('sets localStorage', () => { + it('sets cookie', () => { setShowWhitespace({ commit() {} }, { showWhitespace: true }); - expect(localStorage.setItem).toHaveBeenCalledWith('mr_show_whitespace', true); + expect(Cookies.get(DIFF_WHITESPACE_COOKIE_NAME)).toEqual(SHOW_WHITESPACE); }); it('calls history pushState', () => { diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js index 891de45e268..d87619e1e3c 100644 --- a/spec/frontend/diffs/store/utils_spec.js +++ b/spec/frontend/diffs/store/utils_spec.js @@ -1090,4 +1090,26 @@ describe('DiffsStoreUtils', () => { ]); }); }); + + describe('getDefaultWhitespace', () => { + it('defaults to true if querystring and cookie are undefined', () => { + expect(utils.getDefaultWhitespace()).toBe(true); + }); + + it('returns false if querystring is `1`', () => { + expect(utils.getDefaultWhitespace('1', '0')).toBe(false); + }); + + it('returns true if querystring is `0`', () => { + expect(utils.getDefaultWhitespace('0', undefined)).toBe(true); + }); + + it('returns false if cookie is `1`', () => { + expect(utils.getDefaultWhitespace(undefined, '1')).toBe(false); + }); + + it('returns true if cookie is `0`', () => { + expect(utils.getDefaultWhitespace(undefined, '0')).toBe(true); + }); + }); }); diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap index dac663252ab..cb3f74ca73c 100644 --- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap +++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap @@ -13,17 +13,23 @@ exports[`Dashboard template matches the default snapshot 1`] = ` class="prometheus-graphs-header d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 border-bottom bg-gray-light" > <div - class="mb-2 pr-2 d-flex d-sm-block" + class="mb-2 mr-2 d-flex d-sm-block" > <dashboards-dropdown-stub class="flex-grow-1" data-qa-selector="dashboards_filter_dropdown" defaultbranch="master" id="monitor-dashboards-dropdown" + modalid="duplicateDashboard" toggle-class="dropdown-menu-toggle" /> </div> + <span + aria-hidden="true" + class="gl-pl-3 border-left gl-mb-3 d-none d-sm-block" + /> + <div class="mb-2 pr-2 d-flex d-sm-block" > @@ -121,7 +127,14 @@ exports[`Dashboard template matches the default snapshot 1`] = ` <!----> <!----> + + <!----> </div> + + <duplicate-dashboard-modal-stub + defaultbranch="master" + modalid="duplicateDashboard" + /> </div> <!----> diff --git a/spec/frontend/monitoring/components/create_dashboard_modal_spec.js b/spec/frontend/monitoring/components/create_dashboard_modal_spec.js new file mode 100644 index 00000000000..d1028445638 --- /dev/null +++ b/spec/frontend/monitoring/components/create_dashboard_modal_spec.js @@ -0,0 +1,48 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlModal } from '@gitlab/ui'; +import CreateDashboardModal from '~/monitoring/components/create_dashboard_modal.vue'; + +describe('Create dashboard modal', () => { + let wrapper; + + const defaultProps = { + modalId: 'id', + projectPath: 'https://localhost/', + addDashboardDocumentationPath: 'https://link/to/docs', + }; + + const findDocsButton = () => wrapper.find('[data-testid="create-dashboard-modal-docs-button"]'); + const findRepoButton = () => wrapper.find('[data-testid="create-dashboard-modal-repo-button"]'); + + const createWrapper = (props = {}, options = {}) => { + wrapper = shallowMount(CreateDashboardModal, { + propsData: { ...defaultProps, ...props }, + stubs: { + GlModal, + }, + ...options, + }); + }; + + beforeEach(() => { + createWrapper(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('has button that links to the project url', () => { + findRepoButton().trigger('click'); + + return wrapper.vm.$nextTick().then(() => { + expect(findRepoButton().exists()).toBe(true); + expect(findRepoButton().attributes('href')).toBe(defaultProps.projectPath); + }); + }); + + it('has button that links to the docs', () => { + expect(findDocsButton().exists()).toBe(true); + expect(findDocsButton().attributes('href')).toBe(defaultProps.addDashboardDocumentationPath); + }); +}); diff --git a/spec/frontend/monitoring/components/dashboard_header_spec.js b/spec/frontend/monitoring/components/dashboard_header_spec.js new file mode 100644 index 00000000000..5af7b4049d1 --- /dev/null +++ b/spec/frontend/monitoring/components/dashboard_header_spec.js @@ -0,0 +1,160 @@ +import { shallowMount } from '@vue/test-utils'; +import { createStore } from '~/monitoring/stores'; +import DashboardHeader from '~/monitoring/components/dashboard_header.vue'; +import DuplicateDashboardModal from '~/monitoring/components/duplicate_dashboard_modal.vue'; +import CreateDashboardModal from '~/monitoring/components/create_dashboard_modal.vue'; +import { setupAllDashboards } from '../store_utils'; +import { dashboardGitResponse, dashboardHeaderProps } from '../mock_data'; +import { redirectTo, mergeUrlParams } from '~/lib/utils/url_utility'; + +jest.mock('~/lib/utils/url_utility', () => ({ + redirectTo: jest.fn(), + queryToObject: jest.fn(), + mergeUrlParams: jest.requireActual('~/lib/utils/url_utility').mergeUrlParams, +})); + +describe('Dashboard header', () => { + let store; + let wrapper; + + const findActionsMenu = () => wrapper.find('[data-testid="actions-menu"]'); + const findCreateDashboardMenuItem = () => + findActionsMenu().find('[data-testid="action-create-dashboard"]'); + const findCreateDashboardDuplicateItem = () => + findActionsMenu().find('[data-testid="action-duplicate-dashboard"]'); + const findDuplicateDashboardModal = () => wrapper.find(DuplicateDashboardModal); + const findCreateDashboardModal = () => wrapper.find('[data-testid="create-dashboard-modal"]'); + + const createShallowWrapper = (props = {}, options = {}) => { + wrapper = shallowMount(DashboardHeader, { + propsData: { ...dashboardHeaderProps, ...props }, + store, + ...options, + }); + }; + + beforeEach(() => { + store = createStore(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when a dashboard has been duplicated in the duplicate dashboard modal', () => { + /** + * The duplicate dashboard modal gets called both by a menu item from the + * dashboards dropdown and by an item from the actions menu. + * + * This spec is context agnostic, so it addresses all cases where the + * duplicate dashboard modal gets called. + */ + it('redirects to the newly created dashboard', () => { + delete window.location; + window.location = new URL('https://localhost'); + + const newDashboard = dashboardGitResponse[1]; + const params = { + dashboard: encodeURIComponent(newDashboard.path), + }; + const newDashboardUrl = mergeUrlParams(params, window.location.href); + + createShallowWrapper(); + findDuplicateDashboardModal().vm.$emit('dashboardDuplicated', newDashboard); + + return wrapper.vm.$nextTick().then(() => { + expect(redirectTo).toHaveBeenCalled(); + expect(redirectTo).toHaveBeenCalledWith(newDashboardUrl); + }); + }); + }); + + describe('actions menu', () => { + beforeEach(() => { + store.state.monitoringDashboard.projectPath = ''; + createShallowWrapper(); + }); + + it('is rendered if projectPath is set in store', () => { + store.state.monitoringDashboard.projectPath = 'https://path/to/project'; + + return wrapper.vm.$nextTick().then(() => { + expect(findActionsMenu().exists()).toBe(true); + }); + }); + + it('is not rendered if projectPath is not set in store', () => { + expect(findActionsMenu().exists()).toBe(false); + }); + + it('contains a modal', () => { + store.state.monitoringDashboard.projectPath = 'https://path/to/project'; + + return wrapper.vm.$nextTick().then(() => { + expect(findActionsMenu().contains(CreateDashboardModal)).toBe(true); + }); + }); + + describe('when the selected dashboard is the system dashboard', () => { + it('contains a "Create New" menu item and a "Duplicate Dashboard" menu item', () => { + store.state.monitoringDashboard.projectPath = 'https://path/to/project'; + setupAllDashboards(store); + + return wrapper.vm.$nextTick().then(() => { + expect(findCreateDashboardMenuItem().exists()).toBe(true); + expect(findCreateDashboardDuplicateItem().exists()).toBe(true); + }); + }); + }); + + describe('when the selected dashboard is not the system dashboard', () => { + it('contains a "Create New" menu item and no "Duplicate Dashboard" menu item', () => { + store.state.monitoringDashboard.projectPath = 'https://path/to/project'; + + return wrapper.vm.$nextTick().then(() => { + expect(findCreateDashboardMenuItem().exists()).toBe(true); + expect(findCreateDashboardDuplicateItem().exists()).toBe(false); + }); + }); + }); + }); + + describe('actions menu modals', () => { + const url = 'https://path/to/project'; + + beforeEach(() => { + store.state.monitoringDashboard.projectPath = url; + setupAllDashboards(store); + + createShallowWrapper(); + }); + + it('Clicking on "Create New" opens up a modal', () => { + const modalId = 'createDashboard'; + const modalTrigger = findCreateDashboardMenuItem(); + const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit'); + + modalTrigger.trigger('click'); + + return wrapper.vm.$nextTick().then(() => { + expect(rootEmit.mock.calls[0]).toContainEqual(modalId); + }); + }); + + it('"Create new dashboard" modal contains correct buttons', () => { + expect(findCreateDashboardModal().props('projectPath')).toBe(url); + }); + + it('"Duplicate Dashboard" opens up a modal', () => { + const modalId = 'duplicateDashboard'; + const modalTrigger = findCreateDashboardDuplicateItem(); + const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit'); + + modalTrigger.trigger('click'); + + return wrapper.vm.$nextTick().then(() => { + expect(rootEmit.mock.calls[0]).toContainEqual(modalId); + }); + }); + }); +}); diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js index 948f56dabdb..a4fbc0a981a 100644 --- a/spec/frontend/monitoring/components/dashboard_spec.js +++ b/spec/frontend/monitoring/components/dashboard_spec.js @@ -1237,7 +1237,7 @@ describe('Dashboard', () => { it('uses modal for custom metrics form', () => { expect(wrapper.find(GlModal).exists()).toBe(true); - expect(wrapper.find(GlModal).attributes().modalid).toBe('add-metric'); + expect(wrapper.find(GlModal).attributes().modalid).toBe('addMetric'); }); it('adding new metric is tracked', done => { const submitButton = wrapper diff --git a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js index b29d86cbc5b..9836d3b4bab 100644 --- a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js +++ b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js @@ -1,14 +1,12 @@ import { shallowMount } from '@vue/test-utils'; -import { GlDropdownItem, GlModal, GlLoadingIcon, GlAlert, GlIcon } from '@gitlab/ui'; -import waitForPromises from 'helpers/wait_for_promises'; +import { GlDropdownItem, GlIcon } from '@gitlab/ui'; import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue'; -import DuplicateDashboardForm from '~/monitoring/components/duplicate_dashboard_form.vue'; import { dashboardGitResponse } from '../mock_data'; const defaultBranch = 'master'; - +const modalId = 'duplicateDashboardModalId'; const starredDashboards = dashboardGitResponse.filter(({ starred }) => starred); const notStarredDashboards = dashboardGitResponse.filter(({ starred }) => !starred); @@ -32,6 +30,7 @@ describe('DashboardsDropdown', () => { propsData: { ...props, defaultBranch, + modalId, }, sync: false, ...storeOpts, @@ -82,7 +81,7 @@ describe('DashboardsDropdown', () => { const searchTerm = 'Default'; setSearchTerm(searchTerm); - return wrapper.vm.$nextTick(() => { + return wrapper.vm.$nextTick().then(() => { expect(findItems()).toHaveLength(1); }); }); @@ -91,7 +90,7 @@ describe('DashboardsDropdown', () => { const searchTerm = 'does-not-exist'; setSearchTerm(searchTerm); - return wrapper.vm.$nextTick(() => { + return wrapper.vm.$nextTick().then(() => { expect(findNoItemsMsg().isVisible()).toBe(true); }); }); @@ -151,7 +150,7 @@ describe('DashboardsDropdown', () => { }); }); - describe('when a system dashboard is selected', () => { + describe('when the selected dashboard can be duplicated', () => { let duplicateDashboardAction; let modalDirective; @@ -172,151 +171,53 @@ describe('DashboardsDropdown', () => { }, }, ); - - wrapper.vm.$refs.duplicateDashboardModal.hide = jest.fn(); }); - it('displays an item for each dashboard plus a "duplicate dashboard" item', () => { - const item = wrapper.findAll({ ref: 'duplicateDashboardItem' }); - + it('displays a dropdown item for each dashboard', () => { expect(findItems().length).toEqual(dashboardGitResponse.length + 1); - expect(item.length).toBe(1); }); - describe('modal form', () => { - let okEvent; - - const findModal = () => wrapper.find(GlModal); - const findAlert = () => wrapper.find(GlAlert); - - beforeEach(() => { - okEvent = { - preventDefault: jest.fn(), - }; - }); - - it('exists and contains a form to duplicate a dashboard', () => { - expect(findModal().exists()).toBe(true); - expect(findModal().contains(DuplicateDashboardForm)).toBe(true); - }); - - it('saves a new dashboard', () => { - findModal().vm.$emit('ok', okEvent); - - return waitForPromises().then(() => { - expect(okEvent.preventDefault).toHaveBeenCalled(); - - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); - expect(wrapper.vm.$refs.duplicateDashboardModal.hide).toHaveBeenCalled(); - expect(wrapper.emitted().selectDashboard).toBeTruthy(); - expect(findAlert().exists()).toBe(false); - }); - }); - - describe('when a new dashboard is saved succesfully', () => { - const newDashboard = { - can_edit: true, - default: false, - display_name: 'A new dashboard', - system_dashboard: false, - }; - - const submitForm = formVals => { - duplicateDashboardAction.mockResolvedValueOnce(newDashboard); - findModal() - .find(DuplicateDashboardForm) - .vm.$emit('change', { - dashboard: 'common_metrics.yml', - commitMessage: 'A commit message', - ...formVals, - }); - findModal().vm.$emit('ok', okEvent); - }; - - it('to the default branch, redirects to the new dashboard', () => { - submitForm({ - branch: defaultBranch, - }); - - return waitForPromises().then(() => { - expect(wrapper.emitted().selectDashboard[0][0]).toEqual(newDashboard); - }); - }); - - it('to a new branch refreshes in the current dashboard', () => { - submitForm({ - branch: 'another-branch', - }); - - return waitForPromises().then(() => { - expect(wrapper.emitted().selectDashboard[0][0]).toEqual(dashboardGitResponse[0]); - }); - }); - }); - - it('handles error when a new dashboard is not saved', () => { - const errMsg = 'An error occurred'; + it('displays one "duplicate dashboard" dropdown item with a directive attached', () => { + const item = wrapper.findAll('[data-testid="duplicateDashboardItem"]'); - duplicateDashboardAction.mockRejectedValueOnce(errMsg); - findModal().vm.$emit('ok', okEvent); - - return waitForPromises().then(() => { - expect(okEvent.preventDefault).toHaveBeenCalled(); - - expect(findAlert().exists()).toBe(true); - expect(findAlert().text()).toBe(errMsg); + expect(item.length).toBe(1); + }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); - expect(wrapper.vm.$refs.duplicateDashboardModal.hide).not.toHaveBeenCalled(); - }); - }); + it('"duplicate dashboard" dropdown item directive works', () => { + const item = wrapper.find('[data-testid="duplicateDashboardItem"]'); - it('id is correct, as the value of modal directive binding matches modal id', () => { - expect(modalDirective).toHaveBeenCalledTimes(1); + item.trigger('click'); - // Binding's second argument contains the modal id - expect(modalDirective.mock.calls[0][1]).toEqual( - expect.objectContaining({ - value: findModal().props('modalId'), - }), - ); + return wrapper.vm.$nextTick().then(() => { + expect(modalDirective).toHaveBeenCalled(); }); + }); - it('updates the form on changes', () => { - const formVals = { - dashboard: 'common_metrics.yml', - commitMessage: 'A commit message', - }; - - findModal() - .find(DuplicateDashboardForm) - .vm.$emit('change', formVals); + it('id is correct, as the value of modal directive binding matches modal id', () => { + expect(modalDirective).toHaveBeenCalledTimes(1); - // Binding's second argument contains the modal id - expect(wrapper.vm.form).toEqual(formVals); - }); + // Binding's second argument contains the modal id + expect(modalDirective.mock.calls[0][1]).toEqual( + expect.objectContaining({ + value: modalId, + }), + ); }); }); - describe('when a custom dashboard is selected', () => { - const findModal = () => wrapper.find(GlModal); - + describe('when the selected dashboard can not be duplicated', () => { beforeEach(() => { - wrapper = createComponent({ - selectedDashboard: dashboardGitResponse[1], - }); + [, mockSelectedDashboard] = dashboardGitResponse; + + wrapper = createComponent(); }); - it('displays an item for each dashboard', () => { - const item = wrapper.findAll({ ref: 'duplicateDashboardItem' }); + it('displays a dropdown list item for each dashboard, but no list item for "duplicate dashboard"', () => { + const item = wrapper.findAll('[data-testid="duplicateDashboardItem"]'); expect(findItems()).toHaveLength(dashboardGitResponse.length); expect(item.length).toBe(0); }); - - it('modal form does not exist and contains a form to duplicate a dashboard', () => { - expect(findModal().exists()).toBe(false); - }); }); describe('when a dashboard gets selected by the user', () => { diff --git a/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js b/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js new file mode 100644 index 00000000000..d8ffb4443ac --- /dev/null +++ b/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js @@ -0,0 +1,111 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlAlert, GlLoadingIcon, GlModal } from '@gitlab/ui'; + +import waitForPromises from 'helpers/wait_for_promises'; + +import DuplicateDashboardModal from '~/monitoring/components/duplicate_dashboard_modal.vue'; +import DuplicateDashboardForm from '~/monitoring/components/duplicate_dashboard_form.vue'; + +import { dashboardGitResponse } from '../mock_data'; + +describe('duplicate dashboard modal', () => { + let wrapper; + let mockDashboards; + let mockSelectedDashboard; + let duplicateDashboardAction; + let okEvent; + + function createComponent(opts = {}) { + const storeOpts = { + methods: { + duplicateSystemDashboard: jest.fn(), + }, + computed: { + allDashboards: () => mockDashboards, + selectedDashboard: () => mockSelectedDashboard, + }, + }; + + return shallowMount(DuplicateDashboardModal, { + propsData: { + defaultBranch: 'master', + modalId: 'id', + }, + sync: false, + ...storeOpts, + ...opts, + }); + } + + const findAlert = () => wrapper.find(GlAlert); + const findModal = () => wrapper.find(GlModal); + const findDuplicateDashboardForm = () => wrapper.find(DuplicateDashboardForm); + + beforeEach(() => { + mockDashboards = dashboardGitResponse; + [mockSelectedDashboard] = dashboardGitResponse; + + duplicateDashboardAction = jest.fn().mockResolvedValue(); + + okEvent = { + preventDefault: jest.fn(), + }; + + wrapper = createComponent({ + methods: { + // Mock vuex actions + duplicateSystemDashboard: duplicateDashboardAction, + }, + }); + + wrapper.vm.$refs.duplicateDashboardModal.hide = jest.fn(); + }); + + it('contains a form to duplicate a dashboard', () => { + expect(findDuplicateDashboardForm().exists()).toBe(true); + }); + + it('saves a new dashboard', () => { + findModal().vm.$emit('ok', okEvent); + + return waitForPromises().then(() => { + expect(okEvent.preventDefault).toHaveBeenCalled(); + expect(wrapper.emitted().dashboardDuplicated).toBeTruthy(); + expect(wrapper.emitted().dashboardDuplicated[0]).toEqual([dashboardGitResponse[0]]); + expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.vm.$refs.duplicateDashboardModal.hide).toHaveBeenCalled(); + expect(findAlert().exists()).toBe(false); + }); + }); + + it('handles error when a new dashboard is not saved', () => { + const errMsg = 'An error occurred'; + + duplicateDashboardAction.mockRejectedValueOnce(errMsg); + findModal().vm.$emit('ok', okEvent); + + return waitForPromises().then(() => { + expect(okEvent.preventDefault).toHaveBeenCalled(); + + expect(findAlert().exists()).toBe(true); + expect(findAlert().text()).toBe(errMsg); + + expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.vm.$refs.duplicateDashboardModal.hide).not.toHaveBeenCalled(); + }); + }); + + it('updates the form on changes', () => { + const formVals = { + dashboard: 'common_metrics.yml', + commitMessage: 'A commit message', + }; + + findModal() + .find(DuplicateDashboardForm) + .vm.$emit('change', formVals); + + // Binding's second argument contains the modal id + expect(wrapper.vm.form).toEqual(formVals); + }); +}); diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js index 50395a3ff89..cffc9a21a63 100644 --- a/spec/frontend/monitoring/mock_data.js +++ b/spec/frontend/monitoring/mock_data.js @@ -805,3 +805,13 @@ export const storeVariables = [ ...storeCustomVariables, ...storeMetricLabelValuesVariables, ]; + +export const dashboardHeaderProps = { + defaultBranch: 'master', + addDashboardDocumentationPath: 'https://path/to/docs', + isRearrangingPanels: false, + selectedTimeRange: { + start: '2020-01-01T00:00:00.000Z', + end: '2020-01-01T01:00:00.000Z', + }, +}; diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js index 37da5ea96d9..86a8793c508 100644 --- a/spec/frontend/monitoring/store/mutations_spec.js +++ b/spec/frontend/monitoring/store/mutations_spec.js @@ -386,6 +386,7 @@ describe('Monitoring mutations', () => { }); }); }); + describe('SET_ALL_DASHBOARDS', () => { it('stores `undefined` dashboards as an empty array', () => { mutations[types.SET_ALL_DASHBOARDS](stateCopy, undefined); diff --git a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap index cf2e6b00800..1dca65dd862 100644 --- a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap +++ b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap @@ -10,7 +10,7 @@ exports[`Repository last commit component renders commit widget 1`] = ` imgcssclasses="" imgsize="40" imgsrc="https://test.com" - linkhref="/test" + linkhref="https://test.com/test" tooltipplacement="top" tooltiptext="" username="" @@ -24,7 +24,7 @@ exports[`Repository last commit component renders commit widget 1`] = ` > <gl-link-stub class="commit-row-message item-title" - href="/commit/123" + href="https://test.com/commit/123" > Commit title </gl-link-stub> @@ -36,7 +36,7 @@ exports[`Repository last commit component renders commit widget 1`] = ` > <gl-link-stub class="commit-author-link js-user-link" - href="/test" + href="https://test.com/test" > Test @@ -110,7 +110,7 @@ exports[`Repository last commit component renders the signature HTML as returned imgcssclasses="" imgsize="40" imgsrc="https://test.com" - linkhref="/test" + linkhref="https://test.com/test" tooltipplacement="top" tooltiptext="" username="" @@ -124,7 +124,7 @@ exports[`Repository last commit component renders the signature HTML as returned > <gl-link-stub class="commit-row-message item-title" - href="/commit/123" + href="https://test.com/commit/123" > Commit title </gl-link-stub> @@ -136,7 +136,7 @@ exports[`Repository last commit component renders the signature HTML as returned > <gl-link-stub class="commit-author-link js-user-link" - href="/test" + href="https://test.com/test" > Test diff --git a/spec/frontend/repository/components/last_commit_spec.js b/spec/frontend/repository/components/last_commit_spec.js index 115784a9201..a5bfeb08fe4 100644 --- a/spec/frontend/repository/components/last_commit_spec.js +++ b/spec/frontend/repository/components/last_commit_spec.js @@ -12,13 +12,11 @@ function createCommitData(data = {}) { titleHtml: 'Commit title', message: 'Commit message', webUrl: 'https://test.com/commit/123', - webPath: '/commit/123', authoredDate: '2019-01-01', author: { name: 'Test', avatarUrl: 'https://test.com', webUrl: 'https://test.com/test', - webPath: '/test', }, pipeline: { detailedStatus: { diff --git a/spec/frontend/repository/components/preview/__snapshots__/index_spec.js.snap b/spec/frontend/repository/components/preview/__snapshots__/index_spec.js.snap index 34cd8d67081..69b7a3931f8 100644 --- a/spec/frontend/repository/components/preview/__snapshots__/index_spec.js.snap +++ b/spec/frontend/repository/components/preview/__snapshots__/index_spec.js.snap @@ -16,7 +16,7 @@ exports[`Repository file preview component renders file HTML 1`] = ` /> <gl-link-stub - href="/test.md" + href="http://test.com" > <strong> README.md diff --git a/spec/frontend/repository/components/preview/index_spec.js b/spec/frontend/repository/components/preview/index_spec.js index 30b313d6142..6ae323f5c3f 100644 --- a/spec/frontend/repository/components/preview/index_spec.js +++ b/spec/frontend/repository/components/preview/index_spec.js @@ -31,7 +31,6 @@ describe('Repository file preview component', () => { it('renders file HTML', () => { factory({ webUrl: 'http://test.com', - webPath: '/test.md', name: 'README.md', }); @@ -45,7 +44,6 @@ describe('Repository file preview component', () => { it('handles hash after render', () => { factory({ webUrl: 'http://test.com', - webPath: '/test.md', name: 'README.md', }); @@ -62,7 +60,6 @@ describe('Repository file preview component', () => { it('renders loading icon', () => { factory({ webUrl: 'http://test.com', - webPath: '/test.md', name: 'README.md', }); diff --git a/spec/graphql/types/commit_type_spec.rb b/spec/graphql/types/commit_type_spec.rb index a0dbb32a677..75984786972 100644 --- a/spec/graphql/types/commit_type_spec.rb +++ b/spec/graphql/types/commit_type_spec.rb @@ -10,7 +10,7 @@ RSpec.describe GitlabSchema.types['Commit'] do it 'contains attributes related to commit' do expect(described_class).to have_graphql_fields( :id, :sha, :title, :description, :message, :title_html, :authored_date, - :author_name, :author_gravatar, :author, :web_path, :web_url, :latest_pipeline, + :author_name, :author_gravatar, :author, :web_url, :latest_pipeline, :pipelines, :signature_html ) end diff --git a/spec/graphql/types/notes/note_type_spec.rb b/spec/graphql/types/notes/note_type_spec.rb index 4f2452683e2..180d13d35d2 100644 --- a/spec/graphql/types/notes/note_type_spec.rb +++ b/spec/graphql/types/notes/note_type_spec.rb @@ -19,6 +19,7 @@ RSpec.describe GitlabSchema.types['Note'] do resolved_at resolved_by system + system_note_icon_name updated_at user_permissions ] diff --git a/spec/graphql/types/tree/blob_type_spec.rb b/spec/graphql/types/tree/blob_type_spec.rb index b018a8b6555..2c9089de3dd 100644 --- a/spec/graphql/types/tree/blob_type_spec.rb +++ b/spec/graphql/types/tree/blob_type_spec.rb @@ -5,5 +5,5 @@ require 'spec_helper' RSpec.describe Types::Tree::BlobType do specify { expect(described_class.graphql_name).to eq('Blob') } - specify { expect(described_class).to have_graphql_fields(:id, :sha, :name, :type, :path, :flat_path, :web_path, :web_url, :lfs_oid) } + specify { expect(described_class).to have_graphql_fields(:id, :sha, :name, :type, :path, :flat_path, :web_url, :lfs_oid) } end diff --git a/spec/graphql/types/tree/tree_entry_type_spec.rb b/spec/graphql/types/tree/tree_entry_type_spec.rb index 97dd09d77ee..0e5caf66854 100644 --- a/spec/graphql/types/tree/tree_entry_type_spec.rb +++ b/spec/graphql/types/tree/tree_entry_type_spec.rb @@ -5,5 +5,5 @@ require 'spec_helper' RSpec.describe Types::Tree::TreeEntryType do specify { expect(described_class.graphql_name).to eq('TreeEntry') } - specify { expect(described_class).to have_graphql_fields(:id, :sha, :name, :type, :path, :flat_path, :web_path, :web_url) } + specify { expect(described_class).to have_graphql_fields(:id, :sha, :name, :type, :path, :flat_path, :web_url) } end diff --git a/spec/graphql/types/user_type_spec.rb b/spec/graphql/types/user_type_spec.rb index 641d43e7dfd..6cc3f7bcaa1 100644 --- a/spec/graphql/types/user_type_spec.rb +++ b/spec/graphql/types/user_type_spec.rb @@ -16,7 +16,6 @@ RSpec.describe GitlabSchema.types['User'] do username avatarUrl webUrl - webPath todos state authoredMergeRequests diff --git a/spec/helpers/ci/pipeline_schedules_helper_spec.rb b/spec/helpers/ci/pipeline_schedules_helper_spec.rb new file mode 100644 index 00000000000..2a81c2a44a0 --- /dev/null +++ b/spec/helpers/ci/pipeline_schedules_helper_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::PipelineSchedulesHelper, :aggregate_failures do + describe '#timezone_data' do + subject { helper.timezone_data } + + it 'matches schema' do + expect(subject).not_to be_empty + subject.each_with_index do |timzone_hash, i| + expect(timzone_hash.keys).to contain_exactly(:name, :offset, :identifier), "Failed at index #{i}" + end + end + + it 'formats for display' do + first_timezone = ActiveSupport::TimeZone.all[0] + + expect(subject[0][:name]).to eq(first_timezone.name) + expect(subject[0][:offset]).to eq(first_timezone.now.utc_offset) + expect(subject[0][:identifier]).to eq(first_timezone.tzinfo.identifier) + end + end +end diff --git a/spec/helpers/environments_helper_spec.rb b/spec/helpers/environments_helper_spec.rb index 03428b43966..c083eb89cc0 100644 --- a/spec/helpers/environments_helper_spec.rb +++ b/spec/helpers/environments_helper_spec.rb @@ -23,6 +23,7 @@ RSpec.describe EnvironmentsHelper do 'metrics-dashboard-base-path' => environment_metrics_path(environment), 'current-environment-name' => environment.name, 'documentation-path' => help_page_path('administration/monitoring/prometheus/index.md'), + 'add-dashboard-documentation-path' => help_page_path('user/project/integrations/prometheus.md', anchor: 'adding-a-new-dashboard-to-your-project'), 'empty-getting-started-svg-path' => match_asset_path('/assets/illustrations/monitoring/getting_started.svg'), 'empty-loading-svg-path' => match_asset_path('/assets/illustrations/monitoring/loading.svg'), 'empty-no-data-svg-path' => match_asset_path('/assets/illustrations/monitoring/no_data.svg'), diff --git a/spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb b/spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb index f5fd2fd8d94..1350cba119b 100644 --- a/spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb +++ b/spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb @@ -9,8 +9,8 @@ RSpec.describe 'getting Alert Management Alert Notes' do let_it_be(:current_user) { create(:user) } let_it_be(:first_alert) { create(:alert_management_alert, project: project, assignees: [current_user]) } let_it_be(:second_alert) { create(:alert_management_alert, project: project) } - let_it_be(:first_system_note) { create(:note_on_alert, noteable: first_alert, project: project) } - let_it_be(:second_system_note) { create(:note_on_alert, noteable: first_alert, project: project) } + let_it_be(:first_system_note) { create(:note_on_alert, :with_system_note_metadata, noteable: first_alert, project: project) } + let_it_be(:second_system_note) { create(:note_on_alert, :with_system_note_metadata, noteable: first_alert, project: project) } let(:params) { {} } @@ -21,6 +21,8 @@ RSpec.describe 'getting Alert Management Alert Notes' do notes { nodes { id + body + systemNoteIconName } } } @@ -44,7 +46,17 @@ RSpec.describe 'getting Alert Management Alert Notes' do project.add_developer(current_user) end - it 'returns the notes ordered by createdAt' do + it 'includes expected data' do + post_graphql(query, current_user: current_user) + + expect(first_notes_result.first).to include( + 'id' => first_system_note.to_global_id.to_s, + 'systemNoteIconName' => 'git-merge', + 'body' => first_system_note.note + ) + end + + it 'returns the notes ordered by createdAt with sufficient content' do post_graphql(query, current_user: current_user) expect(first_notes_result.length).to eq(2) @@ -64,4 +76,18 @@ RSpec.describe 'getting Alert Management Alert Notes' do expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(base_count) expect(alerts_result.length).to eq(3) end + + context 'for non-system notes' do + let_it_be(:user_note) { create(:note_on_alert, noteable: second_alert, project: project) } + + it 'includes expected data' do + post_graphql(query, current_user: current_user) + + expect(second_notes_result.first).to include( + 'id' => user_note.to_global_id.to_s, + 'systemNoteIconName' => nil, + 'body' => user_note.note + ) + end + end end diff --git a/spec/services/projects/prometheus/alerts/create_events_service_spec.rb b/spec/services/projects/prometheus/alerts/create_events_service_spec.rb deleted file mode 100644 index c23f2d8c817..00000000000 --- a/spec/services/projects/prometheus/alerts/create_events_service_spec.rb +++ /dev/null @@ -1,312 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Projects::Prometheus::Alerts::CreateEventsService do - let(:user) { create(:user) } - let_it_be(:project) { create(:project) } - let(:metric) { create(:prometheus_metric, project: project) } - let(:service) { described_class.new(project, user, alerts_payload) } - - shared_examples 'events persisted' do |expected_count| - subject { service.execute } - - it 'returns proper amount of created events' do - expect(subject.size).to eq(expected_count) - end - - it 'increments event count' do - expect { subject }.to change { PrometheusAlertEvent.count }.to(expected_count) - end - end - - shared_examples 'no events persisted' do - subject { service.execute } - - it 'returns no created events' do - expect(subject).to be_empty - end - - it 'does not change event count' do - expect { subject }.not_to change { PrometheusAlertEvent.count } - end - end - - shared_examples 'self managed events persisted' do - subject { service.execute } - - it 'returns created events' do - expect(subject).not_to be_empty - end - - it 'does change self managed event count' do - expect { subject }.to change { SelfManagedPrometheusAlertEvent.count } - end - end - - context 'with valid alerts_payload' do - let!(:alert) { create(:prometheus_alert, prometheus_metric: metric, project: project) } - - let(:events) { service.execute } - - context 'with a firing payload' do - let(:started_at) { truncate_to_second(Time.current) } - let(:firing_event) { alert_payload(status: 'firing', started_at: started_at) } - let(:alerts_payload) { { 'alerts' => [firing_event] } } - - it_behaves_like 'events persisted', 1 - - it 'returns created event' do - event = events.first - - expect(event).to be_firing - expect(event.started_at).to eq(started_at) - expect(event.ended_at).to be_nil - end - - context 'with 2 different firing events' do - let(:another_firing_event) { alert_payload(status: 'firing', started_at: started_at + 1) } - let(:alerts_payload) { { 'alerts' => [firing_event, another_firing_event] } } - - it_behaves_like 'events persisted', 2 - end - - context 'with already persisted firing event' do - before do - service.execute - end - - it_behaves_like 'no events persisted' - end - - context 'with duplicate payload' do - let(:alerts_payload) { { 'alerts' => [firing_event, firing_event] } } - - it_behaves_like 'events persisted', 1 - end - end - - context 'with a resolved payload' do - let(:started_at) { truncate_to_second(Time.current) } - let(:ended_at) { started_at + 1 } - let(:resolved_event) { alert_payload(status: 'resolved', started_at: started_at, ended_at: ended_at) } - let(:alerts_payload) { { 'alerts' => [resolved_event] } } - let(:payload_key) { Gitlab::Alerting::Alert.new(project: project, payload: resolved_event).gitlab_fingerprint } - - context 'with a matching firing event' do - before do - create(:prometheus_alert_event, - prometheus_alert: alert, - payload_key: payload_key, - started_at: started_at) - end - - it 'does not create an additional event' do - expect { service.execute }.not_to change { PrometheusAlertEvent.count } - end - - it 'marks firing event as `resolved`' do - expect(events.size).to eq(1) - - event = events.first - expect(event).to be_resolved - expect(event.started_at).to eq(started_at) - expect(event.ended_at).to eq(ended_at) - end - - context 'with duplicate payload' do - let(:alerts_payload) { { 'alerts' => [resolved_event, resolved_event] } } - - it 'does not create an additional event' do - expect { service.execute }.not_to change { PrometheusAlertEvent.count } - end - - it 'marks firing event as `resolved` only once' do - expect(events.size).to eq(1) - end - end - end - - context 'without a matching firing event' do - context 'due to payload_key' do - let(:payload_key) { 'some other payload_key' } - - before do - create(:prometheus_alert_event, - prometheus_alert: alert, - payload_key: payload_key, - started_at: started_at) - end - - it_behaves_like 'no events persisted' - end - - context 'due to status' do - before do - create(:prometheus_alert_event, :resolved, - prometheus_alert: alert, - started_at: started_at) - end - - it_behaves_like 'no events persisted' - end - end - - context 'with already resolved event' do - before do - service.execute - end - - it_behaves_like 'no events persisted' - end - end - - context 'with a metric from another project' do - let(:another_project) { create(:project) } - let(:metric) { create(:prometheus_metric, project: another_project) } - let(:alerts_payload) { { 'alerts' => [alert_payload] } } - - let!(:alert) do - create(:prometheus_alert, - prometheus_metric: metric, - project: another_project) - end - - it_behaves_like 'no events persisted' - end - end - - context 'with invalid payload' do - let(:alert) { create(:prometheus_alert, prometheus_metric: metric, project: project) } - - describe '`alerts` key' do - context 'is missing' do - let(:alerts_payload) { {} } - - it_behaves_like 'no events persisted' - end - - context 'is nil' do - let(:alerts_payload) { { 'alerts' => nil } } - - it_behaves_like 'no events persisted' - end - - context 'is empty' do - let(:alerts_payload) { { 'alerts' => [] } } - - it_behaves_like 'no events persisted' - end - - context 'is not a Hash' do - let(:alerts_payload) { { 'alerts' => [:not_a_hash] } } - - it_behaves_like 'no events persisted' - end - - describe '`status`' do - context 'is missing' do - let(:alerts_payload) { { 'alerts' => [alert_payload(status: nil)] } } - - it_behaves_like 'no events persisted' - end - - context 'is invalid' do - let(:alerts_payload) { { 'alerts' => [alert_payload(status: 'invalid')] } } - - it_behaves_like 'no events persisted' - end - end - - describe '`started_at`' do - context 'is missing' do - let(:alerts_payload) { { 'alerts' => [alert_payload(started_at: nil)] } } - - it_behaves_like 'no events persisted' - end - - context 'is invalid' do - let(:alerts_payload) { { 'alerts' => [alert_payload(started_at: 'invalid date')] } } - - it_behaves_like 'no events persisted' - end - end - - describe '`ended_at`' do - context 'is missing and status is resolved' do - let(:alerts_payload) { { 'alerts' => [alert_payload(ended_at: nil, status: 'resolved')] } } - - it_behaves_like 'no events persisted' - end - - context 'is invalid and status is resolved' do - let(:alerts_payload) { { 'alerts' => [alert_payload(ended_at: 'invalid date', status: 'resolved')] } } - - it_behaves_like 'no events persisted' - end - end - - describe '`labels`' do - describe '`gitlab_alert_id`' do - context 'is missing' do - let(:alerts_payload) { { 'alerts' => [alert_payload(gitlab_alert_id: nil)] } } - - it_behaves_like 'no events persisted' - end - - context 'is missing but title is given' do - let(:alerts_payload) { { 'alerts' => [alert_payload(gitlab_alert_id: nil, title: 'alert')] } } - - it_behaves_like 'self managed events persisted' - end - - context 'is missing and environment name is given' do - let(:environment) { create(:environment, project: project) } - let(:alerts_payload) { { 'alerts' => [alert_payload(gitlab_alert_id: nil, title: 'alert', environment: environment.name)] } } - - it_behaves_like 'self managed events persisted' - - it 'associates the environment to the alert event' do - service.execute - - expect(SelfManagedPrometheusAlertEvent.last.environment).to eq environment - end - end - - context 'is invalid' do - let(:alerts_payload) { { 'alerts' => [alert_payload(gitlab_alert_id: '-1')] } } - - it_behaves_like 'no events persisted' - end - end - end - end - end - - private - - def alert_payload(status: 'firing', started_at: Time.current, ended_at: Time.current, gitlab_alert_id: alert.prometheus_metric_id, title: nil, environment: nil) - payload = {} - - payload['status'] = status if status - payload['startsAt'] = utc_rfc3339(started_at) if started_at - payload['endsAt'] = utc_rfc3339(ended_at) if ended_at - payload['labels'] = {} - payload['labels']['gitlab_alert_id'] = gitlab_alert_id.to_s if gitlab_alert_id - payload['labels']['alertname'] = title if title - payload['labels']['gitlab_environment_name'] = environment if environment - - payload - end - - # Example: 2018-09-27T18:25:31.079079416Z - def utc_rfc3339(date) - date.utc.rfc3339 - rescue - date - end - - def truncate_to_second(date) - date.change(usec: 0) - end -end diff --git a/spec/services/projects/prometheus/alerts/notify_service_spec.rb b/spec/services/projects/prometheus/alerts/notify_service_spec.rb index 557bf216277..aae257e3e3a 100644 --- a/spec/services/projects/prometheus/alerts/notify_service_spec.rb +++ b/spec/services/projects/prometheus/alerts/notify_service_spec.rb @@ -36,48 +36,8 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do end end - shared_examples 'processes incident issues' do |amount| - let(:create_incident_service) { spy } - - it 'processes issues' do - expect(IncidentManagement::ProcessPrometheusAlertWorker) - .to receive(:perform_async) - .with(project.id, kind_of(Hash)) - .exactly(amount).times - - Sidekiq::Testing.inline! do - expect(subject).to be_success - end - end - end - - shared_examples 'does not process incident issues' do - it 'does not process issues' do - expect(IncidentManagement::ProcessPrometheusAlertWorker) - .not_to receive(:perform_async) - - expect(subject).to be_success - end - end - - shared_examples 'persists events' do - let(:create_events_service) { spy } - - it 'persists events' do - expect(Projects::Prometheus::Alerts::CreateEventsService) - .to receive(:new) - .and_return(create_events_service) - - expect(create_events_service) - .to receive(:execute) - - expect(subject).to be_success - end - end - shared_examples 'notifies alerts' do it_behaves_like 'sends notification email' - it_behaves_like 'persists events' end shared_examples 'no notifications' do |http_status:| @@ -257,8 +217,6 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do context 'when incident_management_setting does not exist' do let!(:setting) { nil } - it_behaves_like 'persists events' - it 'does not send notification email', :sidekiq_might_not_need_inline do expect_any_instance_of(NotificationService) .not_to receive(:async) @@ -276,8 +234,6 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do create(:project_incident_management_setting, send_email: false, project: project) end - it_behaves_like 'persists events' - it 'does not send notification' do expect(NotificationService).not_to receive(:new) @@ -311,45 +267,6 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do end end end - - context 'process incident issues' do - before do - create(:prometheus_service, project: project) - create(:project_alerting_setting, project: project, token: token) - end - - context 'with create_issue setting enabled' do - before do - setting.update!(create_issue: true) - end - - it_behaves_like 'processes incident issues', 2 - - context 'multiple firing alerts' do - let(:payload_raw) do - prometheus_alert_payload(firing: [alert_firing, alert_firing], resolved: []) - end - - it_behaves_like 'processes incident issues', 2 - end - - context 'without firing alerts' do - let(:payload_raw) do - prometheus_alert_payload(firing: [], resolved: [alert_resolved]) - end - - it_behaves_like 'processes incident issues', 1 - end - end - - context 'with create_issue setting disabled' do - before do - setting.update!(create_issue: false) - end - - it_behaves_like 'does not process incident issues' - end - end end context 'with invalid payload' do @@ -380,13 +297,6 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do subject end - - it 'does not process issues' do - expect(IncidentManagement::ProcessPrometheusAlertWorker) - .not_to receive(:perform_async) - - subject - end end end diff --git a/spec/workers/incident_management/process_prometheus_alert_worker_spec.rb b/spec/workers/incident_management/process_prometheus_alert_worker_spec.rb index 7cd3d6b2494..c294892a66f 100644 --- a/spec/workers/incident_management/process_prometheus_alert_worker_spec.rb +++ b/spec/workers/incident_management/process_prometheus_alert_worker_spec.rb @@ -19,137 +19,9 @@ RSpec.describe IncidentManagement::ProcessPrometheusAlertWorker do }.with_indifferent_access end - it 'creates an issue' do + it 'does nothing' do expect { subject.perform(project.id, alert_params) } - .to change(Issue, :count) - .by(1) - end - - it 'relates issue to an event' do - expect { subject.perform(project.id, alert_params) } - .to change(prometheus_alert.related_issues, :count) - .from(0) - .to(1) - end - - context 'resolved event' do - let(:issue) { create(:issue, project: project) } - - before do - prometheus_alert_event.related_issues << issue - prometheus_alert_event.resolve - end - - it 'does not create an issue' do - expect { subject.perform(project.id, alert_params) } - .not_to change(Issue, :count) - end - - it 'closes the existing issue' do - expect { subject.perform(project.id, alert_params) } - .to change { issue.reload.state } - .from('opened') - .to('closed') - end - - it 'leaves a system note on the issue' do - expect(SystemNoteService) - .to receive(:auto_resolve_prometheus_alert) - - subject.perform(project.id, alert_params) - end - end - - context 'when project could not be found' do - let(:non_existing_project_id) { non_existing_record_id } - - it 'does not create an issue' do - expect { subject.perform(non_existing_project_id, alert_params) } - .not_to change(Issue, :count) - end - - it 'does not relate issue to an event' do - expect { subject.perform(non_existing_project_id, alert_params) } - .not_to change(prometheus_alert.related_issues, :count) - end - end - - context 'when event could not be found' do - before do - alert_params[:labels][:gitlab_alert_id] = non_existing_record_id - end - - it 'does not create an issue' do - expect { subject.perform(project.id, alert_params) } - .not_to change(Issue, :count) - end - - it 'does not relate issue to an event' do - expect { subject.perform(project.id, alert_params) } - .not_to change(prometheus_alert.related_issues, :count) - end - end - - context 'when issue could not be created' do - before do - allow_next_instance_of(IncidentManagement::CreateIssueService) do |instance| - allow(instance).to receive(:execute).and_return( { error: true } ) - end - end - - it 'does not relate issue to an event' do - expect { subject.perform(project.id, alert_params) } - .not_to change(prometheus_alert.related_issues, :count) - end - end - - context 'self-managed alert' do - let(:alert_name) { 'alert' } - let(:starts_at) { Time.now.rfc3339 } - - let!(:prometheus_alert_event) do - create(:self_managed_prometheus_alert_event, project: project, payload_key: payload_key) - end - - let(:alert_params) do - { - startsAt: starts_at, - generatorURL: 'http://localhost:9090/graph?g0.expr=vector%281%29&g0.tab=1', - labels: { - alertname: alert_name - } - }.with_indifferent_access - end - - it 'creates an issue' do - expect { subject.perform(project.id, alert_params) } - .to change(Issue, :count) - .by(1) - end - - it 'relates issue to an event' do - expect { subject.perform(project.id, alert_params) } - .to change(prometheus_alert_event.related_issues, :count) - .from(0) - .to(1) - end - - context 'when event could not be found' do - before do - alert_params[:generatorURL] = 'http://somethingelse.com' - end - - it 'creates an issue' do - expect { subject.perform(project.id, alert_params) } - .to change(Issue, :count) - .by(1) - end - - it 'does not relate issue to an event' do - expect { subject.perform(project.id, alert_params) } - .not_to change(prometheus_alert.related_issues, :count) - end - end + .not_to change(Issue, :count) end end end diff --git a/yarn.lock b/yarn.lock index 524b5692796..a7b2cfac381 100644 --- a/yarn.lock +++ b/yarn.lock @@ -848,10 +848,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.151.0.tgz#099905295d33eb31033f4a48eb3652da2f686239" integrity sha512-2PTSM8CFhUjeTFKfcq6E/YwPpOVdSVWupf3NhKO/bz/cisSBS5P7aWxaXKIaxy28ySyBKEfKaAT6b4rXTwvVgg== -"@gitlab/ui@17.18.1": - version "17.18.1" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-17.18.1.tgz#212b4560310919cc405a157da21a47b75981546c" - integrity sha512-1VRPg5YnDuEs7SiDdYrT2kkNUHJhbD0PobnME1QW2bjCFjgbVHc9SvKNq9cbb0ao/SAyCefG3iC/aKJsQVhUmQ== +"@gitlab/ui@17.19.0": + version "17.19.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-17.19.0.tgz#892e96375191f2937c48b393a4d0359f32541701" + integrity sha512-ysaOe/q5VJ/xM0G7sI9DX/mIzTBNppg6fH8R9D3Ste2shV0uwdZyaQmqsxUXFajuDvoLc2xrRBREYRIuDUPTqA== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.3.0" |