diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-08 09:09:43 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-08 09:09:43 +0000 |
commit | f5050253469fc0961c02deec0e698ad62bdd9de5 (patch) | |
tree | 30bbd8f8b556fd5b730f0123921138ee1d6bdaa2 | |
parent | f6cdec670b9b757fc2225a2c6627ab79765e5b8a (diff) | |
download | gitlab-ce-f5050253469fc0961c02deec0e698ad62bdd9de5.tar.gz |
Add latest changes from gitlab-org/gitlab@master
71 files changed, 1296 insertions, 393 deletions
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 3553ab14f29..66a025a8fe7 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -269,14 +269,6 @@ RSpec/ScatteredSetup: - 'spec/requests/api/jobs_spec.rb' - 'spec/services/projects/create_service_spec.rb' -# Offense count: 4 -RSpec/VoidExpect: - Exclude: - - 'spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb' - - 'spec/models/ci/group_spec.rb' - - 'spec/models/ci/runner_spec.rb' - - 'spec/services/users/destroy_service_spec.rb' - # Offense count: 10 # Cop supports --auto-correct. Rails/ApplicationController: @@ -457,9 +457,9 @@ end # Gitaly GRPC protocol definitions gem 'gitaly', '~> 12.9.0.pre.rc4' -gem 'grpc', '~> 1.27.0' +gem 'grpc', '~> 1.24.0' -gem 'google-protobuf', '~> 3.11.2' +gem 'google-protobuf', '~> 3.8.0' gem 'toml-rb', '~> 1.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index 1f978df8d31..c65e2252ce6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -427,7 +427,7 @@ GEM mime-types (~> 3.0) representable (~> 3.0) retriable (>= 2.0, < 4.0) - google-protobuf (3.11.4) + google-protobuf (3.8.0) googleapis-common-protos-types (1.0.4) google-protobuf (~> 3.0) googleauth (0.6.6) @@ -468,8 +468,8 @@ GEM graphql (~> 1.6) html-pipeline (~> 2.8) sass (~> 3.4) - grpc (1.27.0) - google-protobuf (~> 3.11) + grpc (1.24.0) + google-protobuf (~> 3.8) googleapis-common-protos-types (~> 1.0) gssapi (1.2.0) ffi (>= 1.0.1) @@ -1251,7 +1251,7 @@ DEPENDENCIES gitlab_omniauth-ldap (~> 2.1.1) gon (~> 6.2) google-api-client (~> 0.23) - google-protobuf (~> 3.11.2) + google-protobuf (~> 3.8.0) gpgme (~> 2.0.19) grape (~> 1.1.0) grape-entity (~> 0.7.1) @@ -1260,7 +1260,7 @@ DEPENDENCIES graphiql-rails (~> 1.4.10) graphql (~> 1.10.5) graphql-docs (~> 1.6.0) - grpc (~> 1.27.0) + grpc (~> 1.24.0) gssapi guard-rspec haml_lint (~> 0.34.0) diff --git a/app/assets/javascripts/ide/components/external_link.vue b/app/assets/javascripts/ide/components/external_link.vue deleted file mode 100644 index 558da9b706e..00000000000 --- a/app/assets/javascripts/ide/components/external_link.vue +++ /dev/null @@ -1,34 +0,0 @@ -<script> -import Icon from '~/vue_shared/components/icon.vue'; - -export default { - components: { - Icon, - }, - props: { - file: { - type: Object, - required: true, - }, - }, - computed: { - showButtons() { - return this.file.permalink; - }, - }, -}; -</script> - -<template> - <div v-if="showButtons" class="pull-right ide-btn-group"> - <a - :href="file.permalink" - :title="s__('IDE|Open in file view')" - target="_blank" - rel="noopener noreferrer" - > - <span class="vertical-align-middle">{{ __('Open in file view') }}</span> - <icon :size="16" name="external-link" class="vertical-align-middle space-right" /> - </a> - </div> -</template> diff --git a/app/assets/javascripts/logs/components/environment_logs.vue b/app/assets/javascripts/logs/components/environment_logs.vue index 70b3af8dc75..487b4f30b5b 100644 --- a/app/assets/javascripts/logs/components/environment_logs.vue +++ b/app/assets/javascripts/logs/components/environment_logs.vue @@ -56,7 +56,6 @@ export default { required: true, }, }, - traceHeight: 600, data() { return { isElasticStackCalloutDismissed: false, @@ -94,6 +93,9 @@ export default { 'showEnvironment', 'fetchEnvironments', 'fetchMoreLogsPrepend', + 'dismissRequestEnvironmentsError', + 'dismissInvalidTimeRangeWarning', + 'dismissRequestLogsError', ]), isCurrentEnvironment(envName) { @@ -115,7 +117,7 @@ export default { }; </script> <template> - <div class="environment-logs-viewer mt-3"> + <div class="environment-logs-viewer d-flex flex-column py-3"> <gl-alert v-if="shouldShowElasticStackCallout" class="mb-3 js-elasticsearch-alert" @@ -132,6 +134,31 @@ export default { </strong> </a> </gl-alert> + <gl-alert + v-if="environments.fetchError" + class="mb-3" + variant="danger" + @dismiss="dismissRequestEnvironmentsError" + > + {{ s__('Metrics|There was an error fetching the environments data, please try again') }} + </gl-alert> + <gl-alert + v-if="timeRange.invalidWarning" + class="mb-3" + variant="warning" + @dismiss="dismissInvalidTimeRangeWarning" + > + {{ s__('Metrics|Invalid time range, please verify.') }} + </gl-alert> + <gl-alert + v-if="logs.fetchError" + class="mb-3" + variant="danger" + @dismiss="dismissRequestLogsError" + > + {{ s__('Environments|There was an error fetching the logs. Please try again.') }} + </gl-alert> + <div class="top-bar d-md-flex border bg-secondary-50 pt-2 pr-1 pb-0 pl-2"> <div class="flex-grow-0"> <gl-dropdown @@ -183,16 +210,15 @@ export default { <gl-infinite-scroll ref="infiniteScroll" - class="log-lines" - :style="{ height: `${$options.traceHeight}px` }" - :max-list-height="$options.traceHeight" + class="log-lines overflow-auto flex-grow-1 min-height-0" :fetched-items="logs.lines.length" @topReached="topReached" @scroll="scroll" > <template #items> <pre - class="build-trace js-log-trace" + ref="logTrace" + class="build-trace" ><code class="bash js-build-output"><div v-if="showLoader" class="build-loader-animation js-build-loader-animation"> <div class="dot"></div> <div class="dot"></div> @@ -205,7 +231,7 @@ export default { ></template> </gl-infinite-scroll> - <div ref="logFooter" class="log-footer py-2 px-3"> + <div ref="logFooter" class="py-2 px-3 text-white bg-secondary-900"> <gl-sprintf :message="s__('Environments|Logs from %{start} to %{end}.')"> <template #start>{{ timeRange.current.start | formatDate }}</template> <template #end>{{ timeRange.current.end | formatDate }}</template> diff --git a/app/assets/javascripts/logs/stores/actions.js b/app/assets/javascripts/logs/stores/actions.js index 1e71b2c7314..be847108a49 100644 --- a/app/assets/javascripts/logs/stores/actions.js +++ b/app/assets/javascripts/logs/stores/actions.js @@ -1,20 +1,10 @@ import { backOff } from '~/lib/utils/common_utils'; import httpStatusCodes from '~/lib/utils/http_status'; import axios from '~/lib/utils/axios_utils'; -import flash from '~/flash'; -import { s__ } from '~/locale'; import { convertToFixedRange } from '~/lib/utils/datetime_range'; import * as types from './mutation_types'; -const flashTimeRangeWarning = () => { - flash(s__('Metrics|Invalid time range, please verify.'), 'warning'); -}; - -const flashLogsError = () => { - flash(s__('Metrics|There was an error fetching the logs, please try again')); -}; - const requestUntilData = (url, params) => backOff((next, stop) => { axios @@ -31,7 +21,7 @@ const requestUntilData = (url, params) => }); }); -const requestLogsUntilData = state => { +const requestLogsUntilData = ({ commit, state }) => { const params = {}; const { logs_api_path } = state.environments.options.find( ({ name }) => name === state.environments.current, @@ -49,7 +39,7 @@ const requestLogsUntilData = state => { params.start_time = start; params.end_time = end; } catch { - flashTimeRangeWarning(); + commit(types.SHOW_TIME_RANGE_INVALID_WARNING); } } if (state.logs.cursor) { @@ -101,26 +91,22 @@ export const fetchEnvironments = ({ commit, dispatch }, environmentsPath) => { }) .catch(() => { commit(types.RECEIVE_ENVIRONMENTS_DATA_ERROR); - flash(s__('Metrics|There was an error fetching the environments data, please try again')); }); }; export const fetchLogs = ({ commit, state }) => { commit(types.REQUEST_LOGS_DATA); - return requestLogsUntilData(state) + return requestLogsUntilData({ commit, state }) .then(({ data }) => { const { pod_name, pods, logs, cursor } = data; commit(types.RECEIVE_LOGS_DATA_SUCCESS, { logs, cursor }); - commit(types.SET_CURRENT_POD_NAME, pod_name); - commit(types.RECEIVE_PODS_DATA_SUCCESS, pods); }) .catch(() => { commit(types.RECEIVE_PODS_DATA_ERROR); commit(types.RECEIVE_LOGS_DATA_ERROR); - flashLogsError(); }); }; @@ -132,16 +118,27 @@ export const fetchMoreLogsPrepend = ({ commit, state }) => { commit(types.REQUEST_LOGS_DATA_PREPEND); - return requestLogsUntilData(state) + return requestLogsUntilData({ commit, state }) .then(({ data }) => { const { logs, cursor } = data; commit(types.RECEIVE_LOGS_DATA_PREPEND_SUCCESS, { logs, cursor }); }) .catch(() => { commit(types.RECEIVE_LOGS_DATA_PREPEND_ERROR); - flashLogsError(); }); }; +export const dismissRequestEnvironmentsError = ({ commit }) => { + commit(types.HIDE_REQUEST_ENVIRONMENTS_ERROR); +}; + +export const dismissRequestLogsError = ({ commit }) => { + commit(types.HIDE_REQUEST_LOGS_ERROR); +}; + +export const dismissInvalidTimeRangeWarning = ({ commit }) => { + commit(types.HIDE_TIME_RANGE_INVALID_WARNING); +}; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/logs/stores/mutation_types.js b/app/assets/javascripts/logs/stores/mutation_types.js index 7e7771a9df8..c1cc7eca52e 100644 --- a/app/assets/javascripts/logs/stores/mutation_types.js +++ b/app/assets/javascripts/logs/stores/mutation_types.js @@ -1,11 +1,16 @@ export const SET_PROJECT_ENVIRONMENT = 'SET_PROJECT_ENVIRONMENT'; export const SET_SEARCH = 'SET_SEARCH'; + export const SET_TIME_RANGE = 'SET_TIME_RANGE'; +export const SHOW_TIME_RANGE_INVALID_WARNING = 'SHOW_TIME_RANGE_INVALID_WARNING'; +export const HIDE_TIME_RANGE_INVALID_WARNING = 'HIDE_TIME_RANGE_INVALID_WARNING'; + export const SET_CURRENT_POD_NAME = 'SET_CURRENT_POD_NAME'; export const REQUEST_ENVIRONMENTS_DATA = 'REQUEST_ENVIRONMENTS_DATA'; export const RECEIVE_ENVIRONMENTS_DATA_SUCCESS = 'RECEIVE_ENVIRONMENTS_DATA_SUCCESS'; export const RECEIVE_ENVIRONMENTS_DATA_ERROR = 'RECEIVE_ENVIRONMENTS_DATA_ERROR'; +export const HIDE_REQUEST_ENVIRONMENTS_ERROR = 'HIDE_REQUEST_ENVIRONMENTS_ERROR'; export const REQUEST_LOGS_DATA = 'REQUEST_LOGS_DATA'; export const RECEIVE_LOGS_DATA_SUCCESS = 'RECEIVE_LOGS_DATA_SUCCESS'; @@ -13,6 +18,7 @@ export const RECEIVE_LOGS_DATA_ERROR = 'RECEIVE_LOGS_DATA_ERROR'; export const REQUEST_LOGS_DATA_PREPEND = 'REQUEST_LOGS_DATA_PREPEND'; export const RECEIVE_LOGS_DATA_PREPEND_SUCCESS = 'RECEIVE_LOGS_DATA_PREPEND_SUCCESS'; export const RECEIVE_LOGS_DATA_PREPEND_ERROR = 'RECEIVE_LOGS_DATA_PREPEND_ERROR'; +export const HIDE_REQUEST_LOGS_ERROR = 'HIDE_REQUEST_LOGS_ERROR'; export const RECEIVE_PODS_DATA_SUCCESS = 'RECEIVE_PODS_DATA_SUCCESS'; export const RECEIVE_PODS_DATA_ERROR = 'RECEIVE_PODS_DATA_ERROR'; diff --git a/app/assets/javascripts/logs/stores/mutations.js b/app/assets/javascripts/logs/stores/mutations.js index d77c2894a05..5e1c794c3a9 100644 --- a/app/assets/javascripts/logs/stores/mutations.js +++ b/app/assets/javascripts/logs/stores/mutations.js @@ -18,6 +18,12 @@ export default { state.timeRange.selected = timeRange; state.timeRange.current = convertToFixedRange(timeRange); }, + [types.SHOW_TIME_RANGE_INVALID_WARNING](state) { + state.timeRange.invalidWarning = true; + }, + [types.HIDE_TIME_RANGE_INVALID_WARNING](state) { + state.timeRange.invalidWarning = false; + }, // Environments Data [types.SET_PROJECT_ENVIRONMENT](state, environmentName) { @@ -38,6 +44,10 @@ export default { [types.RECEIVE_ENVIRONMENTS_DATA_ERROR](state) { state.environments.options = []; state.environments.isLoading = false; + state.environments.fetchError = true; + }, + [types.HIDE_REQUEST_ENVIRONMENTS_ERROR](state) { + state.environments.fetchError = false; }, // Logs data @@ -63,6 +73,7 @@ export default { [types.RECEIVE_LOGS_DATA_ERROR](state) { state.logs.lines = []; state.logs.isLoading = false; + state.logs.fetchError = true; }, [types.REQUEST_LOGS_DATA_PREPEND](state) { @@ -80,6 +91,10 @@ export default { }, [types.RECEIVE_LOGS_DATA_PREPEND_ERROR](state) { state.logs.isLoading = false; + state.logs.fetchError = true; + }, + [types.HIDE_REQUEST_LOGS_ERROR](state) { + state.logs.fetchError = false; }, // Pods data diff --git a/app/assets/javascripts/logs/stores/state.js b/app/assets/javascripts/logs/stores/state.js index 2c8f47294cc..11185c9ccf1 100644 --- a/app/assets/javascripts/logs/stores/state.js +++ b/app/assets/javascripts/logs/stores/state.js @@ -16,6 +16,8 @@ export default () => ({ selected: defaultTimeRange, // Current time range, must be fixed current: convertToFixedRange(defaultTimeRange), + + invalidWarning: false, }, /** @@ -25,6 +27,7 @@ export default () => ({ options: [], isLoading: false, current: null, + fetchError: false, }, /** @@ -39,6 +42,8 @@ export default () => ({ */ cursor: null, isComplete: false, + + fetchError: false, }, /** diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 6a836adba01..ef3f4d0e3f6 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -99,7 +99,17 @@ export default { downstreamNode.classList.contains('child-pipeline') ? 15 : 30, ); - this.$emit('onClickTriggered', this.pipeline, pipeline); + /** + * If the expanded trigger is defined and the id is different than the + * pipeline we clicked, then it means we clicked on a sibling downstream link + * and we want to reset the pipeline store. Triggering the reset without + * this condition would mean not allowing downstreams of downstreams to expand + */ + if (this.expandedTriggered?.id !== pipeline.id) { + this.$emit('onResetTriggered', this.pipeline, pipeline); + } + + this.$emit('onClickTriggered', pipeline); }, calculateMarginTop(downstreamNode, pixelDiff) { return `${downstreamNode.offsetTop - downstreamNode.offsetParent.offsetTop - pixelDiff}px`; @@ -136,9 +146,7 @@ export default { :pipeline="expandedTriggeredBy" :is-linked-pipeline="true" :mediator="mediator" - @onClickTriggeredBy=" - (parentPipeline, pipeline) => clickTriggeredByPipeline(parentPipeline, pipeline) - " + @onClickTriggeredBy="clickTriggeredByPipeline" @refreshPipelineGraph="requestRefreshPipelineGraph" /> @@ -148,9 +156,7 @@ export default { :column-title="__('Upstream')" :project-id="pipelineProjectId" graph-position="left" - @linkedPipelineClick=" - linkedPipeline => $emit('onClickTriggeredBy', pipeline, linkedPipeline) - " + @linkedPipelineClick="$emit('onClickTriggeredBy', $event)" /> <ul @@ -197,9 +203,7 @@ export default { :is-linked-pipeline="true" :style="{ 'margin-top': downstreamMarginTop }" :mediator="mediator" - @onClickTriggered=" - (parentPipeline, pipeline) => clickTriggeredPipeline(parentPipeline, pipeline) - " + @onClickTriggered="clickTriggeredPipeline" @refreshPipelineGraph="requestRefreshPipelineGraph" /> </div> diff --git a/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js b/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js index 1d9366f26df..f987c8f1dd4 100644 --- a/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js +++ b/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js @@ -27,9 +27,9 @@ export default { * @param {String} resetStoreKey Store key for the visible pipeline that will need to be reset * @param {Object} pipeline The clicked pipeline */ - clickPipeline(parentPipeline, pipeline, openMethod, closeMethod) { + clickPipeline(pipeline, openMethod, closeMethod) { if (!pipeline.isExpanded) { - this.mediator.store[openMethod](parentPipeline, pipeline); + this.mediator.store[openMethod](pipeline); this.mediator.store.toggleLoading(pipeline); this.mediator.poll.stop(); @@ -41,21 +41,14 @@ export default { this.mediator.poll.enable({ data: this.mediator.getExpandedParameters() }); } }, - clickTriggeredByPipeline(parentPipeline, pipeline) { - this.clickPipeline( - parentPipeline, - pipeline, - 'openTriggeredByPipeline', - 'closeTriggeredByPipeline', - ); + resetTriggeredPipelines(parentPipeline, pipeline) { + this.mediator.store.resetTriggeredPipelines(parentPipeline, pipeline); }, - clickTriggeredPipeline(parentPipeline, pipeline) { - this.clickPipeline( - parentPipeline, - pipeline, - 'openTriggeredPipeline', - 'closeTriggeredPipeline', - ); + clickTriggeredByPipeline(pipeline) { + this.clickPipeline(pipeline, 'openPipeline', 'closePipeline'); + }, + clickTriggeredPipeline(pipeline) { + this.clickPipeline(pipeline, 'openPipeline', 'closePipeline'); }, requestRefreshPipelineGraph() { // When an action is clicked diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index c901971be50..d76425c96b7 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -42,10 +42,10 @@ export default () => { }, on: { refreshPipelineGraph: this.requestRefreshPipelineGraph, - onClickTriggeredBy: (parentPipeline, pipeline) => - this.clickTriggeredByPipeline(parentPipeline, pipeline), - onClickTriggered: (parentPipeline, pipeline) => - this.clickTriggeredPipeline(parentPipeline, pipeline), + onResetTriggered: (parentPipeline, pipeline) => + this.resetTriggeredPipelines(parentPipeline, pipeline), + onClickTriggeredBy: pipeline => this.clickTriggeredByPipeline(pipeline), + onClickTriggered: pipeline => this.clickTriggeredPipeline(pipeline), }, }); }, diff --git a/app/assets/javascripts/pipelines/stores/pipeline_store.js b/app/assets/javascripts/pipelines/stores/pipeline_store.js index 69e3579a3c7..1ef73760e02 100644 --- a/app/assets/javascripts/pipelines/stores/pipeline_store.js +++ b/app/assets/javascripts/pipelines/stores/pipeline_store.js @@ -54,16 +54,24 @@ export default class PipelineStore { */ parseTriggeredByPipelines(oldPipeline = {}, newPipeline) { // keep old value in case it's opened because we're polling - Vue.set(newPipeline, 'isExpanded', oldPipeline.isExpanded || false); // add isLoading property Vue.set(newPipeline, 'isLoading', false); + // Because there can only ever be one `triggered_by` for any given pipeline, + // the API returns an object for the value instead of an Array. However, + // it's easier to deal with an array in the FE so we convert it. if (newPipeline.triggered_by) { if (!Array.isArray(newPipeline.triggered_by)) { Object.assign(newPipeline, { triggered_by: [newPipeline.triggered_by] }); } - this.parseTriggeredByPipelines(oldPipeline, newPipeline.triggered_by[0]); + + if (newPipeline.triggered_by?.length > 0) { + newPipeline.triggered_by.forEach(el => { + const oldTriggeredBy = oldPipeline.triggered_by?.find(element => element.id === el.id); + this.parseTriggeredPipelines(oldTriggeredBy, el); + }); + } } } diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue index a057913fd5a..00ccc49d770 100644 --- a/app/assets/javascripts/repository/components/table/row.vue +++ b/app/assets/javascripts/repository/components/table/row.vue @@ -1,10 +1,16 @@ <script> import { escapeRegExp } from 'lodash'; -import { GlBadge, GlLink, GlSkeletonLoading, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui'; +import { + GlBadge, + GlLink, + GlSkeletonLoading, + GlTooltipDirective, + GlLoadingIcon, + GlIcon, +} from '@gitlab/ui'; import { escapeFileUrl } from '~/lib/utils/url_utility'; import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; -import Icon from '~/vue_shared/components/icon.vue'; -import { getIconName } from '../../utils/icon'; +import FileIcon from '~/vue_shared/components/file_icon.vue'; import getRefMixin from '../../mixins/get_ref'; import getCommit from '../../queries/getCommit.query.graphql'; @@ -14,8 +20,9 @@ export default { GlLink, GlSkeletonLoading, GlLoadingIcon, + GlIcon, TimeagoTooltip, - Icon, + FileIcon, }, directives: { GlTooltip: GlTooltipDirective, @@ -95,9 +102,6 @@ export default { ? { path: `/-/tree/${escape(this.ref)}/${escapeFileUrl(this.path)}` } : null; }, - iconName() { - return `fa-${getIconName(this.type, this.path)}`; - }, isFolder() { return this.type === 'tree'; }, @@ -123,12 +127,6 @@ export default { <template> <tr class="tree-item"> <td class="tree-item-file-name cursor-default position-relative"> - <gl-loading-icon - v-if="path === loadingPath" - size="sm" - inline - class="d-inline-block align-text-bottom fa-fw" - /> <component :is="linkComponent" ref="link" @@ -140,27 +138,27 @@ export default { class="tree-item-link str-truncated" data-qa-selector="file_name_link" > - <i - v-if="path !== loadingPath" - :aria-label="type" - role="img" - :class="iconName" - class="fa fa-fw mr-1" - ></i - ><span class="position-relative">{{ fullPath }}</span> + <file-icon + :file-name="fullPath" + :folder="isFolder" + :submodule="isSubmodule" + :loading="path === loadingPath" + css-classes="position-relative file-icon" + class="mr-1 position-relative text-secondary" + /><span class="position-relative">{{ fullPath }}</span> </component> <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings --> <gl-badge v-if="lfsOid" variant="default" class="label-lfs ml-1">LFS</gl-badge> <template v-if="isSubmodule"> @ <gl-link :href="submoduleTreeUrl" class="commit-sha">{{ shortSha }}</gl-link> </template> - <icon + <gl-icon v-if="hasLockLabel" v-gl-tooltip :title="commit.lockLabel" name="lock" :size="12" - class="ml-2 vertical-align-middle" + class="ml-1" /> </td> <td class="d-none d-sm-table-cell tree-commit cursor-default"> diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue index 952ffa1fa0e..b084ebdf774 100644 --- a/app/assets/javascripts/vue_shared/components/file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/file_icon.vue @@ -1,7 +1,6 @@ <script> -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; import getIconForFile from './file_icon/file_icon_map'; -import icon from '../../vue_shared/components/icon.vue'; /* This is a re-usable vue component for rendering a svg sprite icon @@ -17,8 +16,8 @@ import icon from '../../vue_shared/components/icon.vue'; */ export default { components: { - icon, GlLoadingIcon, + GlIcon, }, props: { fileName: { @@ -31,7 +30,11 @@ export default { required: false, default: false, }, - + submodule: { + type: Boolean, + required: false, + default: false, + }, opened: { type: Boolean, required: false, @@ -58,7 +61,7 @@ export default { }, computed: { spriteHref() { - const iconName = getIconForFile(this.fileName) || 'file'; + const iconName = this.submodule ? 'folder-git' : getIconForFile(this.fileName) || 'file'; return `${gon.sprite_file_icons}#${iconName}`; }, folderIconName() { @@ -73,9 +76,12 @@ export default { <template> <span> <svg v-if="!loading && !folder" :class="[iconSizeClass, cssClasses]"> - <use v-bind="{ 'xlink:href': spriteHref }" /> - </svg> - <icon v-if="!loading && folder" :name="folderIconName" :size="size" class="folder-icon" /> - <gl-loading-icon v-if="loading" :inline="true" /> + <use v-bind="{ 'xlink:href': spriteHref }" /></svg + ><gl-icon + v-if="!loading && folder" + :name="folderIconName" + :size="size" + class="folder-icon" + /><gl-loading-icon v-if="loading" :inline="true" /> </span> </template> diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 211e1e30161..320bd4adaaa 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -474,6 +474,9 @@ img.emoji { .mw-70p { max-width: 70%; } .mw-90p { max-width: 90%; } +// By default flex items don't shrink below their minimum content size. +// To change this, these clases set a min-width or min-height +.min-width-0 { min-width: 0; } .min-height-0 { min-height: 0; } .svg-w-100 { diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 76b12b2405f..52da1b9abfc 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -199,8 +199,8 @@ /* * Mixin that handles the container for the job logs (CI/CD and kubernetes pod logs) */ -@mixin build-trace { - background: $black; +@mixin build-trace($background: $black) { + background: $background; color: $gray-darkest; white-space: pre; overflow-x: auto; @@ -243,7 +243,7 @@ /* * Mixin that handles the position of the controls placed on the top bar */ -@mixin build-controllers($control-font-size, $flex-direction, $with-grow, $flex-grow-size, $svg-display: 'block', $svg-top: '2px') { +@mixin build-controllers($control-font-size, $flex-direction, $with-grow, $flex-grow-size, $svg-display: block, $svg-top: 2px) { display: flex; font-size: $control-font-size; justify-content: $flex-direction; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index a3c1d8b1709..65efbabaa4f 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -641,6 +641,14 @@ $issue-boards-breadcrumbs-height-xs: 63px; $issue-board-list-difference-xs: $header-height + $issue-boards-breadcrumbs-height-xs; $issue-board-list-difference-sm: $header-height + $breadcrumb-min-height; $issue-board-list-difference-md: $issue-board-list-difference-sm + $issue-boards-filter-height; +/* + The following heights are used in environment_logs.scss and are used for calculation of the log viewer height. +*/ +$environment-logs-breadcrumbs-height: 63px; +$environment-logs-breadcrumbs-height-md: $breadcrumb-min-height; + +$environment-logs-difference-xs-up: $header-height + $environment-logs-breadcrumbs-height; +$environment-logs-difference-md-up: $header-height + $environment-logs-breadcrumbs-height-md; /* * Avatar diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index f8b8a7271ce..f50d4bc736e 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -356,54 +356,3 @@ } } } - -.environment-logs-viewer { - .build-trace-container { - position: relative; - } - - .log-lines, - .gl-infinite-scroll-container { - // makes scrollbar visible by creating contrast - background: $black; - } - - .gl-infinite-scroll-legend { - margin: 0; - } - - .build-trace { - @include build-trace(); - margin: 0; - } - - .top-bar { - .date-time-picker-wrapper, - .dropdown-toggle { - @include media-breakpoint-up(md) { - width: 140px; - } - - @include media-breakpoint-up(lg) { - width: 160px; - } - } - - .controllers { - @include build-controllers(16px, flex-end, false, 2); - } - } - - .btn-refresh svg { - top: 0; - } - - .build-loader-animation { - @include build-loader-animation; - } - - .log-footer { - color: $white-normal; - background-color: $gray-900; - } -} diff --git a/app/assets/stylesheets/pages/environment_logs.scss b/app/assets/stylesheets/pages/environment_logs.scss new file mode 100644 index 00000000000..81cec14062f --- /dev/null +++ b/app/assets/stylesheets/pages/environment_logs.scss @@ -0,0 +1,58 @@ +.environment-logs-page { + .content-wrapper { + padding-bottom: 0; + } +} + +.environment-logs-viewer { + height: calc(100vh - #{$environment-logs-difference-xs-up}); + min-height: 700px; + + @include media-breakpoint-up(md) { + height: calc(100vh - #{$environment-logs-difference-md-up}); + } + + .with-performance-bar & { + height: calc(100vh - #{$environment-logs-difference-xs-up} - #{$performance-bar-height}); + + @include media-breakpoint-up(md) { + height: calc(100vh - #{$environment-logs-difference-md-up} - #{$performance-bar-height}); + } + } + + .top-bar { + .date-time-picker-wrapper, + .dropdown-toggle { + @include media-breakpoint-up(md) { + width: 140px; + } + + @include media-breakpoint-up(lg) { + width: 160px; + } + } + + .controllers { + @include build-controllers(16px, flex-end, false, 2, inline); + } + } + + .log-lines, + .gl-infinite-scroll-container { + // makes scrollbar visible by creating contrast + background: $black; + height: 100%; + } + + .build-trace { + @include build-trace($black); + } + + .gl-infinite-scroll-legend { + margin: 0; + } + + .build-loader-animation { + @include build-loader-animation; + } +} diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index a03101c66ac..142078588df 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -138,6 +138,12 @@ } .tree-item { + .file-icon, + .folder-icon { + position: relative; + top: 2px; + } + .link-container { padding: 0; diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index ae94edac734..cd95105a893 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -16,6 +16,10 @@ class Admin::DashboardController < Admin::ApplicationController end # rubocop: enable CodeReuse/ActiveRecord + def stats + @users_statistics = UsersStatistics.latest + end + def show_license_breakdown? false end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 83ecc7753b6..a815b378f8b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -256,6 +256,7 @@ module ApplicationHelper def page_class class_names = [] class_names << 'issue-boards-page' if current_controller?(:boards) + class_names << 'environment-logs-page' if current_controller?(:logs) class_names << 'with-performance-bar' if performance_bar_enabled? class_names << system_message_class class_names diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 3694d9e2abe..443451cd394 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -176,6 +176,7 @@ module ApplicationSettingsHelper :authorized_keys_enabled, :auto_devops_enabled, :auto_devops_domain, + :container_expiration_policies_enable_historic_entries, :container_registry_token_expire_delay, :default_artifacts_expire_in, :default_branch_protection, diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 9254f7dd633..c1e44748304 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -142,6 +142,9 @@ class ApplicationSetting < ApplicationRecord validates :default_artifacts_expire_in, presence: true, duration: true + validates :container_expiration_policies_enable_historic_entries, + inclusion: { in: [true, false], message: 'must be a boolean value' } + validates :container_registry_token_expire_delay, presence: true, numericality: { only_integer: true, greater_than: 0 } diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index 418fb18cc91..920ad3286d1 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -42,6 +42,7 @@ module ApplicationSettingImplementation asset_proxy_enabled: false, authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand commit_email_hostname: default_commit_email_hostname, + container_expiration_policies_enable_historic_entries: false, container_registry_token_expire_delay: 5, default_artifacts_expire_in: '30 days', default_branch_protection: Settings.gitlab['default_branch_protection'], diff --git a/app/models/users_statistics.rb b/app/models/users_statistics.rb index 1a500717efd..37a430015e5 100644 --- a/app/models/users_statistics.rb +++ b/app/models/users_statistics.rb @@ -1,16 +1,29 @@ # frozen_string_literal: true class UsersStatistics < ApplicationRecord - STATISTICS_NAMES = [ - :without_groups_and_projects, - :with_highest_role_guest, - :with_highest_role_reporter, - :with_highest_role_developer, - :with_highest_role_maintainer, - :with_highest_role_owner, - :bots, - :blocked - ].freeze + scope :order_created_at_desc, -> { order(created_at: :desc) } + + class << self + def latest + order_created_at_desc.first + end + end + + def active + [ + without_groups_and_projects, + with_highest_role_guest, + with_highest_role_reporter, + with_highest_role_developer, + with_highest_role_maintainer, + with_highest_role_owner, + bots + ].sum + end + + def total + active + blocked + end class << self def create_current_stats! diff --git a/app/views/admin/application_settings/_registry.html.haml b/app/views/admin/application_settings/_registry.html.haml index 77623e1495b..0631c024eb8 100644 --- a/app/views/admin/application_settings/_registry.html.haml +++ b/app/views/admin/application_settings/_registry.html.haml @@ -5,5 +5,14 @@ .form-group = f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'label-bold' = f.number_field :container_registry_token_expire_delay, class: 'form-control' + .form-group + .form-check + = f.check_box :container_expiration_policies_enable_historic_entries, class: 'form-check-input' + = f.label :container_expiration_policies_enable_historic_entries, class: 'form-check-label' do + = _("Enable container expiration and retention policies for projects created earlier than GitLab 12.7.") + = link_to icon('question-circle'), help_page_path('user/packages/container_registry/index', anchor: 'expiration-policy') + .form-text.text-muted + = _("Existing projects will be able to use expiration policies. Avoid enabling this if an external Container Registry is being used, as there is a performance risk if many images exist on one project.") + = link_to icon('question-circle'), help_page_path('user/packages/container_registry/index', anchor: 'use-with-external-container-registries') = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 68f761c75d8..951e5364ad8 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -30,7 +30,7 @@ %hr .btn-group.d-flex{ role: 'group' } = link_to 'New user', new_admin_user_path, class: "btn btn-success" - = render_if_exists 'admin/dashboard/users_statistics' + = link_to s_('AdminArea|Users statistics'), admin_dashboard_stats_path, class: 'btn btn-primary' .col-sm-4 .info-well.dark-well .well-segment.well-centered diff --git a/app/views/admin/dashboard/stats.html.haml b/app/views/admin/dashboard/stats.html.haml new file mode 100644 index 00000000000..f7f2c717308 --- /dev/null +++ b/app/views/admin/dashboard/stats.html.haml @@ -0,0 +1,75 @@ +- page_title s_('AdminArea|Users statistics') + +%h3.my-4 + = s_('AdminArea|Users statistics') +%table.table.gl-text-gray-700 + %tr + %td.p-3 + = s_('AdminArea|Users without a Group and Project') + = render_if_exists 'admin/dashboard/included_free_in_license_tooltip' + %td.p-3.text-right + = @users_statistics&.without_groups_and_projects.to_i + %tr + %td.p-3 + = s_('AdminArea|Users with highest role') + %strong + = s_('AdminArea|Guest') + = render_if_exists 'admin/dashboard/included_free_in_license_tooltip' + %td.p-3.text-right + = @users_statistics&.with_highest_role_guest.to_i + %tr + %td.p-3 + = s_('AdminArea|Users with highest role') + %strong + = s_('AdminArea|Reporter') + %td.p-3.text-right + = @users_statistics&.with_highest_role_reporter.to_i + %tr + %td.p-3 + = s_('AdminArea|Users with highest role') + %strong + = s_('AdminArea|Developer') + %td.p-3.text-right + = @users_statistics&.with_highest_role_developer.to_i + %tr + %td.p-3 + = s_('AdminArea|Users with highest role') + %strong + = s_('AdminArea|Maintainer') + %td.p-3.text-right + = @users_statistics&.with_highest_role_maintainer.to_i + %tr + %td.p-3 + = s_('AdminArea|Users with highest role') + %strong + = s_('AdminArea|Owner') + %td.p-3.text-right + = @users_statistics&.with_highest_role_owner.to_i + %tr + %td.p-3 + = s_('AdminArea|Bots') + %td.p-3.text-right + = @users_statistics&.bots.to_i + + %tr.bg-gray-light.gl-text-gray-900 + %td.p-3 + %strong + = s_('AdminArea|Active users') + = render_if_exists 'admin/dashboard/billable_users_text' + %td.p-3.text-right + %strong + = @users_statistics&.active.to_i + %tr.bg-gray-light.gl-text-gray-900 + %td.p-3 + %strong + = s_('AdminArea|Blocked users') + %td.p-3.text-right + %strong + = @users_statistics&.blocked.to_i + %tr.bg-gray-light.gl-text-gray-900 + %td.p-3 + %strong + = s_('AdminArea|Total users') + %td.p-3.text-right + %strong + = @users_statistics&.total.to_i diff --git a/changelogs/unreleased/119017-unable-to-expand-multiple-downstream-pipelines.yml b/changelogs/unreleased/119017-unable-to-expand-multiple-downstream-pipelines.yml new file mode 100644 index 00000000000..6a4eeef358c --- /dev/null +++ b/changelogs/unreleased/119017-unable-to-expand-multiple-downstream-pipelines.yml @@ -0,0 +1,5 @@ +--- +title: Resolve Unable to expand multiple downstream pipelines. +merge_request: 27029 +author: +type: fixed diff --git a/changelogs/unreleased/208735-container-expiration-policy-app-setting.yml b/changelogs/unreleased/208735-container-expiration-policy-app-setting.yml new file mode 100644 index 00000000000..640b236bbf6 --- /dev/null +++ b/changelogs/unreleased/208735-container-expiration-policy-app-setting.yml @@ -0,0 +1,6 @@ +--- +title: Add application setting to enable container expiration and retention policies + on pre 12.8 projects +merge_request: 28479 +author: +type: added diff --git a/changelogs/unreleased/210502-restore-full-height-of-logs-explorer.yml b/changelogs/unreleased/210502-restore-full-height-of-logs-explorer.yml new file mode 100644 index 00000000000..1b9323bde2b --- /dev/null +++ b/changelogs/unreleased/210502-restore-full-height-of-logs-explorer.yml @@ -0,0 +1,5 @@ +--- +title: Enable log explorer to use the full height of the screen +merge_request: 28312 +author: +type: added diff --git a/changelogs/unreleased/add-internal-api-pages-enabled.yml b/changelogs/unreleased/add-internal-api-pages-enabled.yml new file mode 100644 index 00000000000..17c185713a2 --- /dev/null +++ b/changelogs/unreleased/add-internal-api-pages-enabled.yml @@ -0,0 +1,5 @@ +--- +title: Add status endpoint to Pages Internal API +merge_request: 28743 +author: +type: added diff --git a/changelogs/unreleased/ph-treeFileIcons.yml b/changelogs/unreleased/ph-treeFileIcons.yml new file mode 100644 index 00000000000..01b6a72826a --- /dev/null +++ b/changelogs/unreleased/ph-treeFileIcons.yml @@ -0,0 +1,5 @@ +--- +title: Use rich icons for thw rows on the file tree +merge_request: 28112 +author: +type: changed diff --git a/changelogs/unreleased/use_users_statistics_table_in_view.yml b/changelogs/unreleased/use_users_statistics_table_in_view.yml new file mode 100644 index 00000000000..2230714a55b --- /dev/null +++ b/changelogs/unreleased/use_users_statistics_table_in_view.yml @@ -0,0 +1,5 @@ +--- +title: Show user statistics in admin area also in CE, and use daily generated data for these statistics +merge_request: 27345 +author: +type: changed diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 116c607c2cb..96cd6e5f587 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -161,5 +161,7 @@ namespace :admin do concerns :clusterable + get '/dashboard/stats', to: 'dashboard#stats' + root to: 'dashboard#index' end diff --git a/db/migrate/20200331195952_add_container_expiration_policies_enable_historic_entries_to_application_settings.rb b/db/migrate/20200331195952_add_container_expiration_policies_enable_historic_entries_to_application_settings.rb new file mode 100644 index 00000000000..95ce75efccc --- /dev/null +++ b/db/migrate/20200331195952_add_container_expiration_policies_enable_historic_entries_to_application_settings.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class AddContainerExpirationPoliciesEnableHistoricEntriesToApplicationSettings < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default(:application_settings, + :container_expiration_policies_enable_historic_entries, + :boolean, + default: false, + allow_null: false) + end + + def down + remove_column(:application_settings, + :container_expiration_policies_enable_historic_entries) + end +end diff --git a/db/structure.sql b/db/structure.sql index 6f3c271a0db..a284d001ee5 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -397,7 +397,8 @@ CREATE TABLE public.application_settings ( email_restrictions text, npm_package_requests_forwarding boolean DEFAULT true NOT NULL, namespace_storage_size_limit bigint DEFAULT 0 NOT NULL, - seat_link_enabled boolean DEFAULT true NOT NULL + seat_link_enabled boolean DEFAULT true NOT NULL, + container_expiration_policies_enable_historic_entries boolean DEFAULT false NOT NULL ); CREATE SEQUENCE public.application_settings_id_seq @@ -13001,6 +13002,7 @@ COPY "schema_migrations" (version) FROM STDIN; 20200330121000 20200330123739 20200330132913 +20200331195952 20200331220930 20200402123926 20200402135250 diff --git a/doc/administration/packages/container_registry.md b/doc/administration/packages/container_registry.md index a798c9527b0..b940cb6933b 100644 --- a/doc/administration/packages/container_registry.md +++ b/doc/administration/packages/container_registry.md @@ -516,6 +516,10 @@ on how to achieve that. ## Use an external container registry with GitLab as an auth endpoint +NOTE: **Note:** +In using an external container registry, some features associated with the +container registry may be unavailable or have [inherant risks](./../../user/packages/container_registry/index.md#use-with-external-container-registries) + **Omnibus GitLab** You can use GitLab as an auth endpoint with an external container registry. diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 7e863490369..0eebc74cc6c 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -2984,6 +2984,103 @@ type EpicTreeReorderPayload { errors: [String!]! } +type GeoNode { + """ + The maximum concurrency of container repository sync for this secondary node + """ + containerRepositoriesMaxCapacity: Int + + """ + Indicates whether this Geo node is enabled + """ + enabled: Boolean + + """ + The maximum concurrency of LFS/attachment backfill for this secondary node + """ + filesMaxCapacity: Int + + """ + ID of this GeoNode + """ + id: ID! + + """ + The URL defined on the primary node that secondary nodes should use to contact it + """ + internalUrl: String + + """ + The interval (in days) in which the repository verification is valid. Once expired, it will be reverified + """ + minimumReverificationInterval: Int + + """ + The unique identifier for this Geo node + """ + name: String + + """ + Indicates whether this Geo node is the primary + """ + primary: Boolean + + """ + The maximum concurrency of repository backfill for this secondary node + """ + reposMaxCapacity: Int + + """ + The namespaces that should be synced, if `selective_sync_type` == `namespaces` + """ + selectiveSyncNamespaces( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): NamespaceConnection + + """ + The repository storages whose projects should be synced, if `selective_sync_type` == `shards` + """ + selectiveSyncShards: [String!] + + """ + Indicates if syncing is limited to only specific groups, or shards + """ + selectiveSyncType: String + + """ + Indicates if this secondary node will replicate blobs in Object Storage + """ + syncObjectStorage: Boolean + + """ + The user-facing URL for this Geo node + """ + url: String + + """ + The maximum concurrency of repository verification for this secondary node + """ + verificationMaxCapacity: Int +} + type GrafanaIntegration { """ Timestamp of the issue's creation @@ -5435,6 +5532,41 @@ type Namespace { visibility: String } +""" +The connection type for Namespace. +""" +type NamespaceConnection { + """ + A list of edges. + """ + edges: [NamespaceEdge] + + """ + A list of nodes. + """ + nodes: [Namespace] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! +} + +""" +An edge in a connection. +""" +type NamespaceEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Namespace +} + type Note { """ User who wrote this note @@ -6917,6 +7049,16 @@ type Query { ): String! """ + Find a Geo node + """ + geoNode( + """ + The name of the Geo node. Defaults to the current Geo node name. + """ + name: String + ): GeoNode + + """ Find a group """ group( diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 813ab39795a..9abc312da33 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -8655,6 +8655,280 @@ }, { "kind": "OBJECT", + "name": "GeoNode", + "description": null, + "fields": [ + { + "name": "containerRepositoriesMaxCapacity", + "description": "The maximum concurrency of container repository sync for this secondary node", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enabled", + "description": "Indicates whether this Geo node is enabled", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filesMaxCapacity", + "description": "The maximum concurrency of LFS/attachment backfill for this secondary node", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": "ID of this GeoNode", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "internalUrl", + "description": "The URL defined on the primary node that secondary nodes should use to contact it", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "minimumReverificationInterval", + "description": "The interval (in days) in which the repository verification is valid. Once expired, it will be reverified", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "The unique identifier for this Geo node", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "primary", + "description": "Indicates whether this Geo node is the primary", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "reposMaxCapacity", + "description": "The maximum concurrency of repository backfill for this secondary node", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "selectiveSyncNamespaces", + "description": "The namespaces that should be synced, if `selective_sync_type` == `namespaces`", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "NamespaceConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "selectiveSyncShards", + "description": "The repository storages whose projects should be synced, if `selective_sync_type` == `shards`", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "selectiveSyncType", + "description": "Indicates if syncing is limited to only specific groups, or shards", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "syncObjectStorage", + "description": "Indicates if this secondary node will replicate blobs in Object Storage", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "url", + "description": "The user-facing URL for this Geo node", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "verificationMaxCapacity", + "description": "The maximum concurrency of repository verification for this secondary node", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", "name": "GrafanaIntegration", "description": null, "fields": [ @@ -16464,6 +16738,118 @@ }, { "kind": "OBJECT", + "name": "NamespaceConnection", + "description": "The connection type for Namespace.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "NamespaceEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Namespace", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "NamespaceEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Namespace", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", "name": "Note", "description": null, "fields": [ @@ -20779,6 +21165,29 @@ "deprecationReason": null }, { + "name": "geoNode", + "description": "Find a Geo node", + "args": [ + { + "name": "name", + "description": "The name of the Geo node. Defaults to the current Geo node name.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "GeoNode", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "group", "description": "Find a group", "args": [ diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index dbe98639d23..6948f361a14 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -483,6 +483,25 @@ Autogenerated return type of EpicTreeReorder | `clientMutationId` | String | A unique identifier for the client performing the mutation. | | `errors` | String! => Array | Reasons why the mutation failed. | +## GeoNode + +| Name | Type | Description | +| --- | ---- | ---------- | +| `containerRepositoriesMaxCapacity` | Int | The maximum concurrency of container repository sync for this secondary node | +| `enabled` | Boolean | Indicates whether this Geo node is enabled | +| `filesMaxCapacity` | Int | The maximum concurrency of LFS/attachment backfill for this secondary node | +| `id` | ID! | ID of this GeoNode | +| `internalUrl` | String | The URL defined on the primary node that secondary nodes should use to contact it | +| `minimumReverificationInterval` | Int | The interval (in days) in which the repository verification is valid. Once expired, it will be reverified | +| `name` | String | The unique identifier for this Geo node | +| `primary` | Boolean | Indicates whether this Geo node is the primary | +| `reposMaxCapacity` | Int | The maximum concurrency of repository backfill for this secondary node | +| `selectiveSyncShards` | String! => Array | The repository storages whose projects should be synced, if `selective_sync_type` == `shards` | +| `selectiveSyncType` | String | Indicates if syncing is limited to only specific groups, or shards | +| `syncObjectStorage` | Boolean | Indicates if this secondary node will replicate blobs in Object Storage | +| `url` | String | The user-facing URL for this Geo node | +| `verificationMaxCapacity` | Int | The maximum concurrency of repository verification for this secondary node | + ## GrafanaIntegration | Name | Type | Description | diff --git a/doc/api/settings.md b/doc/api/settings.md index 2c75c175fdd..5fe068cf085 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -45,6 +45,7 @@ Example response: "default_group_visibility" : "private", "gravatar_enabled" : true, "sign_in_text" : null, + "container_expiration_policies_enable_historic_entries": true, "container_registry_token_expire_delay": 5, "repository_storages": ["default"], "plantuml_enabled": false, diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md index 29deed724f9..db3d2352f09 100644 --- a/doc/ci/triggers/README.md +++ b/doc/ci/triggers/README.md @@ -15,7 +15,21 @@ tag) with an API call. ## Authentication tokens -The following methods of authentication are supported. +The following methods of authentication are supported: + +- [Trigger token](#trigger-token) +- [CI job token](#ci-job-token) + +If using the `$CI_PIPELINE_SOURCE` [predefined environment variable](../variables/predefined_variables.md#variables-reference) +to limit which jobs run in a pipeline, the value could be either `pipeline` or `trigger`, +depending on which trigger method is used. + +| `$CI_PIPELINE_SOURCE` value | Trigger method | +|-----------------------------|----------------| +| `pipeline` | Using the `trigger:` keyword in the CI/CD configuration file, or using the trigger API with `$CI_JOB_TOKEN`. | +| `trigger` | Using the trigger API using a generated trigger token | + +This also applies when using the `pipelines` or `triggers` keywords with the legacy [`only/except` basic syntax](../yaml/README.md#onlyexcept-basic). ### Trigger token diff --git a/doc/user/admin_area/index.md b/doc/user/admin_area/index.md index cbc033fdedc..204573da02d 100644 --- a/doc/user/admin_area/index.md +++ b/doc/user/admin_area/index.md @@ -147,6 +147,9 @@ The **Total users** is calculated as: **Active users** + **Blocked users**. GitLab billing is based on the number of active users. For details of active users, see [Choosing the number of users](../../subscriptions/index.md#choosing-the-number-of-users). +**Please note** that during the initial stage, the information won't be 100% accurate given that +background processes are still recollecting data. + ### Administering Groups You can administer all groups in the GitLab instance from the Admin Area's Groups page. diff --git a/doc/user/admin_area/settings/index.md b/doc/user/admin_area/settings/index.md index 6731ebb1d8c..f0e7bf272a7 100644 --- a/doc/user/admin_area/settings/index.md +++ b/doc/user/admin_area/settings/index.md @@ -61,7 +61,7 @@ Access the default page for admin area settings by navigating to | ------ | ----------- | | [Continuous Integration and Deployment](continuous_integration.md) | Auto DevOps, runners and job artifacts. | | [Required pipeline configuration](continuous_integration.md#required-pipeline-configuration-premium-only) **(PREMIUM ONLY)** | Set an instance-wide auto included [pipeline configuration](../../../ci/yaml/README.md). This pipeline configuration will be run after the project's own configuration. | -| [Package Registry](continuous_integration.md#package-registry-configuration-premium-only) **(PREMIUM ONLY)**| Settings related to the use and experience of using GitLab's Package Registry. | +| [Package Registry](continuous_integration.md#package-registry-configuration-premium-only) **(PREMIUM ONLY)**| Settings related to the use and experience of using GitLab's Package Registry. Note there are [risks involved](./../../packages/container_registry/index.md#use-with-external-container-registries) in enabling some of these settings. | ## Reporting diff --git a/doc/user/packages/container_registry/index.md b/doc/user/packages/container_registry/index.md index f5b8bd82a2b..d6c6767a8fd 100644 --- a/doc/user/packages/container_registry/index.md +++ b/doc/user/packages/container_registry/index.md @@ -488,7 +488,9 @@ older tags and images are regularly removed from the Container Registry. > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/15398) in GitLab 12.8. NOTE: **Note:** -Expiration policies are only available for projects created in GitLab 12.8 and later. +Expiration policies for projects created before GitLab 12.8 may be enabled by an +admin in the [CI/CD Package Registry settings](./../../admin_area/settings/index.md#cicd). +Note the inherant [risks involved](./index.md#use-with-external-container-registries). It is possible to create a per-project expiration policy, so that you can make sure that older tags and images are regularly removed from the Container Registry. @@ -539,6 +541,15 @@ Examples: See the API documentation for further details: [Edit project](../../../api/projects.md#edit-project). +### Use with external container registries + +When using an [external container registry](./../../../administration/packages/container_registry.md#use-an-external-container-registry-with-gitlab-as-an-auth-endpoint), +running an experation policy on a project may have some performance risks. If a project is going to run +a policy that will remove large quantities of tags (in the thousands), the GitLab background jobs that +run the policy may get backed up or fail completely. It is recommended you only enable container expiration +policies for projects that were created before GitLab 12.8 if you are confident the amount of tags +being cleaned up will be minimal. + ## Limitations Moving or renaming existing Container Registry repositories is not supported diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index 003ed229385..f7aabc8ce4f 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -110,7 +110,7 @@ module API return unless %w[git-receive-pack git-upload-pack git-upload-archive].include?(action) { - repository: repository.gitaly_repository.to_h, + repository: repository.gitaly_repository, address: Gitlab::GitalyClient.address(container.repository_storage), token: Gitlab::GitalyClient.token(container.repository_storage), features: Feature::Gitaly.server_feature_flags diff --git a/lib/api/internal/pages.rb b/lib/api/internal/pages.rb index f19dbe563ac..6c8da414e4d 100644 --- a/lib/api/internal/pages.rb +++ b/lib/api/internal/pages.rb @@ -16,6 +16,13 @@ module API namespace 'internal' do namespace 'pages' do + desc 'Indicates that pages API is enabled and auth token is valid' do + detail 'This feature was introduced in GitLab 12.10.' + end + get "status" do + no_content! + end + desc 'Get GitLab Pages domain configuration by hostname' do detail 'This feature was introduced in GitLab 12.3.' end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 38c0eb5d83d..71b228c276a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1320,12 +1320,36 @@ msgstr "" msgid "Admin notes" msgstr "" +msgid "AdminArea|Active users" +msgstr "" + +msgid "AdminArea|Billable users" +msgstr "" + +msgid "AdminArea|Blocked users" +msgstr "" + msgid "AdminArea|Bots" msgstr "" +msgid "AdminArea|Developer" +msgstr "" + +msgid "AdminArea|Guest" +msgstr "" + msgid "AdminArea|Included Free in license" msgstr "" +msgid "AdminArea|Maintainer" +msgstr "" + +msgid "AdminArea|Owner" +msgstr "" + +msgid "AdminArea|Reporter" +msgstr "" + msgid "AdminArea|Stop all jobs" msgstr "" @@ -1338,15 +1362,18 @@ msgstr "" msgid "AdminArea|Stopping jobs failed" msgstr "" -msgid "AdminArea|Users statistics" +msgid "AdminArea|Total users" msgstr "" -msgid "AdminArea|Users total" +msgid "AdminArea|Users statistics" msgstr "" msgid "AdminArea|Users with highest role" msgstr "" +msgid "AdminArea|Users without a Group and Project" +msgstr "" + msgid "AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running." msgstr "" @@ -7538,6 +7565,9 @@ msgstr "" msgid "Enable classification control using an external service" msgstr "" +msgid "Enable container expiration and retention policies for projects created earlier than GitLab 12.7." +msgstr "" + msgid "Enable email restrictions for sign ups" msgstr "" @@ -7934,6 +7964,9 @@ msgstr "" msgid "Environments|Stopping" msgstr "" +msgid "Environments|There was an error fetching the logs. Please try again." +msgstr "" + msgid "Environments|This action will relaunch the job for commit %{commit_id}, putting the environment in a previous version. Are you sure you want to continue?" msgstr "" @@ -8357,6 +8390,9 @@ msgstr "" msgid "Existing members and groups" msgstr "" +msgid "Existing projects will be able to use expiration policies. Avoid enabling this if an external Container Registry is being used, as there is a performance risk if many images exist on one project." +msgstr "" + msgid "Existing shares" msgstr "" @@ -10650,9 +10686,6 @@ msgstr "" msgid "IDE|Live Preview" msgstr "" -msgid "IDE|Open in file view" -msgstr "" - msgid "IDE|Preview your web application using Web IDE client-side evaluation." msgstr "" @@ -12808,9 +12841,6 @@ msgstr "" msgid "Metrics|There was an error fetching the environments data, please try again" msgstr "" -msgid "Metrics|There was an error fetching the logs, please try again" -msgstr "" - msgid "Metrics|There was an error getting deployment information." msgstr "" @@ -13906,9 +13936,6 @@ msgstr "" msgid "Open in Xcode" msgstr "" -msgid "Open in file view" -msgstr "" - msgid "Open issues" msgstr "" diff --git a/spec/factories/users_statistics.rb b/spec/factories/users_statistics.rb index 5b0871f2262..07699dc38b2 100644 --- a/spec/factories/users_statistics.rb +++ b/spec/factories/users_statistics.rb @@ -2,5 +2,13 @@ FactoryBot.define do factory :users_statistics do + without_groups_and_projects { 23 } + with_highest_role_guest { 5 } + with_highest_role_reporter { 9 } + with_highest_role_developer { 21 } + with_highest_role_maintainer { 6 } + with_highest_role_owner { 5 } + bots { 2 } + blocked { 7 } end end diff --git a/spec/features/admin/dashboard_spec.rb b/spec/features/admin/dashboard_spec.rb index 6cb345c5066..018ef13cbb6 100644 --- a/spec/features/admin/dashboard_spec.rb +++ b/spec/features/admin/dashboard_spec.rb @@ -2,14 +2,14 @@ require 'spec_helper' -describe 'admin visits dashboard', :js do +describe 'admin visits dashboard' do include ProjectForksHelper before do sign_in(create(:admin)) end - context 'counting forks' do + context 'counting forks', :js do it 'correctly counts 2 forks of a project' do project = create(:project) project_fork = fork_project(project) @@ -25,4 +25,26 @@ describe 'admin visits dashboard', :js do expect(page).to have_content('Forks 2') end end + + describe 'Users statistic' do + let_it_be(:users_statistics) { create(:users_statistics) } + + it 'shows correct amounts of users', :aggregate_failures do + expected_active_users_text = Gitlab.ee? ? 'Active users (Billable users) 71' : 'Active users 71' + + sign_in(create(:admin)) + visit admin_dashboard_stats_path + + expect(page).to have_content('Users without a Group and Project 23') + expect(page).to have_content('Users with highest role Guest 5') + expect(page).to have_content('Users with highest role Reporter 9') + expect(page).to have_content('Users with highest role Developer 21') + expect(page).to have_content('Users with highest role Maintainer 6') + expect(page).to have_content('Users with highest role Owner 5') + expect(page).to have_content('Bots 2') + expect(page).to have_content(expected_active_users_text) + expect(page).to have_content('Blocked users 7') + expect(page).to have_content('Total users 78') + end + end end diff --git a/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb b/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb index f8179979018..2eaa2d24c4b 100644 --- a/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb +++ b/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb @@ -174,7 +174,7 @@ describe 'Set up Mattermost slash commands', :js do describe 'stable logo url' do it 'shows a publicly available logo' do - expect(File.exist?(Rails.root.join('public/slash-command-logo.png'))) + expect(File.exist?(Rails.root.join('public/slash-command-logo.png'))).to be_truthy end end end diff --git a/spec/frontend/logs/components/environment_logs_spec.js b/spec/frontend/logs/components/environment_logs_spec.js index 4da987725a1..befcd462828 100644 --- a/spec/frontend/logs/components/environment_logs_spec.js +++ b/spec/frontend/logs/components/environment_logs_spec.js @@ -47,7 +47,7 @@ describe('EnvironmentLogs', () => { const findLogControlButtons = () => wrapper.find({ name: 'log-control-buttons-stub' }); const findInfiniteScroll = () => wrapper.find({ ref: 'infiniteScroll' }); - const findLogTrace = () => wrapper.find('.js-log-trace'); + const findLogTrace = () => wrapper.find({ ref: 'logTrace' }); const findLogFooter = () => wrapper.find({ ref: 'logFooter' }); const getInfiniteScrollAttr = attr => parseInt(findInfiniteScroll().attributes(attr), 10); @@ -169,16 +169,12 @@ describe('EnvironmentLogs', () => { expect(updateControlBtnsMock).not.toHaveBeenCalled(); }); - it('shows an infinite scroll with height and no content', () => { - expect(getInfiniteScrollAttr('max-list-height')).toBeGreaterThan(0); + it('shows an infinite scroll with no content', () => { expect(getInfiniteScrollAttr('fetched-items')).toBe(0); }); - it('shows an infinite scroll container with equal height and max-height ', () => { - const height = getInfiniteScrollAttr('max-list-height'); - - expect(height).toEqual(expect.any(Number)); - expect(findInfiniteScroll().attributes('style')).toMatch(`height: ${height}px;`); + it('shows an infinite scroll container with no set max-height ', () => { + expect(findInfiniteScroll().attributes('max-list-height')).toBeUndefined(); }); it('shows a logs trace', () => { @@ -270,8 +266,7 @@ describe('EnvironmentLogs', () => { expect(findAdvancedFilters().exists()).toBe(true); }); - it('shows infinite scroll with height and no content', () => { - expect(getInfiniteScrollAttr('max-list-height')).toBeGreaterThan(0); + it('shows infinite scroll with content', () => { expect(getInfiniteScrollAttr('fetched-items')).toBe(mockTrace.length); }); diff --git a/spec/frontend/logs/stores/actions_spec.js b/spec/frontend/logs/stores/actions_spec.js index 303737a11cd..882673af984 100644 --- a/spec/frontend/logs/stores/actions_spec.js +++ b/spec/frontend/logs/stores/actions_spec.js @@ -38,7 +38,7 @@ jest.mock('~/logs/utils'); const mockDefaultRange = { start: '2020-01-10T18:00:00.000Z', - end: '2020-01-10T10:00:00.000Z', + end: '2020-01-10T19:00:00.000Z', }; const mockFixedRange = { start: '2020-01-09T18:06:20.000Z', @@ -145,9 +145,6 @@ describe('Logs Store actions', () => { { type: types.RECEIVE_ENVIRONMENTS_DATA_ERROR }, ], [], - () => { - expect(flash).toHaveBeenCalledTimes(1); - }, ); }); }); @@ -186,6 +183,7 @@ describe('Logs Store actions', () => { it('should commit logs and pod data when there is pod name defined', () => { state.pods.current = mockPodName; + state.timeRange.current = mockFixedRange; return testAction(fetchLogs, null, state, expectedMutations, expectedActions, () => { expect(latestGetParams()).toMatchObject({ @@ -214,22 +212,26 @@ describe('Logs Store actions', () => { state.search = mockSearch; state.timeRange.current = 'INVALID_TIME_RANGE'; + expectedMutations.splice(1, 0, { + type: types.SHOW_TIME_RANGE_INVALID_WARNING, + }); + return testAction(fetchLogs, null, state, expectedMutations, expectedActions, () => { expect(latestGetParams()).toEqual({ pod_name: mockPodName, search: mockSearch, }); - // Warning about time ranges was issued - expect(flash).toHaveBeenCalledTimes(1); - expect(flash).toHaveBeenCalledWith(expect.any(String), 'warning'); }); }); it('should commit logs and pod data when no pod name defined', () => { - state.timeRange.current = mockDefaultRange; + state.timeRange.current = defaultTimeRange; return testAction(fetchLogs, null, state, expectedMutations, expectedActions, () => { - expect(latestGetParams()).toEqual({}); + expect(latestGetParams()).toEqual({ + start_time: expect.any(String), + end_time: expect.any(String), + }); }); }); }); @@ -249,6 +251,7 @@ describe('Logs Store actions', () => { it('should commit logs and pod data when there is pod name defined', () => { state.pods.current = mockPodName; + state.timeRange.current = mockFixedRange; expectedActions = []; @@ -293,6 +296,10 @@ describe('Logs Store actions', () => { state.search = mockSearch; state.timeRange.current = 'INVALID_TIME_RANGE'; + expectedMutations.splice(1, 0, { + type: types.SHOW_TIME_RANGE_INVALID_WARNING, + }); + return testAction( fetchMoreLogsPrepend, null, @@ -304,15 +311,12 @@ describe('Logs Store actions', () => { pod_name: mockPodName, search: mockSearch, }); - // Warning about time ranges was issued - expect(flash).toHaveBeenCalledTimes(1); - expect(flash).toHaveBeenCalledWith(expect.any(String), 'warning'); }, ); }); it('should commit logs and pod data when no pod name defined', () => { - state.timeRange.current = mockDefaultRange; + state.timeRange.current = defaultTimeRange; return testAction( fetchMoreLogsPrepend, @@ -321,7 +325,10 @@ describe('Logs Store actions', () => { expectedMutations, expectedActions, () => { - expect(latestGetParams()).toEqual({}); + expect(latestGetParams()).toEqual({ + start_time: expect.any(String), + end_time: expect.any(String), + }); }, ); }); @@ -357,6 +364,7 @@ describe('Logs Store actions', () => { it('fetchLogs should commit logs and pod errors', () => { state.environments.options = mockEnvironments; state.environments.current = mockEnvName; + state.timeRange.current = defaultTimeRange; return testAction( fetchLogs, @@ -377,6 +385,7 @@ describe('Logs Store actions', () => { it('fetchMoreLogsPrepend should commit logs and pod errors', () => { state.environments.options = mockEnvironments; state.environments.current = mockEnvName; + state.timeRange.current = defaultTimeRange; return testAction( fetchMoreLogsPrepend, diff --git a/spec/frontend/logs/stores/mutations_spec.js b/spec/frontend/logs/stores/mutations_spec.js index 37db355af09..46561055a4a 100644 --- a/spec/frontend/logs/stores/mutations_spec.js +++ b/spec/frontend/logs/stores/mutations_spec.js @@ -67,6 +67,7 @@ describe('Logs Store Mutations', () => { options: [], isLoading: false, current: null, + fetchError: true, }); }); }); @@ -83,6 +84,7 @@ describe('Logs Store Mutations', () => { expect(state.logs).toEqual({ lines: [], cursor: null, + fetchError: false, isLoading: true, isComplete: false, }); @@ -101,6 +103,7 @@ describe('Logs Store Mutations', () => { isLoading: false, cursor: mockCursor, isComplete: false, + fetchError: false, }); }); @@ -115,6 +118,7 @@ describe('Logs Store Mutations', () => { isLoading: false, cursor: null, isComplete: true, + fetchError: false, }); }); }); @@ -128,6 +132,7 @@ describe('Logs Store Mutations', () => { isLoading: false, cursor: null, isComplete: false, + fetchError: true, }); }); }); @@ -152,6 +157,7 @@ describe('Logs Store Mutations', () => { isLoading: false, cursor: mockCursor, isComplete: false, + fetchError: false, }); }); @@ -171,6 +177,7 @@ describe('Logs Store Mutations', () => { isLoading: false, cursor: mockNextCursor, isComplete: false, + fetchError: false, }); }); @@ -185,6 +192,7 @@ describe('Logs Store Mutations', () => { isLoading: false, cursor: null, isComplete: true, + fetchError: false, }); }); }); @@ -194,6 +202,7 @@ describe('Logs Store Mutations', () => { mutations[types.RECEIVE_LOGS_DATA_PREPEND_ERROR](state); expect(state.logs.isLoading).toBe(false); + expect(state.logs.fetchError).toBe(true); }); }); diff --git a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap index 5b5c9fd714e..97597ed8063 100644 --- a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap +++ b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap @@ -7,17 +7,16 @@ exports[`Repository table row component renders table row 1`] = ` <td class="tree-item-file-name cursor-default position-relative" > - <!----> - <a class="tree-item-link str-truncated" data-qa-selector="file_name_link" href="https://test.com" > - <i - aria-label="file" - class="fa fa-fw mr-1 fa-file-text-o" - role="img" + <file-icon-stub + class="mr-1 position-relative text-secondary" + cssclasses="position-relative file-icon" + filename="test" + size="16" /> <span class="position-relative" @@ -60,17 +59,16 @@ exports[`Repository table row component renders table row for path with special <td class="tree-item-file-name cursor-default position-relative" > - <!----> - <a class="tree-item-link str-truncated" data-qa-selector="file_name_link" href="https://test.com" > - <i - aria-label="file" - class="fa fa-fw mr-1 fa-file-text-o" - role="img" + <file-icon-stub + class="mr-1 position-relative text-secondary" + cssclasses="position-relative file-icon" + filename="test" + size="16" /> <span class="position-relative" diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js index 7bb7ad6e5dd..cb2193e1d9a 100644 --- a/spec/frontend/repository/components/table/row_spec.js +++ b/spec/frontend/repository/components/table/row_spec.js @@ -1,7 +1,7 @@ import { shallowMount, RouterLinkStub } from '@vue/test-utils'; -import { GlBadge, GlLink, GlLoadingIcon } from '@gitlab/ui'; +import { GlBadge, GlLink, GlIcon } from '@gitlab/ui'; import TableRow from '~/repository/components/table/row.vue'; -import Icon from '~/vue_shared/components/icon.vue'; +import FileIcon from '~/vue_shared/components/file_icon.vue'; let vm; let $router; @@ -188,7 +188,8 @@ describe('Repository table row component', () => { vm.setData({ commit: { lockLabel: 'Locked by Root', committedDate: '2019-01-01' } }); return vm.vm.$nextTick().then(() => { - expect(vm.find(Icon).exists()).toBe(true); + expect(vm.find(GlIcon).exists()).toBe(true); + expect(vm.find(GlIcon).props('name')).toBe('lock'); }); }); @@ -202,6 +203,6 @@ describe('Repository table row component', () => { loadingPath: 'test', }); - expect(vm.find(GlLoadingIcon).exists()).toBe(true); + expect(vm.find(FileIcon).props('loading')).toBe(true); }); }); diff --git a/spec/frontend/vue_shared/components/file_icon_spec.js b/spec/frontend/vue_shared/components/file_icon_spec.js index 7b7633a06d6..5a385eee60c 100644 --- a/spec/frontend/vue_shared/components/file_icon_spec.js +++ b/spec/frontend/vue_shared/components/file_icon_spec.js @@ -1,7 +1,6 @@ import { shallowMount } from '@vue/test-utils'; -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; import FileIcon from '~/vue_shared/components/file_icon.vue'; -import Icon from '~/vue_shared/components/icon.vue'; describe('File Icon component', () => { let wrapper; @@ -48,7 +47,7 @@ describe('File Icon component', () => { }); expect(findIcon().exists()).toBe(false); - expect(wrapper.find(Icon).classes()).toContain('folder-icon'); + expect(wrapper.find(GlIcon).classes()).toContain('folder-icon'); }); it('should render a loading icon', () => { diff --git a/spec/javascripts/ide/components/external_link_spec.js b/spec/javascripts/ide/components/external_link_spec.js deleted file mode 100644 index b3d94c041fa..00000000000 --- a/spec/javascripts/ide/components/external_link_spec.js +++ /dev/null @@ -1,35 +0,0 @@ -import Vue from 'vue'; -import externalLink from '~/ide/components/external_link.vue'; -import createVueComponent from '../../helpers/vue_mount_component_helper'; -import { file } from '../helpers'; - -describe('ExternalLink', () => { - const activeFile = file(); - let vm; - - function createComponent() { - const ExternalLink = Vue.extend(externalLink); - - activeFile.permalink = 'test'; - - return createVueComponent(ExternalLink, { - file: activeFile, - }); - } - - afterEach(() => { - vm.$destroy(); - }); - - it('renders the external link with the correct href', done => { - activeFile.binary = true; - vm = createComponent(); - - vm.$nextTick(() => { - const openLink = vm.$el.querySelector('a'); - - expect(openLink.href).toMatch(`/${activeFile.permalink}`); - done(); - }); - }); -}); diff --git a/spec/javascripts/pipelines/graph/graph_component_spec.js b/spec/javascripts/pipelines/graph/graph_component_spec.js index fa6a5f57410..d2c10362ba3 100644 --- a/spec/javascripts/pipelines/graph/graph_component_spec.js +++ b/spec/javascripts/pipelines/graph/graph_component_spec.js @@ -159,7 +159,6 @@ describe('graph component', () => { expect(component.$emit).toHaveBeenCalledWith( 'onClickTriggeredBy', - component.pipeline, component.pipeline.triggered_by[0], ); }); @@ -196,7 +195,6 @@ describe('graph component', () => { expect(component.$emit).toHaveBeenCalledWith( 'onClickTriggered', - component.pipeline, component.pipeline.triggered[0], ); }); diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 3ec6110d789..523e17f82c1 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -34,6 +34,10 @@ describe ApplicationSetting do it { is_expected.to allow_value("dev.gitlab.com").for(:commit_email_hostname) } it { is_expected.not_to allow_value("@dev.gitlab").for(:commit_email_hostname) } + it { is_expected.to allow_value(true).for(:container_expiration_policies_enable_historic_entries) } + it { is_expected.to allow_value(false).for(:container_expiration_policies_enable_historic_entries) } + it { is_expected.not_to allow_value(nil).for(:container_expiration_policies_enable_historic_entries) } + it { is_expected.to allow_value("myemail@gitlab.com").for(:lets_encrypt_notification_email) } it { is_expected.to allow_value(nil).for(:lets_encrypt_notification_email) } it { is_expected.not_to allow_value("notanemail").for(:lets_encrypt_notification_email) } diff --git a/spec/models/ci/group_spec.rb b/spec/models/ci/group_spec.rb index b3b158a111e..5516a1a9c61 100644 --- a/spec/models/ci/group_spec.rb +++ b/spec/models/ci/group_spec.rb @@ -53,7 +53,7 @@ describe Ci::Group do it 'calls the status from the object itself' do expect(jobs.first).to receive(:detailed_status) - expect(subject.detailed_status(double(:user))) + subject.detailed_status(double(:user)) end end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 55af292e8f3..b8034ba5bf2 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -526,14 +526,14 @@ describe Ci::Runner do it 'sets a new last_update value when it is called the first time' do last_update = runner.ensure_runner_queue_value - expect_value_in_queues.to eq(last_update) + expect(value_in_queues).to eq(last_update) end it 'does not change if it is not expired and called again' do last_update = runner.ensure_runner_queue_value expect(runner.ensure_runner_queue_value).to eq(last_update) - expect_value_in_queues.to eq(last_update) + expect(value_in_queues).to eq(last_update) end context 'updates runner queue after changing editable value' do @@ -544,7 +544,7 @@ describe Ci::Runner do end it 'sets a new last_update value' do - expect_value_in_queues.not_to eq(last_update) + expect(value_in_queues).not_to eq(last_update) end end @@ -556,14 +556,14 @@ describe Ci::Runner do end it 'has an old last_update value' do - expect_value_in_queues.to eq(last_update) + expect(value_in_queues).to eq(last_update) end end - def expect_value_in_queues + def value_in_queues Gitlab::Redis::SharedState.with do |redis| runner_queue_key = runner.send(:runner_queue_key) - expect(redis.get(runner_queue_key)) + redis.get(runner_queue_key) end end end diff --git a/spec/models/users_statistics_spec.rb b/spec/models/users_statistics_spec.rb index fc23bed711f..4437a5469c6 100644 --- a/spec/models/users_statistics_spec.rb +++ b/spec/models/users_statistics_spec.rb @@ -2,7 +2,36 @@ require 'spec_helper' -RSpec.describe UsersStatistics do +describe UsersStatistics do + let(:users_statistics) { build(:users_statistics) } + + describe 'scopes' do + describe '.order_created_at_desc' do + it 'returns the entries ordered by created at descending' do + users_statistics1 = create(:users_statistics, created_at: Time.current) + users_statistics2 = create(:users_statistics, created_at: Time.current - 2.days) + users_statistics3 = create(:users_statistics, created_at: Time.current - 5.hours) + + expect(described_class.order_created_at_desc).to eq( + [ + users_statistics1, + users_statistics3, + users_statistics2 + ] + ) + end + end + end + + describe '.latest' do + it 'returns the latest entry' do + create(:users_statistics, created_at: Time.current - 1.day) + users_statistics = create(:users_statistics, created_at: Time.current) + + expect(described_class.latest).to eq(users_statistics) + end + end + describe '.create_current_stats!' do before do create_list(:user_highest_role, 4) @@ -40,4 +69,16 @@ RSpec.describe UsersStatistics do end end end + + describe '#active' do + it 'sums users statistics values without the value for blocked' do + expect(users_statistics.active).to eq(71) + end + end + + describe '#total' do + it 'sums all users statistics values' do + expect(users_statistics.total).to eq(78) + end + end end diff --git a/spec/presenters/ci/pipeline_presenter_spec.rb b/spec/presenters/ci/pipeline_presenter_spec.rb index c9c4f567549..28eb6804703 100644 --- a/spec/presenters/ci/pipeline_presenter_spec.rb +++ b/spec/presenters/ci/pipeline_presenter_spec.rb @@ -7,7 +7,7 @@ describe Ci::PipelinePresenter do let(:user) { create(:user) } let(:current_user) { user } - let(:project) { create(:project) } + let(:project) { create(:project, :test_repo) } let(:pipeline) { create(:ci_pipeline, project: project) } subject(:presenter) do @@ -87,34 +87,32 @@ describe Ci::PipelinePresenter do end describe '#name' do + before do + allow(pipeline).to receive(:merge_request_event_type) { event_type } + end + subject { presenter.name } - context 'when pipeline is detached merge request pipeline' do - let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) } - let(:pipeline) { merge_request.all_pipelines.last } + context 'for a detached merge request pipeline' do + let(:event_type) { :detached } it { is_expected.to eq('Detached merge request pipeline') } end - context 'when pipeline is merge request pipeline' do - let(:merge_request) { create(:merge_request, :with_merge_request_pipeline) } - let(:pipeline) { merge_request.all_pipelines.last } + context 'for a merged result pipeline' do + let(:event_type) { :merged_result } it { is_expected.to eq('Merged result pipeline') } end - context 'when pipeline is merge train pipeline' do - let(:pipeline) { create(:ci_pipeline, project: project) } - - before do - allow(pipeline).to receive(:merge_request_event_type) { :merge_train } - end + context 'for a merge train pipeline' do + let(:event_type) { :merge_train } it { is_expected.to eq('Merge train pipeline') } end context 'when pipeline is branch pipeline' do - let(:pipeline) { create(:ci_pipeline, project: project) } + let(:event_type) { nil } it { is_expected.to eq('Pipeline') } end @@ -145,8 +143,6 @@ describe Ci::PipelinePresenter do end context 'when pipeline is branch pipeline' do - let(:pipeline) { create(:ci_pipeline, project: project) } - context 'when ref exists in the repository' do before do allow(pipeline).to receive(:ref_exists?) { true } @@ -165,7 +161,7 @@ describe Ci::PipelinePresenter do end end - context 'when ref exists in the repository' do + context 'when ref does not exist in the repository' do before do allow(pipeline).to receive(:ref_exists?) { false } end @@ -188,12 +184,17 @@ describe Ci::PipelinePresenter do describe '#all_related_merge_request_text' do subject { presenter.all_related_merge_request_text } + let(:mr_1) { create(:merge_request) } + let(:mr_2) { create(:merge_request) } + context 'with zero related merge requests (branch pipeline)' do it { is_expected.to eq('No related merge requests found.') } end context 'with one related merge request' do - let!(:mr_1) { create(:merge_request, project: project, source_project: project) } + before do + allow(pipeline).to receive(:all_merge_requests).and_return(MergeRequest.where(id: mr_1.id)) + end it { is_expected.to eq("1 related merge request: " \ @@ -202,8 +203,9 @@ describe Ci::PipelinePresenter do end context 'with two related merge requests' do - let!(:mr_1) { create(:merge_request, project: project, source_project: project, target_branch: 'staging') } - let!(:mr_2) { create(:merge_request, project: project, source_project: project, target_branch: 'feature') } + before do + allow(pipeline).to receive(:all_merge_requests).and_return(MergeRequest.where(id: [mr_1.id, mr_2.id])) + end it { is_expected.to eq("2 related merge requests: " \ @@ -223,22 +225,25 @@ describe Ci::PipelinePresenter do end describe '#all_related_merge_requests' do + subject(:all_related_merge_requests) do + presenter.send(:all_related_merge_requests) + end + it 'memoizes the returned relation' do - query_count = ActiveRecord::QueryRecorder.new do - 3.times { presenter.send(:all_related_merge_requests).count } - end.count + expect(pipeline).to receive(:all_merge_requests_by_recency).exactly(1).time.and_call_original + 2.times { presenter.send(:all_related_merge_requests).count } + end + + context 'for a branch pipeline with two open MRs' do + let!(:one) { create(:merge_request, source_project: project, source_branch: pipeline.ref) } + let!(:two) { create(:merge_request, source_project: project, source_branch: pipeline.ref, target_branch: 'wip') } - expect(query_count).to eq(2) + it { is_expected.to contain_exactly(one, two) } end context 'permissions' do - let!(:merge_request) do - create(:merge_request, project: project, source_project: project) - end - - subject(:all_related_merge_requests) do - presenter.send(:all_related_merge_requests) - end + let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline, source_project: project) } + let(:pipeline) { merge_request.all_pipelines.take } shared_examples 'private merge requests' do context 'when not logged in' do @@ -315,61 +320,51 @@ describe Ci::PipelinePresenter do describe '#link_to_merge_request' do subject { presenter.link_to_merge_request } - let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) } - let(:pipeline) { merge_request.all_pipelines.last } + context 'with a related merge request' do + let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline, source_project: project) } + let(:pipeline) { merge_request.all_pipelines.take } - it 'returns a correct link' do - is_expected - .to include(project_merge_request_path(merge_request.project, merge_request)) + it 'returns a correct link' do + is_expected.to include(project_merge_request_path(project, merge_request)) + end end context 'when pipeline is branch pipeline' do - let(:pipeline) { create(:ci_pipeline, project: project) } - - it 'returns nothing' do - is_expected.to be_nil - end + it { is_expected.to be_nil } end end describe '#link_to_merge_request_source_branch' do subject { presenter.link_to_merge_request_source_branch } - let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) } - let(:pipeline) { merge_request.all_pipelines.last } + context 'with a related merge request' do + let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline, source_project: project) } + let(:pipeline) { merge_request.all_pipelines.take } - it 'returns a correct link' do - is_expected - .to include(project_commits_path(merge_request.source_project, - merge_request.source_branch)) + it 'returns a correct link' do + is_expected.to include(project_commits_path(project, merge_request.source_branch)) + end end context 'when pipeline is branch pipeline' do - let(:pipeline) { create(:ci_pipeline, project: project) } - - it 'returns nothing' do - is_expected.to be_nil - end + it { is_expected.to be_nil } end end describe '#link_to_merge_request_target_branch' do subject { presenter.link_to_merge_request_target_branch } - let(:merge_request) { create(:merge_request, :with_merge_request_pipeline) } - let(:pipeline) { merge_request.all_pipelines.last } + context 'with a related merge request' do + let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline, source_project: project) } + let(:pipeline) { merge_request.all_pipelines.take } - it 'returns a correct link' do - is_expected - .to include(project_commits_path(merge_request.target_project, merge_request.target_branch)) + it 'returns a correct link' do + is_expected.to include(project_commits_path(project, merge_request.target_branch)) + end end context 'when pipeline is branch pipeline' do - let(:pipeline) { create(:ci_pipeline, project: project) } - - it 'returns nothing' do - is_expected.to be_nil - end + it { is_expected.to be_nil } end end end diff --git a/spec/requests/api/internal/pages_spec.rb b/spec/requests/api/internal/pages_spec.rb index 0c3c2fa22d6..fecf15c29c2 100644 --- a/spec/requests/api/internal/pages_spec.rb +++ b/spec/requests/api/internal/pages_spec.rb @@ -3,13 +3,36 @@ require 'spec_helper' describe API::Internal::Pages do - describe "GET /internal/pages" do - let(:pages_secret) { SecureRandom.random_bytes(Gitlab::Pages::SECRET_LENGTH) } + let(:auth_headers) do + jwt_token = JWT.encode({ 'iss' => 'gitlab-pages' }, Gitlab::Pages.secret, 'HS256') + { Gitlab::Pages::INTERNAL_API_REQUEST_HEADER => jwt_token } + end + let(:pages_secret) { SecureRandom.random_bytes(Gitlab::Pages::SECRET_LENGTH) } + + before do + allow(Gitlab::Pages).to receive(:secret).and_return(pages_secret) + end + + describe "GET /internal/pages/status" do + def query_enabled(headers = {}) + get api("/internal/pages/status"), headers: headers + end - before do - allow(Gitlab::Pages).to receive(:secret).and_return(pages_secret) + it 'responds with 401 Unauthorized' do + query_enabled + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'responds with 204 no content' do + query_enabled(auth_headers) + + expect(response).to have_gitlab_http_status(:no_content) + expect(response.body).to be_empty end + end + describe "GET /internal/pages" do def query_host(host, headers = {}) get api("/internal/pages"), headers: headers, params: { host: host } end diff --git a/spec/services/ci/expire_pipeline_cache_service_spec.rb b/spec/services/ci/expire_pipeline_cache_service_spec.rb index 7b9d6ed4f41..78e1ba0109a 100644 --- a/spec/services/ci/expire_pipeline_cache_service_spec.rb +++ b/spec/services/ci/expire_pipeline_cache_service_spec.rb @@ -22,19 +22,19 @@ describe Ci::ExpirePipelineCacheService do end it 'invalidates Etag caching for merge request pipelines if pipeline runs on any commit of that source branch' do - pipeline = create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master') - merge_request = create(:merge_request, source_project: project, source_branch: pipeline.ref) + merge_request = create(:merge_request, :with_detached_merge_request_pipeline) + project = merge_request.target_project + merge_request_pipelines_path = "/#{project.full_path}/-/merge_requests/#{merge_request.iid}/pipelines.json" allow_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch) expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(merge_request_pipelines_path) - subject.execute(pipeline) + subject.execute(merge_request.all_pipelines.last) end it 'updates the cached status for a project' do - expect(Gitlab::Cache::Ci::ProjectPipelineStatus).to receive(:update_for_pipeline) - .with(pipeline) + expect(Gitlab::Cache::Ci::ProjectPipelineStatus).to receive(:update_for_pipeline).with(pipeline) subject.execute(pipeline) end diff --git a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb index 4d87fa3e832..0cec1e7be22 100644 --- a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb +++ b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb @@ -8,10 +8,6 @@ describe MergeRequests::AddTodoWhenBuildFailsService do let(:sha) { '1234567890abcdef1234567890abcdef12345678' } let(:ref) { merge_request.source_branch } - let(:pipeline) do - create(:ci_pipeline, ref: ref, project: project, sha: sha) - end - let(:service) do described_class.new(project, user, commit_message: 'Awesome message') end @@ -19,12 +15,11 @@ describe MergeRequests::AddTodoWhenBuildFailsService do let(:todo_service) { spy('todo service') } let(:merge_request) do - create(:merge_request, merge_user: user, - source_branch: 'master', - target_branch: 'feature', - source_project: project, - target_project: project, - state: 'opened') + create(:merge_request, :with_detached_merge_request_pipeline, :opened, merge_user: user) + end + + let(:pipeline) do + merge_request.all_pipelines.take end before do diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb index a664719783a..216d9170274 100644 --- a/spec/services/users/destroy_service_spec.rb +++ b/spec/services/users/destroy_service_spec.rb @@ -15,7 +15,7 @@ describe Users::DestroyService do it 'deletes the user' do user_data = service.execute(user) - expect { user_data['email'].to eq(user.email) } + expect(user_data['email']).to eq(user.email) expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound) expect { Namespace.find(namespace.id) }.to raise_error(ActiveRecord::RecordNotFound) end |