diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-31 09:08:16 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-31 09:08:16 +0000 |
commit | 6044caed20964a70c1ac6c5a3365d567ed96dfde (patch) | |
tree | 3fe8f14b4acbd542265544843efeb6f59b5d3efe | |
parent | 92077e0f8d70c70a908395808b16f98ecd3a5fcd (diff) | |
download | gitlab-ce-6044caed20964a70c1ac6c5a3365d567ed96dfde.tar.gz |
Add latest changes from gitlab-org/gitlab@master
49 files changed, 999 insertions, 436 deletions
diff --git a/app/assets/javascripts/behaviors/markdown/render_metrics.js b/app/assets/javascripts/behaviors/markdown/render_metrics.js index 8050604e6e7..9260a89bd52 100644 --- a/app/assets/javascripts/behaviors/markdown/render_metrics.js +++ b/app/assets/javascripts/behaviors/markdown/render_metrics.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import Metrics from '~/monitoring/components/embed.vue'; -import { createStore } from '~/monitoring/stores'; +import EmbedGroup from '~/monitoring/components/embeds/embed_group.vue'; +import { createStore } from '~/monitoring/stores/embed_group/'; // TODO: Handle copy-pasting - https://gitlab.com/gitlab-org/gitlab-foss/issues/64369. export default function renderMetrics(elements) { @@ -8,16 +8,36 @@ export default function renderMetrics(elements) { return; } + const EmbedGroupComponent = Vue.extend(EmbedGroup); + + const wrapperList = []; + elements.forEach(element => { - const { dashboardUrl } = element.dataset; - const MetricsComponent = Vue.extend(Metrics); + let wrapper; + const { previousElementSibling } = element; + const isFirstElementInGroup = !previousElementSibling?.urls; + + if (isFirstElementInGroup) { + wrapper = document.createElement('div'); + wrapper.urls = [element.dataset.dashboardUrl]; + element.parentNode.insertBefore(wrapper, element); + wrapperList.push(wrapper); + } else { + wrapper = previousElementSibling; + wrapper.urls.push(element.dataset.dashboardUrl); + } + + // Clean up processed element + element.parentNode.removeChild(element); + }); + wrapperList.forEach(wrapper => { // eslint-disable-next-line no-new - new MetricsComponent({ - el: element, + new EmbedGroupComponent({ + el: wrapper, store: createStore(), propsData: { - dashboardUrl, + urls: wrapper.urls, }, }); }); diff --git a/app/assets/javascripts/monitoring/components/embed.vue b/app/assets/javascripts/monitoring/components/embed.vue deleted file mode 100644 index 6182b570e76..00000000000 --- a/app/assets/javascripts/monitoring/components/embed.vue +++ /dev/null @@ -1,99 +0,0 @@ -<script> -import { mapActions, mapState, mapGetters } from 'vuex'; -import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue'; -import { convertToFixedRange } from '~/lib/utils/datetime_range'; -import { timeRangeFromUrl, removeTimeRangeParams } from '../utils'; -import { sidebarAnimationDuration } from '../constants'; -import { defaultTimeRange } from '~/vue_shared/constants'; - -let sidebarMutationObserver; - -export default { - components: { - PanelType, - }, - props: { - dashboardUrl: { - type: String, - required: true, - }, - }, - data() { - const timeRange = timeRangeFromUrl(this.dashboardUrl) || defaultTimeRange; - return { - timeRange: convertToFixedRange(timeRange), - elWidth: 0, - }; - }, - computed: { - ...mapState('monitoringDashboard', ['dashboard']), - ...mapGetters('monitoringDashboard', ['metricsWithData']), - charts() { - if (!this.dashboard || !this.dashboard.panelGroups) { - return []; - } - const groupWithMetrics = this.dashboard.panelGroups.find(group => - group.panels.find(chart => this.chartHasData(chart)), - ) || { panels: [] }; - - return groupWithMetrics.panels.filter(chart => this.chartHasData(chart)); - }, - isSingleChart() { - return this.charts.length === 1; - }, - }, - mounted() { - this.setInitialState(); - this.setTimeRange(this.timeRange); - this.fetchDashboard(); - - sidebarMutationObserver = new MutationObserver(this.onSidebarMutation); - sidebarMutationObserver.observe(document.querySelector('.layout-page'), { - attributes: true, - childList: false, - subtree: false, - }); - }, - beforeDestroy() { - if (sidebarMutationObserver) { - sidebarMutationObserver.disconnect(); - } - }, - methods: { - ...mapActions('monitoringDashboard', [ - 'setTimeRange', - 'fetchDashboard', - 'setEndpoints', - 'setFeatureFlags', - 'setShowErrorBanner', - ]), - chartHasData(chart) { - return chart.metrics.some(metric => this.metricsWithData().includes(metric.metricId)); - }, - onSidebarMutation() { - setTimeout(() => { - this.elWidth = this.$el.clientWidth; - }, sidebarAnimationDuration); - }, - setInitialState() { - this.setEndpoints({ - dashboardEndpoint: removeTimeRangeParams(this.dashboardUrl), - }); - this.setShowErrorBanner(false); - }, - }, -}; -</script> -<template> - <div class="metrics-embed" :class="{ 'd-inline-flex col-lg-6 p-0': isSingleChart }"> - <div v-if="charts.length" class="row w-100 m-n2 pb-4"> - <panel-type - v-for="(graphData, graphIndex) in charts" - :key="`panel-type-${graphIndex}`" - class="w-100" - :graph-data="graphData" - :group-id="dashboardUrl" - /> - </div> - </div> -</template> diff --git a/app/assets/javascripts/monitoring/components/embeds/embed_group.vue b/app/assets/javascripts/monitoring/components/embeds/embed_group.vue new file mode 100644 index 00000000000..b8562afe441 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/embeds/embed_group.vue @@ -0,0 +1,101 @@ +<script> +import { mapState, mapActions, mapGetters } from 'vuex'; +import sum from 'lodash/sum'; +import { GlButton, GlCard, GlIcon } from '@gitlab/ui'; +import { n__ } from '~/locale'; +import { monitoringDashboard } from '~/monitoring/stores'; +import MetricEmbed from './metric_embed.vue'; + +export default { + components: { + GlButton, + GlCard, + GlIcon, + MetricEmbed, + }, + props: { + urls: { + type: Array, + required: true, + validator: urls => urls.length > 0, + }, + }, + data() { + return { + isCollapsed: false, + }; + }, + computed: { + ...mapState('embedGroup', ['module']), + ...mapGetters('embedGroup', ['metricsWithData']), + arrowIconName() { + return this.isCollapsed ? 'chevron-right' : 'chevron-down'; + }, + bodyClass() { + return ['border-top', 'pl-3', 'pt-3', { 'd-none': this.isCollapsed }]; + }, + buttonLabel() { + return this.isCollapsed + ? n__('View chart', 'View charts', this.numCharts) + : n__('Hide chart', 'Hide charts', this.numCharts); + }, + containerClass() { + return this.isSingleChart ? 'col-lg-12' : 'col-lg-6'; + }, + numCharts() { + if (this.metricsWithData === null) { + return 0; + } + return sum(this.metricsWithData); + }, + isSingleChart() { + return this.numCharts === 1; + }, + }, + created() { + this.urls.forEach((url, index) => { + const name = this.getNamespace(index); + this.$store.registerModule(name, monitoringDashboard); + this.addModule(name); + }); + }, + methods: { + ...mapActions('embedGroup', ['addModule']), + getNamespace(id) { + return `monitoringDashboard/${id}`; + }, + toggleCollapsed() { + this.isCollapsed = !this.isCollapsed; + }, + }, +}; +</script> +<template> + <gl-card + v-show="numCharts > 0" + class="collapsible-card border p-0 mb-3" + header-class="d-flex align-items-center border-bottom-0 py-2" + :body-class="bodyClass" + > + <template #header> + <gl-button + class="collapsible-card-btn d-flex text-decoration-none" + :aria-label="buttonLabel" + variant="link" + @click="toggleCollapsed" + > + <gl-icon class="mr-1" :name="arrowIconName" /> + {{ buttonLabel }} + </gl-button> + </template> + <div class="d-flex flex-wrap"> + <metric-embed + v-for="(url, index) in urls" + :key="`${index}/${url}`" + :dashboard-url="url" + :namespace="getNamespace(index)" + :container-class="containerClass" + /> + </div> + </gl-card> +</template> diff --git a/app/assets/javascripts/monitoring/components/embeds/metric_embed.vue b/app/assets/javascripts/monitoring/components/embeds/metric_embed.vue new file mode 100644 index 00000000000..8a44e6bd737 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/embeds/metric_embed.vue @@ -0,0 +1,131 @@ +<script> +import { mapState, mapActions } from 'vuex'; +import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue'; +import { convertToFixedRange } from '~/lib/utils/datetime_range'; +import { defaultTimeRange } from '~/vue_shared/constants'; +import { timeRangeFromUrl, removeTimeRangeParams } from '../../utils'; +import { sidebarAnimationDuration } from '../../constants'; + +let sidebarMutationObserver; + +export default { + components: { + PanelType, + }, + props: { + containerClass: { + type: String, + required: false, + default: 'col-lg-12', + }, + dashboardUrl: { + type: String, + required: true, + }, + namespace: { + type: String, + required: false, + default: 'monitoringDashboard', + }, + }, + data() { + const timeRange = timeRangeFromUrl(this.dashboardUrl) || defaultTimeRange; + return { + timeRange: convertToFixedRange(timeRange), + elWidth: 0, + }; + }, + computed: { + ...mapState({ + dashboard(state) { + return state[this.namespace].dashboard; + }, + metricsWithData(state, getters) { + return getters[`${this.namespace}/metricsWithData`](); + }, + }), + charts() { + if (!this.dashboard || !this.dashboard.panelGroups) { + return []; + } + return this.dashboard.panelGroups.reduce( + (acc, currentGroup) => acc.concat(currentGroup.panels.filter(this.chartHasData)), + [], + ); + }, + isSingleChart() { + return this.charts.length === 1; + }, + embedClass() { + return this.isSingleChart ? this.containerClass : 'col-lg-12'; + }, + panelClass() { + return this.isSingleChart ? 'col-lg-12' : 'col-lg-6'; + }, + }, + mounted() { + this.setInitialState(); + this.setTimeRange(this.timeRange); + this.fetchDashboard(); + + sidebarMutationObserver = new MutationObserver(this.onSidebarMutation); + sidebarMutationObserver.observe(document.querySelector('.layout-page'), { + attributes: true, + childList: false, + subtree: false, + }); + }, + beforeDestroy() { + if (sidebarMutationObserver) { + sidebarMutationObserver.disconnect(); + } + }, + methods: { + // Use function args to support dynamic namespaces in mapXXX helpers. Pattern described + // in https://github.com/vuejs/vuex/issues/863#issuecomment-329510765 + ...mapActions({ + setTimeRange(dispatch, payload) { + return dispatch(`${this.namespace}/setTimeRange`, payload); + }, + fetchDashboard(dispatch, payload) { + return dispatch(`${this.namespace}/fetchDashboard`, payload); + }, + setEndpoints(dispatch, payload) { + return dispatch(`${this.namespace}/setEndpoints`, payload); + }, + setFeatureFlags(dispatch, payload) { + return dispatch(`${this.namespace}/setFeatureFlags`, payload); + }, + setShowErrorBanner(dispatch, payload) { + return dispatch(`${this.namespace}/setShowErrorBanner`, payload); + }, + }), + chartHasData(chart) { + return chart.metrics.some(metric => this.metricsWithData.includes(metric.metricId)); + }, + onSidebarMutation() { + setTimeout(() => { + this.elWidth = this.$el.clientWidth; + }, sidebarAnimationDuration); + }, + setInitialState() { + this.setEndpoints({ + dashboardEndpoint: removeTimeRangeParams(this.dashboardUrl), + }); + this.setShowErrorBanner(false); + }, + }, +}; +</script> +<template> + <div class="metrics-embed p-0 d-flex flex-wrap" :class="embedClass"> + <panel-type + v-for="(graphData, graphIndex) in charts" + :key="`panel-type-${graphIndex}`" + :class="panelClass" + :graph-data="graphData" + :group-id="dashboardUrl" + :namespace="namespace" + /> + </div> +</template> diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue index da305c7dda3..d6d60c2d5da 100644 --- a/app/assets/javascripts/monitoring/components/panel_type.vue +++ b/app/assets/javascripts/monitoring/components/panel_type.vue @@ -68,6 +68,11 @@ export default { required: false, default: 'panel-type-chart', }, + namespace: { + type: String, + required: false, + default: 'monitoringDashboard', + }, }, data() { return { @@ -76,7 +81,22 @@ export default { }; }, computed: { - ...mapState('monitoringDashboard', ['deploymentData', 'projectPath', 'logsPath', 'timeRange']), + // Use functions to support dynamic namespaces in mapXXX helpers. Pattern described + // in https://github.com/vuejs/vuex/issues/863#issuecomment-329510765 + ...mapState({ + deploymentData(state) { + return state[this.namespace].deploymentData; + }, + projectPath(state) { + return state[this.namespace].projectPath; + }, + logsPath(state) { + return state[this.namespace].logsPath; + }, + timeRange(state) { + return state[this.namespace].timeRange; + }, + }), title() { return this.graphData.title || ''; }, diff --git a/app/assets/javascripts/monitoring/stores/embed_group/actions.js b/app/assets/javascripts/monitoring/stores/embed_group/actions.js new file mode 100644 index 00000000000..cbe0950d954 --- /dev/null +++ b/app/assets/javascripts/monitoring/stores/embed_group/actions.js @@ -0,0 +1,5 @@ +import * as types from './mutation_types'; + +export const addModule = ({ commit }, data) => commit(types.ADD_MODULE, data); + +export default () => {}; diff --git a/app/assets/javascripts/monitoring/stores/embed_group/getters.js b/app/assets/javascripts/monitoring/stores/embed_group/getters.js new file mode 100644 index 00000000000..9b08cf762c1 --- /dev/null +++ b/app/assets/javascripts/monitoring/stores/embed_group/getters.js @@ -0,0 +1,4 @@ +export const metricsWithData = (state, getters, rootState, rootGetters) => + state.modules.map(module => rootGetters[`${module}/metricsWithData`]().length); + +export default () => {}; diff --git a/app/assets/javascripts/monitoring/stores/embed_group/index.js b/app/assets/javascripts/monitoring/stores/embed_group/index.js new file mode 100644 index 00000000000..773bca9f87e --- /dev/null +++ b/app/assets/javascripts/monitoring/stores/embed_group/index.js @@ -0,0 +1,24 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import * as actions from './actions'; +import * as getters from './getters'; +import mutations from './mutations'; +import state from './state'; + +Vue.use(Vuex); + +// In practice this store will have a number of `monitoringDashboard` modules added dynamically +export const createStore = () => + new Vuex.Store({ + modules: { + embedGroup: { + namespaced: true, + actions, + getters, + mutations, + state, + }, + }, + }); + +export default createStore(); diff --git a/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js b/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js new file mode 100644 index 00000000000..e7a425d3623 --- /dev/null +++ b/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js @@ -0,0 +1,3 @@ +export const ADD_MODULE = 'ADD_MODULE'; + +export default () => {}; diff --git a/app/assets/javascripts/monitoring/stores/embed_group/mutations.js b/app/assets/javascripts/monitoring/stores/embed_group/mutations.js new file mode 100644 index 00000000000..3c66129f239 --- /dev/null +++ b/app/assets/javascripts/monitoring/stores/embed_group/mutations.js @@ -0,0 +1,7 @@ +import * as types from './mutation_types'; + +export default { + [types.ADD_MODULE](state, module) { + state.modules.push(module); + }, +}; diff --git a/app/assets/javascripts/monitoring/stores/embed_group/state.js b/app/assets/javascripts/monitoring/stores/embed_group/state.js new file mode 100644 index 00000000000..016c7e5dac7 --- /dev/null +++ b/app/assets/javascripts/monitoring/stores/embed_group/state.js @@ -0,0 +1,3 @@ +export default () => ({ + modules: [], +}); diff --git a/app/assets/javascripts/monitoring/stores/index.js b/app/assets/javascripts/monitoring/stores/index.js index c1c466b7cf0..f08a6402aa6 100644 --- a/app/assets/javascripts/monitoring/stores/index.js +++ b/app/assets/javascripts/monitoring/stores/index.js @@ -7,16 +7,18 @@ import state from './state'; Vue.use(Vuex); +export const monitoringDashboard = { + namespaced: true, + actions, + getters, + mutations, + state, +}; + export const createStore = () => new Vuex.Store({ modules: { - monitoringDashboard: { - namespaced: true, - actions, - getters, - mutations, - state, - }, + monitoringDashboard, }, }); diff --git a/app/assets/stylesheets/components/collapsible_card.scss b/app/assets/stylesheets/components/collapsible_card.scss new file mode 100644 index 00000000000..c7c7423c1cd --- /dev/null +++ b/app/assets/stylesheets/components/collapsible_card.scss @@ -0,0 +1,9 @@ +.collapsible-card { + .collapsible-card-btn { + color: $gl-text-color; + + &:hover { + color: $blue-600; + } + } +} diff --git a/app/views/admin/application_settings/metrics_and_profiling.html.haml b/app/views/admin/application_settings/metrics_and_profiling.html.haml index 0b747082de0..6a703d0b70c 100644 --- a/app/views/admin/application_settings/metrics_and_profiling.html.haml +++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml @@ -60,4 +60,6 @@ .settings-content = render 'usage' += render_if_exists 'admin/application_settings/seat_link_setting', expanded: expanded_by_default? + = render_if_exists 'admin/application_settings/pseudonymizer_settings', expanded: expanded_by_default? diff --git a/changelogs/unreleased/211802-optimize-service_desk_enabled_projects-counter-in-usage_data.yml b/changelogs/unreleased/211802-optimize-service_desk_enabled_projects-counter-in-usage_data.yml new file mode 100644 index 00000000000..248d2af1236 --- /dev/null +++ b/changelogs/unreleased/211802-optimize-service_desk_enabled_projects-counter-in-usage_data.yml @@ -0,0 +1,5 @@ +--- +title: Optimize service desk enabled projects counter +merge_request: 27589 +author: +type: performance diff --git a/changelogs/unreleased/merge-auto-devops-beta-template-into-main.yml b/changelogs/unreleased/merge-auto-devops-beta-template-into-main.yml new file mode 100644 index 00000000000..8d29f37a2ba --- /dev/null +++ b/changelogs/unreleased/merge-auto-devops-beta-template-into-main.yml @@ -0,0 +1,5 @@ +--- +title: Add file-based pipeline conditions to default Auto DevOps CI template +merge_request: 28242 +author: +type: changed diff --git a/changelogs/unreleased/tr-remove-unfurled-chart.yml b/changelogs/unreleased/tr-remove-unfurled-chart.yml new file mode 100644 index 00000000000..a9584283c03 --- /dev/null +++ b/changelogs/unreleased/tr-remove-unfurled-chart.yml @@ -0,0 +1,5 @@ +--- +title: Allow embedded metrics charts to be hidden +merge_request: 23929 +author: +type: added diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index f31762d9ac6..54659ee9c8f 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -210,6 +210,7 @@ Gitlab.ee do Settings.gitlab['mirror_max_delay'] ||= 300 Settings.gitlab['mirror_max_capacity'] ||= 30 Settings.gitlab['mirror_capacity_threshold'] ||= 15 + Settings.gitlab['seat_link_enabled'] = true if Settings.gitlab['seat_link_enabled'].nil? end # diff --git a/db/migrate/20200324093258_add_index_on_id_creator_id_and_created_at_to_projects_table.rb b/db/migrate/20200324093258_add_index_on_id_creator_id_and_created_at_to_projects_table.rb new file mode 100644 index 00000000000..6c4d59af5d6 --- /dev/null +++ b/db/migrate/20200324093258_add_index_on_id_creator_id_and_created_at_to_projects_table.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AddIndexOnIdCreatorIdAndCreatedAtToProjectsTable < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + INDEX_NAME = 'index_service_desk_enabled_projects_on_id_creator_id_created_at' + + disable_ddl_transaction! + + def up + add_concurrent_index :projects, [:id, :creator_id, :created_at], where: '"projects"."service_desk_enabled" = TRUE', name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :projects, INDEX_NAME + end +end diff --git a/db/migrate/20200325152327_add_seat_link_enabled_to_application_settings.rb b/db/migrate/20200325152327_add_seat_link_enabled_to_application_settings.rb new file mode 100644 index 00000000000..52f579c175c --- /dev/null +++ b/db/migrate/20200325152327_add_seat_link_enabled_to_application_settings.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class AddSeatLinkEnabledToApplicationSettings < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default(:application_settings, :seat_link_enabled, + :boolean, + default: true, + allow_null: false) + end + + def down + remove_column(:application_settings, :seat_link_enabled) + end +end diff --git a/db/structure.sql b/db/structure.sql index 505050397da..6c807ccc5df 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -396,7 +396,8 @@ CREATE TABLE public.application_settings ( email_restrictions_enabled boolean DEFAULT false NOT NULL, email_restrictions text, npm_package_requests_forwarding boolean DEFAULT true NOT NULL, - namespace_storage_size_limit bigint DEFAULT 0 NOT NULL + namespace_storage_size_limit bigint DEFAULT 0 NOT NULL, + seat_link_enabled boolean DEFAULT true NOT NULL ); CREATE SEQUENCE public.application_settings_id_seq @@ -9924,6 +9925,8 @@ CREATE INDEX index_serverless_domain_cluster_on_creator_id ON public.serverless_ CREATE INDEX index_serverless_domain_cluster_on_pages_domain_id ON public.serverless_domain_cluster USING btree (pages_domain_id); +CREATE INDEX index_service_desk_enabled_projects_on_id_creator_id_created_at ON public.projects USING btree (id, creator_id, created_at) WHERE (service_desk_enabled = true); + CREATE INDEX index_services_on_project_id_and_type ON public.services USING btree (project_id, type); CREATE INDEX index_services_on_template ON public.services USING btree (template); @@ -12846,7 +12849,9 @@ COPY "schema_migrations" (version) FROM STDIN; 20200323080714 20200323122201 20200323134519 +20200324093258 20200324115359 +20200325152327 20200325160952 20200325183636 \. diff --git a/doc/administration/packages/container_registry.md b/doc/administration/packages/container_registry.md index 51c03f2edd0..a798c9527b0 100644 --- a/doc/administration/packages/container_registry.md +++ b/doc/administration/packages/container_registry.md @@ -646,6 +646,13 @@ NOTE: **Note:** The garbage collection tools are only available when you've installed GitLab via an Omnibus package or the cloud native chart. +DANGER: **Danger:** +By running the built-in garbage collection command, it will cause downtime to +the Container Registry. Running this command on an instance in an HA environment +while one of your other instances is still writing to the Registry storage, +will remove referenced manifests. To avoid that, make sure Registry is set to +[read-only mode](#performing-garbage-collection-without-downtime) before proceeding. + Container Registry can use considerable amounts of disk space. To clear up some unused layers, the registry includes a garbage collect command. @@ -695,13 +702,6 @@ built-in command: specify its path. - After the garbage collection is done, the registry should start up automatically. -DANGER: **Danger:** -By running the built-in garbage collection command, it will cause downtime to -the Container Registry. Running this command on an instance in an HA environment -while one of your other instances is still writing to the Registry storage, -will remove referenced manifests. To avoid that, make sure Registry is set to -[read-only mode](#performing-garbage-collection-without-downtime) before proceeding. - If you did not change the default location of the configuration file, run: ```sh diff --git a/doc/install/aws/img/associate_subnet_gateway.png b/doc/install/aws/img/associate_subnet_gateway.png Binary files differdeleted file mode 100644 index 1edca974fca..00000000000 --- a/doc/install/aws/img/associate_subnet_gateway.png +++ /dev/null diff --git a/doc/install/aws/img/associate_subnet_gateway_2.png b/doc/install/aws/img/associate_subnet_gateway_2.png Binary files differdeleted file mode 100644 index 6e10d9647b1..00000000000 --- a/doc/install/aws/img/associate_subnet_gateway_2.png +++ /dev/null diff --git a/doc/install/aws/index.md b/doc/install/aws/index.md index ed46876619d..cafc19690d2 100644 --- a/doc/install/aws/index.md +++ b/doc/install/aws/index.md @@ -135,16 +135,6 @@ Instances deployed in our private subnets need to connect to the internet for up Create a second NAT gateway but this time place it in the second public subnet, `gitlab-public-10.0.2.0`. -### Route Table - -Up to now all our subnets are private. We need to create a Route Table -to associate an Internet Gateway. On the same VPC dashboard: - -1. Select **Route Tables** from the left menu. -1. Click **Create Route Table**. -1. At the "Name tag" enter `gitlab-public` and choose `gitlab-vpc` under "VPC". -1. Hit **Yes, Create**. - ### Internet Gateway Now, still on the same dashboard, go to Internet Gateways and @@ -160,25 +150,44 @@ create a new one: 1. Choose `gitlab-vpc` from the list and hit **Attach**. -### Configuring subnets +### Route Tables + +#### Public Route Table + +We need to create a route table for our public subnets to reach the internet via the internet gateway we created in the previous step. -We now need to add a new target which will be our Internet Gateway and have +On the VPC dashboard: + +1. Select **Route Tables** from the left menu. +1. Click **Create Route Table**. +1. At the "Name tag" enter `gitlab-public` and choose `gitlab-vpc` under "VPC". +1. Click **Create**. + +We now need to add our internet gateway as a new target and have it receive traffic from any destination. 1. Select **Route Tables** from the left menu and select the `gitlab-public` route to show the options at the bottom. -1. Select the **Routes** tab, hit **Edit > Add another route** and set `0.0.0.0/0` - as destination. In the target, select the `gitlab-gateway` we created previously. - Hit **Save** once done. - - ![Associate subnet with gateway](img/associate_subnet_gateway.png) +1. Select the **Routes** tab, click **Edit routes > Add route** and set `0.0.0.0/0` + as the destination. In the target column, select the `gitlab-gateway` we created previously. + Hit **Save routes** once done. Next, we must associate the **public** subnets to the route table: -1. Select the **Subnet Associations** tab and hit **Edit**. -1. Check only the public subnet and hit **Save**. +1. Select the **Subnet Associations** tab and click **Edit subnet associations**. +1. Check only the public subnets and click **Save**. + +#### Private Route Tables + +We also need to create two private route tables so that instances in each private subnet can reach the internet via the NAT gateway in the corresponding public subnet in the same availability zone. - ![Associate subnet with gateway](img/associate_subnet_gateway_2.png) +1. Follow the same steps as above to create two private route tables. Name them `gitlab-public-a` and `gitlab-public-b` respectively. +1. Next, add a new route to each of the private route tables where the destination is `0.0.0.0/0` and the target is one of the NAT gateways we created earlier. + 1. Add the NAT gateway we created in `gitlab-public-10.0.0.0` as the target for the new route in the `gitlab-public-a` route table. + 1. Similarly, add the NAT gateway in `gitlab-public-10.0.2.0` as the target for the new route in the `gitlab-public-b`. +1. Lastly, associate each private subnet with a private route table. + 1. Associate `gitlab-private-10.0.1.0` with `gitlab-public-a`. + 1. Associate `gitlab-private-10.0.3.0` with `gitlab-public-b`. --- diff --git a/doc/subscriptions/index.md b/doc/subscriptions/index.md index 562422c8000..e3dd46af6bf 100644 --- a/doc/subscriptions/index.md +++ b/doc/subscriptions/index.md @@ -245,11 +245,12 @@ Seat Link allows us to provide our self-managed customers with prorated charges Seat Link sends to GitLab daily a count of all users in connected self-managed instances. That information is used to automate prorated reconciliations. The data is sent securely through an encrypted HTTPS connection. -Seat Link is mandatory because we need the user count data to enable prorated billing. Seat Link provides **only** the following information to GitLab: +Seat Link provides **only** the following information to GitLab: - Date - License key - Historical maximum user count +- Active users count For air-gapped or closed network customers, the existing [true-up model](#users-over-license) will be used. Prorated charges are not possible without user count data. @@ -293,12 +294,39 @@ TjJ4eVlVUkdkWEJtDQpkSHByYWpreVJrcG9UVlo0Y0hKSU9URndiV2RzVFdO VlhHNXRhVmszTkV0SVEzcEpNMWRyZEVoRU4ydHINCmRIRnFRVTlCVUVVM1pV SlRORE4xUjFaYVJGb3JlWGM5UFZ4dUlpd2lhWFlpt2lKV00yRnNVbk5RTjJk Sg0KU1hNMGExaE9SVGR2V2pKQlBUMWNiaUo5DQo=', - max_historical_user_count: 10 + max_historical_user_count: 10, + active_users: 6 } </code></pre> </details> +#### Disable Seat Link + +Seat Link is enabled by default. To disable this feature, go to +**{admin}** **Admin Area > Settings > Metrics and profiling** and +clear the Seat Link checkbox. + +To disable Seat Link in an Omnibus GitLab installation, and prevent it from +being configured in the future through the administration panel, set the following in +[`gitlab.rb`](https://docs.gitlab.com/omnibus/settings/configuration.html#configuration-options): + +```ruby +gitlab_rails['seat_link_enabled'] = false +``` + +To disable Seat Link in a GitLab source installation, and prevent it from +being configured in the future through the administration panel, +set the following in `gitlab.yml`: + +```yaml +production: &base + # ... + gitlab: + # ... + seat_link_enabled: false +``` + ### Renew or change a GitLab.com subscription To renew for more users than are currently active in your GitLab.com system, contact our sales team via `renewals@gitlab.com` for assistance as this can't be done in the Customers Portal. diff --git a/doc/user/project/integrations/img/embed_metrics.png b/doc/user/project/integrations/img/embed_metrics.png Binary files differdeleted file mode 100644 index 6f9660c9aec..00000000000 --- a/doc/user/project/integrations/img/embed_metrics.png +++ /dev/null diff --git a/doc/user/project/integrations/img/hide_embedded_metrics_v12_10.png b/doc/user/project/integrations/img/hide_embedded_metrics_v12_10.png Binary files differnew file mode 100644 index 00000000000..1213029d1d1 --- /dev/null +++ b/doc/user/project/integrations/img/hide_embedded_metrics_v12_10.png diff --git a/doc/user/project/integrations/img/view_embedded_metrics_v12_10.png b/doc/user/project/integrations/img/view_embedded_metrics_v12_10.png Binary files differnew file mode 100644 index 00000000000..95bb148ba71 --- /dev/null +++ b/doc/user/project/integrations/img/view_embedded_metrics_v12_10.png diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index 04be63de5ea..86f00cdcc1c 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -777,7 +777,11 @@ The following requirements must be met for the metric to unfurl: If all of the above are true, then the metric will unfurl as seen below: -![Embedded Metrics](img/embed_metrics.png) +![Embedded Metrics](img/view_embedded_metrics_v12_10.png) + +Metric charts may also be hidden: + +![Show Hide](img/hide_embedded_metrics_v12_10.png) ### Embedding metrics in issue templates diff --git a/lib/gitlab/ci/pipeline/chain/config/content/auto_devops.rb b/lib/gitlab/ci/pipeline/chain/config/content/auto_devops.rb index 8d19a73dfc3..4947e2eb879 100644 --- a/lib/gitlab/ci/pipeline/chain/config/content/auto_devops.rb +++ b/lib/gitlab/ci/pipeline/chain/config/content/auto_devops.rb @@ -23,15 +23,7 @@ module Gitlab private def template_name - if beta_enabled? - 'Beta/Auto-DevOps' - else - 'Auto-DevOps' - end - end - - def beta_enabled? - Feature.enabled?(:auto_devops_beta, project, default_enabled: true) + 'Auto-DevOps' end end end diff --git a/lib/gitlab/ci/pipeline/chain/config/content/legacy_auto_devops.rb b/lib/gitlab/ci/pipeline/chain/config/content/legacy_auto_devops.rb index c72b5f18424..5e4bb84360c 100644 --- a/lib/gitlab/ci/pipeline/chain/config/content/legacy_auto_devops.rb +++ b/lib/gitlab/ci/pipeline/chain/config/content/legacy_auto_devops.rb @@ -23,15 +23,7 @@ module Gitlab private def template_name - if beta_enabled? - 'Beta/Auto-DevOps' - else - 'Auto-DevOps' - end - end - - def beta_enabled? - Feature.enabled?(:auto_devops_beta, project, default_enabled: true) + 'Auto-DevOps' end end end diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml index 5a7642d24ee..a9f29bda9b9 100644 --- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @@ -72,6 +72,83 @@ stages: - performance - cleanup +workflow: + rules: + - if: '$BUILDPACK_URL || $AUTO_DEVOPS_EXPLICITLY_ENABLED == "1"' + + - exists: + - Dockerfile + + # https://github.com/heroku/heroku-buildpack-clojure + - exists: + - project.clj + + # https://github.com/heroku/heroku-buildpack-go + - exists: + - go.mod + - Gopkg.mod + - Godeps/Godeps.json + - vendor/vendor.json + - glide.yaml + - src/**/*.go + + # https://github.com/heroku/heroku-buildpack-gradle + - exists: + - gradlew + - build.gradle + - settings.gradle + + # https://github.com/heroku/heroku-buildpack-java + - exists: + - pom.xml + - pom.atom + - pom.clj + - pom.groovy + - pom.rb + - pom.scala + - pom.yaml + - pom.yml + + # https://github.com/heroku/heroku-buildpack-multi + - exists: + - .buildpacks + + # https://github.com/heroku/heroku-buildpack-nodejs + - exists: + - package.json + + # https://github.com/heroku/heroku-buildpack-php + - exists: + - composer.json + - index.php + + # https://github.com/heroku/heroku-buildpack-play + # TODO: detect script excludes some scala files + - exists: + - '**/conf/application.conf' + + # https://github.com/heroku/heroku-buildpack-python + # TODO: detect script checks that all of these exist, not any + - exists: + - requirements.txt + - setup.py + - Pipfile + + # https://github.com/heroku/heroku-buildpack-ruby + - exists: + - Gemfile + + # https://github.com/heroku/heroku-buildpack-scala + - exists: + - '*.sbt' + - project/*.scala + - .sbt/*.scala + - project/build.properties + + # https://github.com/dokku/buildpack-nginx + - exists: + - .static + include: - template: Jobs/Build.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml - template: Jobs/Test.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml diff --git a/lib/gitlab/ci/templates/Beta/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Beta/Auto-DevOps.gitlab-ci.yml deleted file mode 100644 index 2c5035705ac..00000000000 --- a/lib/gitlab/ci/templates/Beta/Auto-DevOps.gitlab-ci.yml +++ /dev/null @@ -1,163 +0,0 @@ -# Auto DevOps - BETA do not use -# This CI/CD configuration provides a standard pipeline for -# * building a Docker image (using a buildpack if necessary), -# * storing the image in the container registry, -# * running tests from a buildpack, -# * running code quality analysis, -# * creating a review app for each topic branch, -# * and continuous deployment to production -# -# Test jobs may be disabled by setting environment variables: -# * test: TEST_DISABLED -# * code_quality: CODE_QUALITY_DISABLED -# * license_management: LICENSE_MANAGEMENT_DISABLED -# * performance: PERFORMANCE_DISABLED -# * sast: SAST_DISABLED -# * dependency_scanning: DEPENDENCY_SCANNING_DISABLED -# * container_scanning: CONTAINER_SCANNING_DISABLED -# * dast: DAST_DISABLED -# * review: REVIEW_DISABLED -# * stop_review: REVIEW_DISABLED -# -# In order to deploy, you must have a Kubernetes cluster configured either -# via a project integration, or via group/project variables. -# KUBE_INGRESS_BASE_DOMAIN must also be set on the cluster settings, -# as a variable at the group or project level, or manually added below. -# -# Continuous deployment to production is enabled by default. -# If you want to deploy to staging first, set STAGING_ENABLED environment variable. -# If you want to enable incremental rollout, either manual or time based, -# set INCREMENTAL_ROLLOUT_MODE environment variable to "manual" or "timed". -# If you want to use canary deployments, set CANARY_ENABLED environment variable. -# -# If Auto DevOps fails to detect the proper buildpack, or if you want to -# specify a custom buildpack, set a project variable `BUILDPACK_URL` to the -# repository URL of the buildpack. -# e.g. BUILDPACK_URL=https://github.com/heroku/heroku-buildpack-ruby.git#v142 -# If you need multiple buildpacks, add a file to your project called -# `.buildpacks` that contains the URLs, one on each line, in order. -# Note: Auto CI does not work with multiple buildpacks yet - -image: alpine:latest - -variables: - # KUBE_INGRESS_BASE_DOMAIN is the application deployment domain and should be set as a variable at the group or project level. - # KUBE_INGRESS_BASE_DOMAIN: domain.example.com - - POSTGRES_USER: user - POSTGRES_PASSWORD: testing-password - POSTGRES_ENABLED: "true" - POSTGRES_DB: $CI_ENVIRONMENT_SLUG - POSTGRES_VERSION: 9.6.2 - - DOCKER_DRIVER: overlay2 - - ROLLOUT_RESOURCE_TYPE: deployment - - DOCKER_TLS_CERTDIR: "" # https://gitlab.com/gitlab-org/gitlab-runner/issues/4501 - -stages: - - build - - test - - deploy # dummy stage to follow the template guidelines - - review - - dast - - staging - - canary - - production - - incremental rollout 10% - - incremental rollout 25% - - incremental rollout 50% - - incremental rollout 100% - - performance - - cleanup - -workflow: - rules: - - if: '$BUILDPACK_URL || $AUTO_DEVOPS_EXPLICITLY_ENABLED == "1"' - - - exists: - - Dockerfile - - # https://github.com/heroku/heroku-buildpack-clojure - - exists: - - project.clj - - # https://github.com/heroku/heroku-buildpack-go - - exists: - - go.mod - - Gopkg.mod - - Godeps/Godeps.json - - vendor/vendor.json - - glide.yaml - - src/**/*.go - - # https://github.com/heroku/heroku-buildpack-gradle - - exists: - - gradlew - - build.gradle - - settings.gradle - - # https://github.com/heroku/heroku-buildpack-java - - exists: - - pom.xml - - pom.atom - - pom.clj - - pom.groovy - - pom.rb - - pom.scala - - pom.yaml - - pom.yml - - # https://github.com/heroku/heroku-buildpack-multi - - exists: - - .buildpacks - - # https://github.com/heroku/heroku-buildpack-nodejs - - exists: - - package.json - - # https://github.com/heroku/heroku-buildpack-php - - exists: - - composer.json - - index.php - - # https://github.com/heroku/heroku-buildpack-play - # TODO: detect script excludes some scala files - - exists: - - '**/conf/application.conf' - - # https://github.com/heroku/heroku-buildpack-python - # TODO: detect script checks that all of these exist, not any - - exists: - - requirements.txt - - setup.py - - Pipfile - - # https://github.com/heroku/heroku-buildpack-ruby - - exists: - - Gemfile - - # https://github.com/heroku/heroku-buildpack-scala - - exists: - - '*.sbt' - - project/*.scala - - .sbt/*.scala - - project/build.properties - - # https://github.com/dokku/buildpack-nginx - - exists: - - .static - -include: - - template: Jobs/Build.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml - - template: Jobs/Test.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml - - template: Jobs/Code-Quality.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml - - template: Jobs/Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml - - template: Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml - - template: Jobs/Browser-Performance-Testing.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml - - template: Security/DAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml - - template: Security/Container-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml - - template: Security/Dependency-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml - - template: Security/License-Management.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml - - template: Security/SAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml diff --git a/lib/gitlab/database/batch_count.rb b/lib/gitlab/database/batch_count.rb index 8972b77abef..e0f6b0f9eee 100644 --- a/lib/gitlab/database/batch_count.rb +++ b/lib/gitlab/database/batch_count.rb @@ -4,12 +4,18 @@ # Implements a distinct and ordinary batch counter # Needs indexes on the column below to calculate max, min and range queries # For larger tables just set use higher batch_size with index optimization +# +# In order to not use a possible complex time consuming query when calculating min and max for batch_distinct_count +# the start and finish can be sent specifically +# # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22705 +# # Examples: # extend ::Gitlab::Database::BatchCount # batch_count(User.active) # batch_count(::Clusters::Cluster.aws_installed.enabled, :cluster_id) # batch_distinct_count(::Project, :creator_id) +# batch_distinct_count(::Project.with_active_services.service_desk_enabled.where(time_period), start: ::User.minimum(:id), finish: ::User.maximum(:id)) module Gitlab module Database module BatchCount @@ -17,8 +23,8 @@ module Gitlab BatchCounter.new(relation, column: column).count(batch_size: batch_size) end - def batch_distinct_count(relation, column = nil, batch_size: nil) - BatchCounter.new(relation, column: column).count(mode: :distinct, batch_size: batch_size) + def batch_distinct_count(relation, column = nil, batch_size: nil, start: nil, finish: nil) + BatchCounter.new(relation, column: column).count(mode: :distinct, batch_size: batch_size, start: start, finish: finish) end class << self @@ -31,9 +37,10 @@ module Gitlab MIN_REQUIRED_BATCH_SIZE = 1_250 MAX_ALLOWED_LOOPS = 10_000 SLEEP_TIME_IN_SECONDS = 0.01 # 10 msec sleep - # Each query should take <<500ms https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22705 - DEFAULT_DISTINCT_BATCH_SIZE = 10_000 - DEFAULT_BATCH_SIZE = 100_000 + + # Each query should take < 500ms https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22705 + DEFAULT_DISTINCT_BATCH_SIZE = 100_000 + DEFAULT_BATCH_SIZE = 10_000 def initialize(relation, column: nil) @relation = relation @@ -46,15 +53,15 @@ module Gitlab start > finish end - def count(batch_size: nil, mode: :itself) + def count(batch_size: nil, mode: :itself, start: nil, finish: nil) raise 'BatchCount can not be run inside a transaction' if ActiveRecord::Base.connection.transaction_open? raise "The mode #{mode.inspect} is not supported" unless [:itself, :distinct].include?(mode) # non-distinct have better performance batch_size ||= mode == :distinct ? DEFAULT_DISTINCT_BATCH_SIZE : DEFAULT_BATCH_SIZE - start = @relation.minimum(@column) || 0 - finish = @relation.maximum(@column) || 0 + start = actual_start(start) + finish = actual_finish(finish) raise "Batch counting expects positive values only for #{@column}" if start < 0 || finish < 0 return FALLBACK if unwanted_configuration?(finish, batch_size, start) @@ -84,6 +91,16 @@ module Gitlab # rubocop:disable GitlabSecurity/PublicSend @relation.select(@column).public_send(mode).where(@column => start..(finish - 1)).count end + + private + + def actual_start(start) + start || @relation.minimum(@column) || 0 + end + + def actual_finish(finish) + finish || @relation.maximum(@column) || 0 + end end end end diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 8c0da1ba999..c131566380e 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -240,9 +240,9 @@ module Gitlab fallback end - def distinct_count(relation, column = nil, fallback: -1, batch: true) + def distinct_count(relation, column = nil, fallback: -1, batch: true, start: nil, finish: nil) if batch && Feature.enabled?(:usage_ping_batch_counter, default_enabled: true) - Gitlab::Database::BatchCount.batch_distinct_count(relation, column) + Gitlab::Database::BatchCount.batch_distinct_count(relation, column, start: start, finish: finish) else relation.distinct_count_by(column) end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 4f713893a9c..3dda347772b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -339,6 +339,9 @@ msgstr "" msgid "%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}." msgstr "" +msgid "%{link_start}Learn more%{link_end} about what information is shared with GitLab Inc." +msgstr "" + msgid "%{link_start}Read more%{link_end} about role permissions" msgstr "" @@ -7458,6 +7461,9 @@ msgstr "" msgid "Enable SAML authentication for this group" msgstr "" +msgid "Enable Seat Link" +msgstr "" + msgid "Enable access to Grafana" msgstr "" @@ -7500,6 +7506,9 @@ msgstr "" msgid "Enable mirror configuration" msgstr "" +msgid "Enable or disable Seat Link." +msgstr "" + msgid "Enable or disable keyboard shortcuts" msgstr "" @@ -9012,6 +9021,9 @@ msgstr "" msgid "For more information, see the documentation on %{deactivating_usage_ping_link_start}deactivating the usage ping%{deactivating_usage_ping_link_end}." msgstr "" +msgid "For more information, see the documentation on %{link_start}disabling Seat Link%{link_end}." +msgstr "" + msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)" msgstr "" @@ -10488,6 +10500,11 @@ msgstr "" msgid "Hide archived projects" msgstr "" +msgid "Hide chart" +msgid_plural "Hide charts" +msgstr[0] "" +msgstr[1] "" + msgid "Hide file browser" msgstr "" @@ -17543,6 +17560,12 @@ msgid_plural "SearchResults|wiki results" msgstr[0] "" msgstr[1] "" +msgid "Seat Link" +msgstr "" + +msgid "Seat Link is disabled, and cannot be configured through this form." +msgstr "" + msgid "Seats currently in use" msgstr "" @@ -21101,6 +21124,9 @@ msgstr "" msgid "To set up this service:" msgstr "" +msgid "To simplify the billing process, GitLab will collect user counts in order to prorate charges for user growth throughout the year using a quarterly reconciliation process." +msgstr "" + msgid "To specify the notification level per project of a group you belong to, you need to visit project page and change notification level there." msgstr "" @@ -22340,6 +22366,11 @@ msgstr "" msgid "View blame prior to this change" msgstr "" +msgid "View chart" +msgid_plural "View charts" +msgstr[0] "" +msgstr[1] "" + msgid "View dependency details for your project" msgstr "" diff --git a/spec/frontend/behaviors/markdown/render_metrics_spec.js b/spec/frontend/behaviors/markdown/render_metrics_spec.js index 9157d2a8448..3f7beeb817b 100644 --- a/spec/frontend/behaviors/markdown/render_metrics_spec.js +++ b/spec/frontend/behaviors/markdown/render_metrics_spec.js @@ -1,37 +1,49 @@ -import Vue from 'vue'; import { TEST_HOST } from 'helpers/test_constants'; import renderMetrics from '~/behaviors/markdown/render_metrics'; -const originalExtend = Vue.extend; +const mockEmbedGroup = jest.fn(); -describe('Render metrics for Gitlab Flavoured Markdown', () => { - const container = { - Metrics() {}, - }; - - let spyExtend; - - beforeEach(() => { - Vue.extend = () => container.Metrics; - spyExtend = jest.spyOn(Vue, 'extend'); - }); +jest.mock('vue', () => ({ extend: () => mockEmbedGroup })); +jest.mock('~/monitoring/components/embeds/embed_group.vue', () => jest.fn()); +jest.mock('~/monitoring/stores/embed_group/', () => ({ createStore: jest.fn() })); - afterEach(() => { - Vue.extend = originalExtend; - }); +const getElements = () => Array.from(document.getElementsByClassName('js-render-metrics')); +describe('Render metrics for Gitlab Flavoured Markdown', () => { it('does nothing when no elements are found', () => { renderMetrics([]); - expect(spyExtend).not.toHaveBeenCalled(); + expect(mockEmbedGroup).not.toHaveBeenCalled(); }); it('renders a vue component when elements are found', () => { - const element = document.createElement('div'); - element.setAttribute('data-dashboard-url', TEST_HOST); + document.body.innerHTML = `<div class="js-render-metrics" data-dashboard-url="${TEST_HOST}"></div>`; - renderMetrics([element]); + renderMetrics(getElements()); + + expect(mockEmbedGroup).toHaveBeenCalledTimes(1); + expect(mockEmbedGroup).toHaveBeenCalledWith( + expect.objectContaining({ propsData: { urls: [`${TEST_HOST}`] } }), + ); + }); - expect(spyExtend).toHaveBeenCalled(); + it('takes sibling metrics and groups them under a shared parent', () => { + document.body.innerHTML = ` + <p><span>Hello</span></p> + <div class="js-render-metrics" data-dashboard-url="${TEST_HOST}/1"></div> + <div class="js-render-metrics" data-dashboard-url="${TEST_HOST}/2"></div> + <p><span>Hello</span></p> + <div class="js-render-metrics" data-dashboard-url="${TEST_HOST}/3"></div> + `; + + renderMetrics(getElements()); + + expect(mockEmbedGroup).toHaveBeenCalledTimes(2); + expect(mockEmbedGroup).toHaveBeenCalledWith( + expect.objectContaining({ propsData: { urls: [`${TEST_HOST}/1`, `${TEST_HOST}/2`] } }), + ); + expect(mockEmbedGroup).toHaveBeenCalledWith( + expect.objectContaining({ propsData: { urls: [`${TEST_HOST}/3`] } }), + ); }); }); diff --git a/spec/frontend/monitoring/components/embeds/embed_group_spec.js b/spec/frontend/monitoring/components/embeds/embed_group_spec.js new file mode 100644 index 00000000000..54d21def603 --- /dev/null +++ b/spec/frontend/monitoring/components/embeds/embed_group_spec.js @@ -0,0 +1,163 @@ +import { createLocalVue, mount, shallowMount } from '@vue/test-utils'; +import Vuex from 'vuex'; +import { GlButton, GlCard } from '@gitlab/ui'; +import { TEST_HOST } from 'helpers/test_constants'; +import EmbedGroup from '~/monitoring/components/embeds/embed_group.vue'; +import MetricEmbed from '~/monitoring/components/embeds/metric_embed.vue'; +import { + addModuleAction, + initialEmbedGroupState, + singleEmbedProps, + dashboardEmbedProps, + multipleEmbedProps, +} from './mock_data'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('Embed Group', () => { + let wrapper; + let store; + const metricsWithDataGetter = jest.fn(); + + function mountComponent({ urls = [TEST_HOST], shallow = true, stubs } = {}) { + const mountMethod = shallow ? shallowMount : mount; + wrapper = mountMethod(EmbedGroup, { + localVue, + store, + propsData: { + urls, + }, + stubs, + }); + } + + beforeEach(() => { + store = new Vuex.Store({ + modules: { + embedGroup: { + namespaced: true, + actions: { addModule: jest.fn() }, + getters: { metricsWithData: metricsWithDataGetter }, + state: initialEmbedGroupState, + }, + }, + }); + store.registerModule = jest.fn(); + jest.spyOn(store, 'dispatch'); + }); + + afterEach(() => { + metricsWithDataGetter.mockReset(); + if (wrapper) { + wrapper.destroy(); + } + }); + + describe('interactivity', () => { + it('hides the component when no chart data is loaded', () => { + metricsWithDataGetter.mockReturnValue([]); + mountComponent(); + + expect(wrapper.find(GlCard).isVisible()).toBe(false); + }); + + it('shows the component when chart data is loaded', () => { + metricsWithDataGetter.mockReturnValue([1]); + mountComponent(); + + expect(wrapper.find(GlCard).isVisible()).toBe(true); + }); + + it('is expanded by default', () => { + metricsWithDataGetter.mockReturnValue([1]); + mountComponent({ shallow: false, stubs: { MetricEmbed: '<div />' } }); + + expect(wrapper.find('.card-body').classes()).not.toContain('d-none'); + }); + + it('collapses when clicked', done => { + metricsWithDataGetter.mockReturnValue([1]); + mountComponent({ shallow: false, stubs: { MetricEmbed: '<div />' } }); + + wrapper.find(GlButton).trigger('click'); + + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.card-body').classes()).toContain('d-none'); + done(); + }); + }); + }); + + describe('single metrics', () => { + beforeEach(() => { + metricsWithDataGetter.mockReturnValue([1]); + mountComponent(); + }); + + it('renders an Embed component', () => { + expect(wrapper.find(MetricEmbed).exists()).toBe(true); + }); + + it('passes the correct props to the Embed component', () => { + expect(wrapper.find(MetricEmbed).props()).toEqual(singleEmbedProps()); + }); + + it('adds the monitoring dashboard module', () => { + expect(store.dispatch).toHaveBeenCalledWith(addModuleAction, 'monitoringDashboard/0'); + }); + }); + + describe('dashboard metrics', () => { + beforeEach(() => { + metricsWithDataGetter.mockReturnValue([2]); + mountComponent(); + }); + + it('passes the correct props to the dashboard Embed component', () => { + expect(wrapper.find(MetricEmbed).props()).toEqual(dashboardEmbedProps()); + }); + + it('adds the monitoring dashboard module', () => { + expect(store.dispatch).toHaveBeenCalledWith(addModuleAction, 'monitoringDashboard/0'); + }); + }); + + describe('multiple metrics', () => { + beforeEach(() => { + metricsWithDataGetter.mockReturnValue([1, 1]); + mountComponent({ urls: [TEST_HOST, TEST_HOST] }); + }); + + it('creates Embed components', () => { + expect(wrapper.findAll(MetricEmbed)).toHaveLength(2); + }); + + it('passes the correct props to the Embed components', () => { + expect(wrapper.findAll(MetricEmbed).wrappers.map(item => item.props())).toEqual( + multipleEmbedProps(), + ); + }); + + it('adds multiple monitoring dashboard modules', () => { + expect(store.dispatch).toHaveBeenCalledWith(addModuleAction, 'monitoringDashboard/0'); + expect(store.dispatch).toHaveBeenCalledWith(addModuleAction, 'monitoringDashboard/1'); + }); + }); + + describe('button text', () => { + it('has a singular label when there is one embed', () => { + metricsWithDataGetter.mockReturnValue([1]); + mountComponent({ shallow: false, stubs: { MetricEmbed: '<div />' } }); + + expect(wrapper.find(GlButton).text()).toBe('Hide chart'); + }); + + it('has a plural label when there are multiple embeds', () => { + metricsWithDataGetter.mockReturnValue([2]); + mountComponent({ shallow: false, stubs: { MetricEmbed: '<div />' } }); + + expect(wrapper.find(GlButton).text()).toBe('Hide charts'); + }); + }); +}); diff --git a/spec/frontend/monitoring/embed/embed_spec.js b/spec/frontend/monitoring/components/embeds/metric_embed_spec.js index 850092c4a72..d0fe22cefec 100644 --- a/spec/frontend/monitoring/embed/embed_spec.js +++ b/spec/frontend/monitoring/components/embeds/metric_embed_spec.js @@ -2,20 +2,20 @@ import { createLocalVue, shallowMount } from '@vue/test-utils'; import Vuex from 'vuex'; import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue'; import { TEST_HOST } from 'helpers/test_constants'; -import Embed from '~/monitoring/components/embed.vue'; +import MetricEmbed from '~/monitoring/components/embeds/metric_embed.vue'; import { groups, initialState, metricsData, metricsWithData } from './mock_data'; const localVue = createLocalVue(); localVue.use(Vuex); -describe('Embed', () => { +describe('MetricEmbed', () => { let wrapper; let store; let actions; let metricsWithDataGetter; function mountComponent() { - wrapper = shallowMount(Embed, { + wrapper = shallowMount(MetricEmbed, { localVue, store, propsData: { diff --git a/spec/frontend/monitoring/embed/mock_data.js b/spec/frontend/monitoring/components/embeds/mock_data.js index da8eb8c0fc4..9cf66e52d22 100644 --- a/spec/frontend/monitoring/embed/mock_data.js +++ b/spec/frontend/monitoring/components/embeds/mock_data.js @@ -1,3 +1,5 @@ +import { TEST_HOST } from 'helpers/test_constants'; + export const metricsWithData = ['15_metric_a', '16_metric_b']; export const groups = [ @@ -52,3 +54,34 @@ export const initialState = () => ({ }, useDashboardEndpoint: true, }); + +export const initialEmbedGroupState = () => ({ + modules: [], +}); + +export const singleEmbedProps = () => ({ + dashboardUrl: TEST_HOST, + containerClass: 'col-lg-12', + namespace: 'monitoringDashboard/0', +}); + +export const dashboardEmbedProps = () => ({ + dashboardUrl: TEST_HOST, + containerClass: 'col-lg-6', + namespace: 'monitoringDashboard/0', +}); + +export const multipleEmbedProps = () => [ + { + dashboardUrl: TEST_HOST, + containerClass: 'col-lg-6', + namespace: 'monitoringDashboard/0', + }, + { + dashboardUrl: TEST_HOST, + containerClass: 'col-lg-6', + namespace: 'monitoringDashboard/1', + }, +]; + +export const addModuleAction = 'embedGroup/addModule'; diff --git a/spec/frontend/monitoring/components/panel_type_spec.js b/spec/frontend/monitoring/components/panel_type_spec.js index 927d93ab697..782a276a91b 100644 --- a/spec/frontend/monitoring/components/panel_type_spec.js +++ b/spec/frontend/monitoring/components/panel_type_spec.js @@ -8,8 +8,17 @@ import PanelType from '~/monitoring/components/panel_type.vue'; import EmptyChart from '~/monitoring/components/charts/empty_chart.vue'; import TimeSeriesChart from '~/monitoring/components/charts/time_series.vue'; import AnomalyChart from '~/monitoring/components/charts/anomaly.vue'; -import { anomalyMockGraphData, graphDataPrometheusQueryRange } from 'jest/monitoring/mock_data'; -import { createStore } from '~/monitoring/stores'; +import { + anomalyMockGraphData, + graphDataPrometheusQueryRange, + mockLogsHref, + mockLogsPath, + mockNamespace, + mockNamespacedData, + mockTimeRange, +} from 'jest/monitoring/mock_data'; +import { createStore, monitoringDashboard } from '~/monitoring/stores'; +import { createStore as createEmbedGroupStore } from '~/monitoring/stores/embed_group'; global.IS_EE = true; global.URL.createObjectURL = jest.fn(); @@ -29,6 +38,7 @@ describe('Panel Type component', () => { const exampleText = 'example_text'; const findCopyLink = () => wrapper.find({ ref: 'copyChartLink' }); + const findTimeChart = () => wrapper.find({ ref: 'timeChart' }); const createWrapper = props => { wrapper = shallowMount(PanelType, { @@ -99,8 +109,6 @@ describe('Panel Type component', () => { }); describe('when graph data is available', () => { - const findTimeChart = () => wrapper.find({ ref: 'timeChart' }); - beforeEach(() => { createWrapper({ graphData: graphDataPrometheusQueryRange, @@ -242,10 +250,6 @@ describe('Panel Type component', () => { }); describe('View Logs dropdown item', () => { - const mockLogsPath = '/path/to/logs'; - const mockTimeRange = { duration: { seconds: 120 } }; - - const findTimeChart = () => wrapper.find({ ref: 'timeChart' }); const findViewLogsLink = () => wrapper.find({ ref: 'viewLogsLink' }); beforeEach(() => { @@ -292,8 +296,7 @@ describe('Panel Type component', () => { state.timeRange = mockTimeRange; return wrapper.vm.$nextTick(() => { - const href = `${mockLogsPath}?duration_seconds=${mockTimeRange.duration.seconds}`; - expect(findViewLogsLink().attributes('href')).toMatch(href); + expect(findViewLogsLink().attributes('href')).toMatch(mockLogsHref); }); }); @@ -388,4 +391,53 @@ describe('Panel Type component', () => { }); }); }); + + describe('when using dynamic modules', () => { + const { mockDeploymentData, mockProjectPath } = mockNamespacedData; + + beforeEach(() => { + store = createEmbedGroupStore(); + store.registerModule(mockNamespace, monitoringDashboard); + store.state.embedGroup.modules.push(mockNamespace); + + wrapper = shallowMount(PanelType, { + propsData: { + graphData: graphDataPrometheusQueryRange, + namespace: mockNamespace, + }, + store, + mocks, + }); + }); + + it('handles namespaced time range and logs path state', () => { + store.state[mockNamespace].timeRange = mockTimeRange; + store.state[mockNamespace].logsPath = mockLogsPath; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.find({ ref: 'viewLogsLink' }).attributes().href).toBe(mockLogsHref); + }); + }); + + it('handles namespaced deployment data state', () => { + store.state[mockNamespace].deploymentData = mockDeploymentData; + + return wrapper.vm.$nextTick().then(() => { + expect(findTimeChart().props().deploymentData).toEqual(mockDeploymentData); + }); + }); + + it('handles namespaced project path state', () => { + store.state[mockNamespace].projectPath = mockProjectPath; + + return wrapper.vm.$nextTick().then(() => { + expect(findTimeChart().props().projectPath).toBe(mockProjectPath); + }); + }); + + it('it renders a time series chart with no errors', () => { + expect(wrapper.find(TimeSeriesChart).isVueInstance()).toBe(true); + expect(wrapper.find(TimeSeriesChart).exists()).toBe(true); + }); + }); }); diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js index 284b7a0997f..58693723624 100644 --- a/spec/frontend/monitoring/mock_data.js +++ b/spec/frontend/monitoring/mock_data.js @@ -750,3 +750,20 @@ export const barMockData = { }, ], }; + +export const baseNamespace = 'monitoringDashboard'; + +export const mockNamespace = `${baseNamespace}/1`; + +export const mockNamespaces = [`${baseNamespace}/1`, `${baseNamespace}/2`]; + +export const mockTimeRange = { duration: { seconds: 120 } }; + +export const mockNamespacedData = { + mockDeploymentData: ['mockDeploymentData'], + mockProjectPath: '/mockProjectPath', +}; + +export const mockLogsPath = '/mockLogsPath'; + +export const mockLogsHref = `${mockLogsPath}?duration_seconds=${mockTimeRange.duration.seconds}`; diff --git a/spec/frontend/monitoring/store/embed_group/actions_spec.js b/spec/frontend/monitoring/store/embed_group/actions_spec.js new file mode 100644 index 00000000000..5bdfc506cff --- /dev/null +++ b/spec/frontend/monitoring/store/embed_group/actions_spec.js @@ -0,0 +1,16 @@ +// import store from '~/monitoring/stores/embed_group'; +import * as actions from '~/monitoring/stores/embed_group/actions'; +import * as types from '~/monitoring/stores/embed_group/mutation_types'; +import { mockNamespace } from '../../mock_data'; + +describe('Embed group actions', () => { + describe('addModule', () => { + it('adds a module to the store', () => { + const commit = jest.fn(); + + actions.addModule({ commit }, mockNamespace); + + expect(commit).toHaveBeenCalledWith(types.ADD_MODULE, mockNamespace); + }); + }); +}); diff --git a/spec/frontend/monitoring/store/embed_group/getters_spec.js b/spec/frontend/monitoring/store/embed_group/getters_spec.js new file mode 100644 index 00000000000..e3241e41f5e --- /dev/null +++ b/spec/frontend/monitoring/store/embed_group/getters_spec.js @@ -0,0 +1,19 @@ +import { metricsWithData } from '~/monitoring/stores/embed_group/getters'; +import { mockNamespaces } from '../../mock_data'; + +describe('Embed group getters', () => { + describe('metricsWithData', () => { + it('correctly sums the number of metrics with data', () => { + const mockMetric = {}; + const state = { + modules: mockNamespaces, + }; + const rootGetters = { + [`${mockNamespaces[0]}/metricsWithData`]: () => [mockMetric], + [`${mockNamespaces[1]}/metricsWithData`]: () => [mockMetric, mockMetric], + }; + + expect(metricsWithData(state, null, null, rootGetters)).toEqual([1, 2]); + }); + }); +}); diff --git a/spec/frontend/monitoring/store/embed_group/mutations_spec.js b/spec/frontend/monitoring/store/embed_group/mutations_spec.js new file mode 100644 index 00000000000..a1d04e23e41 --- /dev/null +++ b/spec/frontend/monitoring/store/embed_group/mutations_spec.js @@ -0,0 +1,16 @@ +import state from '~/monitoring/stores/embed_group/state'; +import mutations from '~/monitoring/stores/embed_group/mutations'; +import * as types from '~/monitoring/stores/embed_group/mutation_types'; +import { mockNamespace } from '../../mock_data'; + +describe('Embed group mutations', () => { + describe('ADD_MODULE', () => { + it('should add a module', () => { + const stateCopy = state(); + + mutations[types.ADD_MODULE](stateCopy, mockNamespace); + + expect(stateCopy.modules).toEqual([mockNamespace]); + }); + }); +}); diff --git a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb index 4c4359ad5d2..b5f0783cb42 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb @@ -45,7 +45,7 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do subject.perform! expect(pipeline.config_source).to eq 'auto_devops_source' - template = Gitlab::Template::GitlabCiYmlTemplate.find('Beta/Auto-DevOps') + template = Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps') expect(command.config_content).to eq(template.content) end end @@ -78,7 +78,7 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do expect(pipeline.config_source).to eq 'auto_devops_source' expect(pipeline.pipeline_config).to be_nil - template = Gitlab::Template::GitlabCiYmlTemplate.find('Beta/Auto-DevOps') + template = Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps') expect(command.config_content).to eq(template.content) end end @@ -91,7 +91,7 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do expect(pipeline.config_source).to eq 'auto_devops_source' expect(pipeline.pipeline_config).to be_nil - template = Gitlab::Template::GitlabCiYmlTemplate.find('Beta/Auto-DevOps') + template = Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps') expect(command.config_content).to eq(template.content) end end @@ -122,34 +122,13 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do expect(project).to receive(:auto_devops_enabled?).and_return(true) end - context 'when beta is enabled' do - before do - stub_feature_flags(auto_devops_beta: true) - end - - it 'returns the content of AutoDevops template' do - subject.perform! - - expect(pipeline.config_source).to eq 'auto_devops_source' - expect(pipeline.pipeline_config).to be_nil - template = Gitlab::Template::GitlabCiYmlTemplate.find('Beta/Auto-DevOps') - expect(command.config_content).to eq(template.content) - end - end - - context 'when beta is disabled' do - before do - stub_feature_flags(auto_devops_beta: false) - end - - it 'returns the content of AutoDevops template' do - subject.perform! + it 'returns the content of AutoDevops template' do + subject.perform! - expect(pipeline.config_source).to eq 'auto_devops_source' - expect(pipeline.pipeline_config).to be_nil - template = Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps') - expect(command.config_content).to eq(template.content) - end + expect(pipeline.config_source).to eq 'auto_devops_source' + expect(pipeline.pipeline_config).to be_nil + template = Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps') + expect(command.config_content).to eq(template.content) end end @@ -285,7 +264,7 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do <<~EOY --- include: - - template: Beta/Auto-DevOps.gitlab-ci.yml + - template: Auto-DevOps.gitlab-ci.yml EOY end @@ -293,40 +272,12 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do expect(project).to receive(:auto_devops_enabled?).and_return(true) end - context 'when beta is enabled' do - before do - stub_feature_flags(auto_devops_beta: true) - end - - it 'builds root config including the auto-devops template' do - subject.perform! - - expect(pipeline.config_source).to eq 'auto_devops_source' - expect(pipeline.pipeline_config.content).to eq(config_content_result) - expect(command.config_content).to eq(config_content_result) - end - end - - context 'when beta is disabled' do - before do - stub_feature_flags(auto_devops_beta: false) - end - - let(:config_content_result) do - <<~EOY - --- - include: - - template: Auto-DevOps.gitlab-ci.yml - EOY - end - - it 'builds root config including the auto-devops template' do - subject.perform! + it 'builds root config including the auto-devops template' do + subject.perform! - expect(pipeline.config_source).to eq 'auto_devops_source' - expect(pipeline.pipeline_config.content).to eq(config_content_result) - expect(command.config_content).to eq(config_content_result) - end + expect(pipeline.config_source).to eq 'auto_devops_source' + expect(pipeline.pipeline_config.content).to eq(config_content_result) + expect(command.config_content).to eq(config_content_result) end end diff --git a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb index 12600d97b2f..0c5d172f17c 100644 --- a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb @@ -133,8 +133,6 @@ describe 'Auto-DevOps.gitlab-ci.yml' do end with_them do - subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Beta/Auto-DevOps') } - let(:user) { create(:admin) } let(:project) { create(:project, :custom_repo, files: files) } let(:service) { Ci::CreatePipelineService.new(project, user, ref: 'master' ) } diff --git a/spec/lib/gitlab/database/batch_count_spec.rb b/spec/lib/gitlab/database/batch_count_spec.rb index 0731791d9b0..b126d8579fc 100644 --- a/spec/lib/gitlab/database/batch_count_spec.rb +++ b/spec/lib/gitlab/database/batch_count_spec.rb @@ -90,5 +90,13 @@ describe Gitlab::Database::BatchCount do [1, 2, 4, 5, 6].each { |i| expect(described_class.batch_distinct_count(model, column, batch_size: i)).to eq(2) } end + + it 'counts with a start and finish' do + expect(described_class.batch_distinct_count(model, column, start: model.minimum(column), finish: model.maximum(column))).to eq(2) + end + + it 'counts with User min and max as start and finish' do + expect(described_class.batch_distinct_count(model, column, start: User.minimum(:id), finish: User.maximum(:id))).to eq(2) + end end end |