summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-04-08 09:09:43 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-04-08 09:09:43 +0000
commitf5050253469fc0961c02deec0e698ad62bdd9de5 (patch)
tree30bbd8f8b556fd5b730f0123921138ee1d6bdaa2
parentf6cdec670b9b757fc2225a2c6627ab79765e5b8a (diff)
downloadgitlab-ce-f5050253469fc0961c02deec0e698ad62bdd9de5.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo.yml8
-rw-r--r--Gemfile4
-rw-r--r--Gemfile.lock10
-rw-r--r--app/assets/javascripts/ide/components/external_link.vue34
-rw-r--r--app/assets/javascripts/logs/components/environment_logs.vue40
-rw-r--r--app/assets/javascripts/logs/stores/actions.js35
-rw-r--r--app/assets/javascripts/logs/stores/mutation_types.js6
-rw-r--r--app/assets/javascripts/logs/stores/mutations.js15
-rw-r--r--app/assets/javascripts/logs/stores/state.js5
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue24
-rw-r--r--app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js25
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js8
-rw-r--r--app/assets/javascripts/pipelines/stores/pipeline_store.js12
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue44
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon.vue24
-rw-r--r--app/assets/stylesheets/framework/common.scss3
-rw-r--r--app/assets/stylesheets/framework/mixins.scss6
-rw-r--r--app/assets/stylesheets/framework/variables.scss8
-rw-r--r--app/assets/stylesheets/pages/builds.scss51
-rw-r--r--app/assets/stylesheets/pages/environment_logs.scss58
-rw-r--r--app/assets/stylesheets/pages/tree.scss6
-rw-r--r--app/controllers/admin/dashboard_controller.rb4
-rw-r--r--app/helpers/application_helper.rb1
-rw-r--r--app/helpers/application_settings_helper.rb1
-rw-r--r--app/models/application_setting.rb3
-rw-r--r--app/models/application_setting_implementation.rb1
-rw-r--r--app/models/users_statistics.rb33
-rw-r--r--app/views/admin/application_settings/_registry.html.haml9
-rw-r--r--app/views/admin/dashboard/index.html.haml2
-rw-r--r--app/views/admin/dashboard/stats.html.haml75
-rw-r--r--changelogs/unreleased/119017-unable-to-expand-multiple-downstream-pipelines.yml5
-rw-r--r--changelogs/unreleased/208735-container-expiration-policy-app-setting.yml6
-rw-r--r--changelogs/unreleased/210502-restore-full-height-of-logs-explorer.yml5
-rw-r--r--changelogs/unreleased/add-internal-api-pages-enabled.yml5
-rw-r--r--changelogs/unreleased/ph-treeFileIcons.yml5
-rw-r--r--changelogs/unreleased/use_users_statistics_table_in_view.yml5
-rw-r--r--config/routes/admin.rb2
-rw-r--r--db/migrate/20200331195952_add_container_expiration_policies_enable_historic_entries_to_application_settings.rb22
-rw-r--r--db/structure.sql4
-rw-r--r--doc/administration/packages/container_registry.md4
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql142
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json409
-rw-r--r--doc/api/graphql/reference/index.md19
-rw-r--r--doc/api/settings.md1
-rw-r--r--doc/ci/triggers/README.md16
-rw-r--r--doc/user/admin_area/index.md3
-rw-r--r--doc/user/admin_area/settings/index.md2
-rw-r--r--doc/user/packages/container_registry/index.md13
-rw-r--r--lib/api/helpers/internal_helpers.rb2
-rw-r--r--lib/api/internal/pages.rb7
-rw-r--r--locale/gitlab.pot49
-rw-r--r--spec/factories/users_statistics.rb8
-rw-r--r--spec/features/admin/dashboard_spec.rb26
-rw-r--r--spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb2
-rw-r--r--spec/frontend/logs/components/environment_logs_spec.js15
-rw-r--r--spec/frontend/logs/stores/actions_spec.js37
-rw-r--r--spec/frontend/logs/stores/mutations_spec.js9
-rw-r--r--spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap22
-rw-r--r--spec/frontend/repository/components/table/row_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/file_icon_spec.js5
-rw-r--r--spec/javascripts/ide/components/external_link_spec.js35
-rw-r--r--spec/javascripts/pipelines/graph/graph_component_spec.js2
-rw-r--r--spec/models/application_setting_spec.rb4
-rw-r--r--spec/models/ci/group_spec.rb2
-rw-r--r--spec/models/ci/runner_spec.rb12
-rw-r--r--spec/models/users_statistics_spec.rb43
-rw-r--r--spec/presenters/ci/pipeline_presenter_spec.rb119
-rw-r--r--spec/requests/api/internal/pages_spec.rb31
-rw-r--r--spec/services/ci/expire_pipeline_cache_service_spec.rb10
-rw-r--r--spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb15
-rw-r--r--spec/services/users/destroy_service_spec.rb2
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:
diff --git a/Gemfile b/Gemfile
index eab8e2043a5..906709656f3 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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