Commit f5050253 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent f6cdec67
...@@ -269,14 +269,6 @@ RSpec/ScatteredSetup: ...@@ -269,14 +269,6 @@ RSpec/ScatteredSetup:
- 'spec/requests/api/jobs_spec.rb' - 'spec/requests/api/jobs_spec.rb'
- 'spec/services/projects/create_service_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 # Offense count: 10
# Cop supports --auto-correct. # Cop supports --auto-correct.
Rails/ApplicationController: Rails/ApplicationController:
......
...@@ -457,9 +457,9 @@ end ...@@ -457,9 +457,9 @@ end
# Gitaly GRPC protocol definitions # Gitaly GRPC protocol definitions
gem 'gitaly', '~> 12.9.0.pre.rc4' 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' gem 'toml-rb', '~> 1.0.0'
......
...@@ -427,7 +427,7 @@ GEM ...@@ -427,7 +427,7 @@ GEM
mime-types (~> 3.0) mime-types (~> 3.0)
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.0) retriable (>= 2.0, < 4.0)
google-protobuf (3.11.4) google-protobuf (3.8.0)
googleapis-common-protos-types (1.0.4) googleapis-common-protos-types (1.0.4)
google-protobuf (~> 3.0) google-protobuf (~> 3.0)
googleauth (0.6.6) googleauth (0.6.6)
...@@ -468,8 +468,8 @@ GEM ...@@ -468,8 +468,8 @@ GEM
graphql (~> 1.6) graphql (~> 1.6)
html-pipeline (~> 2.8) html-pipeline (~> 2.8)
sass (~> 3.4) sass (~> 3.4)
grpc (1.27.0) grpc (1.24.0)
google-protobuf (~> 3.11) google-protobuf (~> 3.8)
googleapis-common-protos-types (~> 1.0) googleapis-common-protos-types (~> 1.0)
gssapi (1.2.0) gssapi (1.2.0)
ffi (>= 1.0.1) ffi (>= 1.0.1)
...@@ -1251,7 +1251,7 @@ DEPENDENCIES ...@@ -1251,7 +1251,7 @@ DEPENDENCIES
gitlab_omniauth-ldap (~> 2.1.1) gitlab_omniauth-ldap (~> 2.1.1)
gon (~> 6.2) gon (~> 6.2)
google-api-client (~> 0.23) google-api-client (~> 0.23)
google-protobuf (~> 3.11.2) google-protobuf (~> 3.8.0)
gpgme (~> 2.0.19) gpgme (~> 2.0.19)
grape (~> 1.1.0) grape (~> 1.1.0)
grape-entity (~> 0.7.1) grape-entity (~> 0.7.1)
...@@ -1260,7 +1260,7 @@ DEPENDENCIES ...@@ -1260,7 +1260,7 @@ DEPENDENCIES
graphiql-rails (~> 1.4.10) graphiql-rails (~> 1.4.10)
graphql (~> 1.10.5) graphql (~> 1.10.5)
graphql-docs (~> 1.6.0) graphql-docs (~> 1.6.0)
grpc (~> 1.27.0) grpc (~> 1.24.0)
gssapi gssapi
guard-rspec guard-rspec
haml_lint (~> 0.34.0) haml_lint (~> 0.34.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>
...@@ -56,7 +56,6 @@ export default { ...@@ -56,7 +56,6 @@ export default {
required: true, required: true,
}, },
}, },
traceHeight: 600,
data() { data() {
return { return {
isElasticStackCalloutDismissed: false, isElasticStackCalloutDismissed: false,
...@@ -94,6 +93,9 @@ export default { ...@@ -94,6 +93,9 @@ export default {
'showEnvironment', 'showEnvironment',
'fetchEnvironments', 'fetchEnvironments',
'fetchMoreLogsPrepend', 'fetchMoreLogsPrepend',
'dismissRequestEnvironmentsError',
'dismissInvalidTimeRangeWarning',
'dismissRequestLogsError',
]), ]),
isCurrentEnvironment(envName) { isCurrentEnvironment(envName) {
...@@ -115,7 +117,7 @@ export default { ...@@ -115,7 +117,7 @@ export default {
}; };
</script> </script>
<template> <template>
<div class="environment-logs-viewer mt-3"> <div class="environment-logs-viewer d-flex flex-column py-3">
<gl-alert <gl-alert
v-if="shouldShowElasticStackCallout" v-if="shouldShowElasticStackCallout"
class="mb-3 js-elasticsearch-alert" class="mb-3 js-elasticsearch-alert"
...@@ -132,6 +134,31 @@ export default { ...@@ -132,6 +134,31 @@ export default {
</strong> </strong>
</a> </a>
</gl-alert> </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="top-bar d-md-flex border bg-secondary-50 pt-2 pr-1 pb-0 pl-2">
<div class="flex-grow-0"> <div class="flex-grow-0">
<gl-dropdown <gl-dropdown
...@@ -183,16 +210,15 @@ export default { ...@@ -183,16 +210,15 @@ export default {
<gl-infinite-scroll <gl-infinite-scroll
ref="infiniteScroll" ref="infiniteScroll"
class="log-lines" class="log-lines overflow-auto flex-grow-1 min-height-0"
:style="{ height: `${$options.traceHeight}px` }"
:max-list-height="$options.traceHeight"
:fetched-items="logs.lines.length" :fetched-items="logs.lines.length"
@topReached="topReached" @topReached="topReached"
@scroll="scroll" @scroll="scroll"
> >
<template #items> <template #items>
<pre <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"> ><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>
<div class="dot"></div> <div class="dot"></div>
...@@ -205,7 +231,7 @@ export default { ...@@ -205,7 +231,7 @@ export default {
></template> ></template>
</gl-infinite-scroll> </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}.')"> <gl-sprintf :message="s__('Environments|Logs from %{start} to %{end}.')">
<template #start>{{ timeRange.current.start | formatDate }}</template> <template #start>{{ timeRange.current.start | formatDate }}</template>
<template #end>{{ timeRange.current.end | formatDate }}</template> <template #end>{{ timeRange.current.end | formatDate }}</template>
......
import { backOff } from '~/lib/utils/common_utils'; import { backOff } from '~/lib/utils/common_utils';
import httpStatusCodes from '~/lib/utils/http_status'; import httpStatusCodes from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { s__ } from '~/locale';
import { convertToFixedRange } from '~/lib/utils/datetime_range'; import { convertToFixedRange } from '~/lib/utils/datetime_range';
import * as types from './mutation_types'; 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) => const requestUntilData = (url, params) =>
backOff((next, stop) => { backOff((next, stop) => {
axios axios
...@@ -31,7 +21,7 @@ const requestUntilData = (url, params) => ...@@ -31,7 +21,7 @@ const requestUntilData = (url, params) =>
}); });
}); });
const requestLogsUntilData = state => { const requestLogsUntilData = ({ commit, state }) => {
const params = {}; const params = {};
const { logs_api_path } = state.environments.options.find( const { logs_api_path } = state.environments.options.find(
({ name }) => name === state.environments.current, ({ name }) => name === state.environments.current,
...@@ -49,7 +39,7 @@ const requestLogsUntilData = state => { ...@@ -49,7 +39,7 @@ const requestLogsUntilData = state => {
params.start_time = start; params.start_time = start;
params.end_time = end; params.end_time = end;
} catch { } catch {
flashTimeRangeWarning(); commit(types.SHOW_TIME_RANGE_INVALID_WARNING);
} }
} }
if (state.logs.cursor) { if (state.logs.cursor) {
...@@ -101,26 +91,22 @@ export const fetchEnvironments = ({ commit, dispatch }, environmentsPath) => { ...@@ -101,26 +91,22 @@ export const fetchEnvironments = ({ commit, dispatch }, environmentsPath) => {
}) })
.catch(() => { .catch(() => {
commit(types.RECEIVE_ENVIRONMENTS_DATA_ERROR); 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 }) => { export const fetchLogs = ({ commit, state }) => {
commit(types.REQUEST_LOGS_DATA); commit(types.REQUEST_LOGS_DATA);
return requestLogsUntilData(state) return requestLogsUntilData({ commit, state })
.then(({ data }) => { .then(({ data }) => {
const { pod_name, pods, logs, cursor } = data; const { pod_name, pods, logs, cursor } = data;
commit(types.RECEIVE_LOGS_DATA_SUCCESS, { logs, cursor }); commit(types.RECEIVE_LOGS_DATA_SUCCESS, { logs, cursor });
commit(types.SET_CURRENT_POD_NAME, pod_name); commit(types.SET_CURRENT_POD_NAME, pod_name);
commit(types.RECEIVE_PODS_DATA_SUCCESS, pods); commit(types.RECEIVE_PODS_DATA_SUCCESS, pods);
}) })
.catch(() => { .catch(() => {
commit(types.RECEIVE_PODS_DATA_ERROR); commit(types.RECEIVE_PODS_DATA_ERROR);
commit(types.RECEIVE_LOGS_DATA_ERROR); commit(types.RECEIVE_LOGS_DATA_ERROR);
flashLogsError();
}); });
}; };
...@@ -132,16 +118,27 @@ export const fetchMoreLogsPrepend = ({ commit, state }) => { ...@@ -132,16 +118,27 @@ export const fetchMoreLogsPrepend = ({ commit, state }) => {
commit(types.REQUEST_LOGS_DATA_PREPEND); commit(types.REQUEST_LOGS_DATA_PREPEND);
return requestLogsUntilData(state) return requestLogsUntilData({ commit, state })
.then(({ data }) => { .then(({ data }) => {
const { logs, cursor } = data; const { logs, cursor } = data;
commit(types.RECEIVE_LOGS_DATA_PREPEND_SUCCESS, { logs, cursor }); commit(types.RECEIVE_LOGS_DATA_PREPEND_SUCCESS, { logs, cursor });
}) })
.catch(() => { .catch(() => {
commit(types.RECEIVE_LOGS_DATA_PREPEND_ERROR); 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 // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
export const SET_PROJECT_ENVIRONMENT = 'SET_PROJECT_ENVIRONMENT'; export const SET_PROJECT_ENVIRONMENT = 'SET_PROJECT_ENVIRONMENT';
export const SET_SEARCH = 'SET_SEARCH'; export const SET_SEARCH = 'SET_SEARCH';
export const SET_TIME_RANGE = 'SET_TIME_RANGE'; 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 SET_CURRENT_POD_NAME = 'SET_CURRENT_POD_NAME';
export const REQUEST_ENVIRONMENTS_DATA = 'REQUEST_ENVIRONMENTS_DATA'; export const REQUEST_ENVIRONMENTS_DATA = 'REQUEST_ENVIRONMENTS_DATA';
export const RECEIVE_ENVIRONMENTS_DATA_SUCCESS = 'RECEIVE_ENVIRONMENTS_DATA_SUCCESS'; export const RECEIVE_ENVIRONMENTS_DATA_SUCCESS = 'RECEIVE_ENVIRONMENTS_DATA_SUCCESS';
export const RECEIVE_ENVIRONMENTS_DATA_ERROR = 'RECEIVE_ENVIRONMENTS_DATA_ERROR'; 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 REQUEST_LOGS_DATA = 'REQUEST_LOGS_DATA';
export const RECEIVE_LOGS_DATA_SUCCESS = 'RECEIVE_LOGS_DATA_SUCCESS'; export const RECEIVE_LOGS_DATA_SUCCESS = 'RECEIVE_LOGS_DATA_SUCCESS';
...@@ -13,6 +18,7 @@ export const RECEIVE_LOGS_DATA_ERROR = 'RECEIVE_LOGS_DATA_ERROR'; ...@@ -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 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_SUCCESS = 'RECEIVE_LOGS_DATA_PREPEND_SUCCESS';
export const RECEIVE_LOGS_DATA_PREPEND_ERROR = 'RECEIVE_LOGS_DATA_PREPEND_ERROR'; 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_SUCCESS = 'RECEIVE_PODS_DATA_SUCCESS';
export const RECEIVE_PODS_DATA_ERROR = 'RECEIVE_PODS_DATA_ERROR'; export const RECEIVE_PODS_DATA_ERROR = 'RECEIVE_PODS_DATA_ERROR';
...@@ -18,6 +18,12 @@ export default { ...@@ -18,6 +18,12 @@ export default {
state.timeRange.selected = timeRange; state.timeRange.selected = timeRange;
state.timeRange.current = convertToFixedRange(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 // Environments Data
[types.SET_PROJECT_ENVIRONMENT](state, environmentName) { [types.SET_PROJECT_ENVIRONMENT](state, environmentName) {
...@@ -38,6 +44,10 @@ export default { ...@@ -38,6 +44,10 @@ export default {
[types.RECEIVE_ENVIRONMENTS_DATA_ERROR](state) { [types.RECEIVE_ENVIRONMENTS_DATA_ERROR](state) {
state.environments.options = []; state.environments.options = [];
state.environments.isLoading = false; state.environments.isLoading = false;
state.environments.fetchError = true;
},
[types.HIDE_REQUEST_ENVIRONMENTS_ERROR](state) {
state.environments.fetchError = false;
}, },
// Logs data // Logs data
...@@ -63,6 +73,7 @@ export default { ...@@ -63,6 +73,7 @@ export default {
[types.RECEIVE_LOGS_DATA_ERROR](state) { [types.RECEIVE_LOGS_DATA_ERROR](state) {
state.logs.lines = []; state.logs.lines = [];
state.logs.isLoading = false; state.logs.isLoading = false;
state.logs.fetchError = true;
}, },
[types.REQUEST_LOGS_DATA_PREPEND](state) { [types.REQUEST_LOGS_DATA_PREPEND](state) {
...@@ -80,6 +91,10 @@ export default { ...@@ -80,6 +91,10 @@ export default {
}, },
[types.RECEIVE_LOGS_DATA_PREPEND_ERROR](state) { [types.RECEIVE_LOGS_DATA_PREPEND_ERROR](state) {
state.logs.isLoading = false; state.logs.isLoading = false;
state.logs.fetchError = true;
},
[types.HIDE_REQUEST_LOGS_ERROR](state) {
state.logs.fetchError = false;
}, },
// Pods data // Pods data
......
...@@ -16,6 +16,8 @@ export default () => ({ ...@@ -16,6 +16,8 @@ export default () => ({
selected: defaultTimeRange, selected: defaultTimeRange,
// Current time range, must be fixed // Current time range, must be fixed
current: convertToFixedRange(defaultTimeRange), current: convertToFixedRange(defaultTimeRange),
invalidWarning: false,
}, },
/** /**
...@@ -25,6 +27,7 @@ export default () => ({ ...@@ -25,6 +27,7 @@ export default () => ({
options: [], options: [],
isLoading: false, isLoading: false,
current: null, current: null,
fetchError: false,
}, },
/** /**
...@@ -39,6 +42,8 @@ export default () => ({ ...@@ -39,6 +42,8 @@ export default () => ({
*/ */
cursor: null, cursor: null,
isComplete: false, isComplete: false,
fetchError: false,
}, },
/** /**
......
...@@ -99,7 +99,17 @@ export default { ...@@ -99,7 +99,17 @@ export default {
downstreamNode.classList.contains('child-pipeline') ? 15 : 30, 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) { calculateMarginTop(downstreamNode, pixelDiff) {
return `${downstreamNode.offsetTop - downstreamNode.offsetParent.offsetTop - pixelDiff}px`; return `${downstreamNode.offsetTop - downstreamNode.offsetParent.offsetTop - pixelDiff}px`;
...@@ -136,9 +146,7 @@ export default { ...@@ -136,9 +146,7 @@ export default {
:pipeline="expandedTriggeredBy" :pipeline="expandedTriggeredBy"
:is-linked-pipeline="true" :is-linked-pipeline="true"
:mediator="mediator" :mediator="mediator"
@onClickTriggeredBy=" @onClickTriggeredBy="clickTriggeredByPipeline"
(parentPipeline, pipeline) => clickTriggeredByPipeline(parentPipeline, pipeline)
"
@refreshPipelineGraph="requestRefreshPipelineGraph" @refreshPipelineGraph="requestRefreshPipelineGraph"
/> />
...@@ -148,9 +156,7 @@ export default { ...@@ -148,9 +156,7 @@ export default {
:column-title="__('Upstream')" :column-title="__('Upstream')"
:project-id="pipelineProjectId" :project-id="pipelineProjectId"
graph-position="left" graph-position="left"
@linkedPipelineClick=" @linkedPipelineClick="$emit('onClickTriggeredBy', $event)"
linkedPipeline => $emit('onClickTriggeredBy', pipeline, linkedPipeline)
"
/> />
<ul <ul
...@@ -197,9 +203,7 @@ export default { ...@@ -197,9 +203,7 @@ export default {
:is-linked-pipeline="true" :is-linked-pipeline="true"
:style="{ 'margin-top': downstreamMarginTop }" :style="{ 'margin-top': downstreamMarginTop }"
:mediator="mediator" :mediator="mediator"
@onClickTriggered=" @onClickTriggered="clickTriggeredPipeline"
(parentPipeline, pipeline) => clickTriggeredPipeline(parentPipeline, pipeline)
"
@refreshPipelineGraph="requestRefreshPipelineGraph" @refreshPipelineGraph="requestRefreshPipelineGraph"
/> />
</div> </div>
......
...@@ -27,9 +27,9 @@ export default { ...@@ -27,9 +27,9 @@ export default {
* @param {String} resetStoreKey Store key for the visible pipeline that will need to be reset * @param {String} resetStoreKey Store key for the visible pipeline that will need to be reset
* @param {Object} pipeline The clicked pipeline * @param {Object} pipeline The clicked pipeline
*/ */
clickPipeline(parentPipeline, pipeline, openMethod, closeMethod) { clickPipeline(pipeline, openMethod, closeMethod) {
if (!pipeline.isExpanded) { if (!pipeline.isExpanded) {
this.mediator.store[openMethod](parentPipeline, pipeline); this.mediator.store[openMethod](pipeline);
this.mediator.store.toggleLoading(pipeline); this.mediator.store.toggleLoading(pipeline);
this.mediator.poll.stop(); this.mediator.poll.stop();
...@@ -41,21 +41,14 @@ export default { ...@@ -41,21 +41,14 @@ export default {
this.mediator.poll.enable({ data: this.mediator.getExpandedParameters() }); this.mediator.poll.enable({ data: this.mediator.getExpandedParameters() });
} }
}, },
clickTriggeredByPipeline(parentPipeline, pipeline) { resetTriggeredPipelines(parentPipeline, pipeline) {
this.clickPipeline( this.mediator.store.resetTriggeredPipelines(parentPipeline, pipeline);
parentPipeline,
pipeline,
'openTriggeredByPipeline',
'closeTriggeredByPipeline',
);
}, },
clickTriggeredPipeline(parentPipeline, pipeline) { clickTriggeredByPipeline(pipeline) {
this.clickPipeline( this.clickPipeline(pipeline, 'openPipeline', 'closePipeline');
parentPipeline, },
pipeline, clickTriggeredPipeline(pipeline) {
'openTriggeredPipeline', this.clickPipeline(pipeline, 'openPipeline', 'closePipeline');
'closeTriggeredPipeline',
);
}, },
requestRefreshPipelineGraph() { requestRefreshPipelineGraph() {
// When an action is clicked // When an action is clicked
......
...@@ -42,10 +42,10 @@ export default () => { ...@@ -42,10 +42,10 @@ export default () => {
}, },
on: { on: {
refreshPipelineGraph: this.requestRefreshPipelineGraph, refreshPipelineGraph: this.requestRefreshPipelineGraph,
onClickTriggeredBy: (parentPipeline, pipeline) => onResetTriggered: (parentPipeline, pipeline) =>
this.clickTriggeredByPipeline(parentPipeline, pipeline), this.resetTriggeredPipelines(parentPipeline, pipeline),
onClickTriggered: (parentPipeline, pipeline) => onClickTriggeredBy: pipeline => this.clickTriggeredByPipeline(pipeline),
this.clickTriggeredPipeline(parentPipeline, pipeline), onClickTriggered: pipeline => this.clickTriggeredPipeline(pipeline),
}, },
}); });
}, },
......
...@@ -54,16 +54,24 @@ export default class PipelineStore { ...@@ -54,16 +54,24 @@ export default class PipelineStore {
*/ */
parseTriggeredByPipelines(oldPipeline = {}, newPipeline) { parseTriggeredByPipelines(oldPipeline = {}, newPipeline) {
// keep old value in case it's opened because we're polling // keep old value in case it's opened because we're polling
Vue.set(newPipeline, 'isExpanded', oldPipeline.isExpanded || false); Vue.set(newPipeline, 'isExpanded', oldPipeline.isExpanded || false);
// add isLoading property // add isLoading property
Vue.set(newPipeline, 'isLoading', false); 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 (newPipeline.triggered_by) {
if (!Array.isArray(newPipeline.triggered_by)) { if (!Array.isArray(newPipeline.triggered_by)) {
Object.assign(newPipeline, { triggered_by: [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);
});
}
} }
} }
......
<script> <script>
import { escapeRegExp } from 'lodash'; 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 { escapeFileUrl } from '~/lib/utils/url_utility';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import Icon from '~/vue_shared/components/icon.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue';
import { getIconName } from '../../utils/icon';
import getRefMixin from '../../mixins/get_ref'; import getRefMixin from '../../mixins/get_ref';
import getCommit from '../../queries/getCommit.query.graphql'; import getCommit from '../../queries/getCommit.query.graphql';
...@@ -14,8 +20,9 @@ export default { ...@@ -14,8 +20,9 @@ export default {
GlLink, GlLink,
GlSkeletonLoading, GlSkeletonLoading,
GlLoadingIcon, GlLoadingIcon,
GlIcon,
TimeagoTooltip, TimeagoTooltip,
Icon, FileIcon,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -95,9 +102,6 @@ export default { ...@@ -95,9 +102,6 @@ export default {
? { path: `/-/tree/${escape(this.ref)}/${escapeFileUrl(this.path)}` } ? { path: `/-/tree/${escape(this.ref)}/${escapeFileUrl(this.path)}` }
: null; : null;
}, },
iconName() {
return `fa-${getIconName(this.type, this.path)}`;
},
isFolder() { isFolder() {
return this.type === 'tree'; return this.type === 'tree';
}, },
...@@ -123,12 +127,6 @@ export default { ...@@ -123,12 +127,6 @@ export default {
<template> <template>
<tr class="tree-item"> <tr class="tree-item">
<td class="tree-item-file-name cursor-default position-relative"> <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 <component
:is="linkComponent" :is="linkComponent"
ref="link" ref="link"
...@@ -140,27 +138,27 @@ export default { ...@@ -140,27 +138,27 @@ export default {
class="tree-item-link str-truncated" class="tree-item-link str-truncated"
data-qa-selector="file_name_link" data-qa-selector="file_name_link"
> >
<i <file-icon
v-if="path !== loadingPath" :file-name="fullPath"
:aria-label="type" :folder="isFolder"
role="img" :submodule="isSubmodule"
:class="iconName" :loading="path === loadingPath"
class="fa fa-fw mr-1" css-classes="position-relative file-icon"
></i class="mr-1 position-relative text-secondary"
><span class="position-relative">{{ fullPath }}</span> /><span class="position-relative">{{ fullPath }}</span>
</component> </component>
<!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings --> <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
<gl-badge v-if="lfsOid" variant="default" class="label-lfs ml-1">LFS</gl-badge> <gl-badge v-if="lfsOid" variant="default" class="label-lfs ml-1">LFS</gl-badge>
<template v-if="isSubmodule"> <template v-if="isSubmodule">
@ <gl-link :href="submoduleTreeUrl" class="commit-sha">{{ shortSha }}</gl-link> @ <gl-link :href="submoduleTreeUrl" class="commit-sha">{{ shortSha }}</gl-link>
</template> </template>
<icon <gl-icon
v-if="hasLockLabel" v-if="hasLockLabel"
v-gl-tooltip v-gl-tooltip
:title="commit.lockLabel" :title="commit.lockLabel"
name="lock" name="lock"
:size="12" :size="12"
class="ml-2 vertical-align-middle" class="ml-1"
/> />
</td> </td>
<td class="d-none d-sm-table-cell tree-commit cursor-default"> <td class="d-none d-sm-table-cell tree-commit cursor-default">
......
<script> <script>
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import getIconForFile from './file_icon/file_icon_map'; 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 /* This is a re-usable vue component for rendering a svg sprite
icon icon
...@@ -17,8 +16,8 @@ import icon from '../../vue_shared/components/icon.vue'; ...@@ -17,8 +16,8 @@ import icon from '../../vue_shared/components/icon.vue';
*/ */
export default { export default {
components: { components: {
icon,
GlLoadingIcon, GlLoadingIcon,
GlIcon,
}, },
props: { props: {
fileName: { fileName: {
...@@ -31,7 +30,11 @@ export default { ...@@ -31,7 +30,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
submodule: {
type: Boolean,
required: false,
default: false,
},
opened: { opened: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -58,7 +61,7 @@ export default { ...@@ -58,7 +61,7 @@ export default {
}, },
computed: { computed: {
spriteHref() { spriteHref() {
const iconName = getIconForFile(this.fileName) || 'file'; const iconName = this.submodule ? 'folder-git' : getIconForFile(this.fileName) || 'file';
return `${gon.sprite_file_icons}#${iconName}`; return `${gon.sprite_file_icons}#${iconName}`;
}, },
folderIconName() { folderIconName() {
...@@ -73,9 +76,12 @@ export default { ...@@ -73,9 +76,12 @@ export default {
<template> <template>
<span> <span>
<svg v-if="!loading && !folder" :class="[iconSizeClass, cssClasses]"> <svg v-if="!loading && !folder" :class="[iconSizeClass, cssClasses]">
<use v-bind="{ 'xlink:href': spriteHref }" /> <use v-bind="{ 'xlink:href': spriteHref }" /></svg
</svg> ><gl-icon
<icon v-if="!loading && folder" :name="folderIconName" :size="size" class="folder-icon" /> v-if="!loading && folder"
<gl-loading-icon v-if="loading" :inline="true" /> :name="folderIconName"
:size="size"
class="folder-icon"
/><gl-loading-icon v-if="loading" :inline="true" />
</span> </span>
</template> </template>
...@@ -474,6 +474,9 @@ img.emoji { ...@@ -474,6 +474,9 @@ img.emoji {
.mw-70p { max-width: 70%; } .mw-70p { max-width: 70%; }
.mw-90p { max-width: 90%; } .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; } .min-height-0 { min-height: 0; }
.svg-w-100 { .svg-w-100 {
......
...@@ -199,8 +199,8 @@ ...@@ -199,8 +199,8 @@
/* /*
* Mixin that handles the container for the job logs (CI/CD and kubernetes pod logs) * Mixin that handles the container for the job logs (CI/CD and kubernetes pod logs)
*/ */
@mixin build-trace { @mixin build-trace($background: $black) {
background: $black; background: $background;
color: $gray-darkest; color: $gray-darkest;
white-space: pre; white-space: pre;
overflow-x: auto; overflow-x: auto;
...@@ -243,7 +243,7 @@ ...@@ -243,7 +243,7 @@
/* /*
* Mixin that handles the position of the controls placed on the top bar * 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; display: flex;
font-size: $control-font-size; font-size: $control-font-size;
justify-content: $flex-direction; justify-content: $flex-direction;
......
...@@ -641,6 +641,14 @@ $issue-boards-breadcrumbs-height-xs: 63px; ...@@ -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-xs: $header-height + $issue-boards-breadcrumbs-height-xs;
$issue-board-list-difference-sm: $header-height + $breadcrumb-min-height; $issue-board-list-difference-sm: $header-height + $breadcrumb-min-height;
$issue-board-list-difference-md: $issue-board-list-difference-sm + $issue-boards-filter-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 * Avatar
......
...@@ -356,54 +356,3 @@ ...@@ -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;
}
}
.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;
}
}
...@@ -138,6 +138,12 @@ ...@@ -138,6 +138,12 @@
} }
.tree-item { .tree-item {
.file-icon,
.folder-icon {
position: relative;
top: 2px;
}
.link-container { .link-container {
padding: 0; padding: 0;
......
...@@ -16,6 +16,10 @@ class Admin::DashboardController < Admin::ApplicationController ...@@ -16,6 +16,10 @@ class Admin::DashboardController < Admin::ApplicationController
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def stats
@users_statistics = UsersStatistics.latest
end
def show_license_breakdown? def show_license_breakdown?
false false
end end
......
...@@ -256,6 +256,7 @@ module ApplicationHelper ...@@ -256,6 +256,7 @@ module ApplicationHelper
def page_class def page_class
class_names = [] class_names = []
class_names << 'issue-boards-page' if current_controller?(:boards) 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 << 'with-performance-bar' if performance_bar_enabled?
class_names << system_message_class class_names << system_message_class
class_names class_names
......
...@@ -176,6 +176,7 @@ module ApplicationSettingsHelper ...@@ -176,6 +176,7 @@ module ApplicationSettingsHelper
:authorized_keys_enabled, :authorized_keys_enabled,
:auto_devops_enabled, :auto_devops_enabled,
:auto_devops_domain, :auto_devops_domain,
:container_expiration_policies_enable_historic_entries,
:container_registry_token_expire_delay, :container_registry_token_expire_delay,
:default_artifacts_expire_in, :default_artifacts_expire_in,
:default_branch_protection, :default_branch_protection,
......
...@@ -142,6 +142,9 @@ class ApplicationSetting < ApplicationRecord ...@@ -142,6 +142,9 @@ class ApplicationSetting < ApplicationRecord
validates :default_artifacts_expire_in, presence: true, duration: true 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, validates :container_registry_token_expire_delay,
presence: true, presence: true,
numericality: { only_integer: true, greater_than: 0 } numericality: { only_integer: true, greater_than: 0 }
......
...@@ -42,6 +42,7 @@ module ApplicationSettingImplementation ...@@ -42,6 +42,7 @@ module ApplicationSettingImplementation
asset_proxy_enabled: false, asset_proxy_enabled: false,
authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand
commit_email_hostname: default_commit_email_hostname, commit_email_hostname: default_commit_email_hostname,
container_expiration_policies_enable_historic_entries: false,
container_registry_token_expire_delay: 5, container_registry_token_expire_delay: 5,
default_artifacts_expire_in: '30 days', default_artifacts_expire_in: '30 days',
default_branch_protection: Settings.gitlab['default_branch_protection'], default_branch_protection: Settings.gitlab['default_branch_protection'],
......
# frozen_string_literal: true # frozen_string_literal: true
class UsersStatistics < ApplicationRecord class UsersStatistics < ApplicationRecord
STATISTICS_NAMES = [ scope :order_created_at_desc, -> { order(created_at: :desc) }
:without_groups_and_projects,
:with_highest_role_guest, class << self
:with_highest_role_reporter, def latest
:with_highest_role_developer, order_created_at_desc.first
:with_highest_role_maintainer, end
:with_highest_role_owner, end
:bots,
:blocked def active
].freeze [
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 class << self
def create_current_stats! def create_current_stats!
......
...@@ -5,5 +5,14 @@ ...@@ -5,5 +5,14 @@
.form-group .form-group
= f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'label-bold' = 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' = 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" = f.submit 'Save changes', class: "btn btn-success"
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
%hr %hr
.btn-group.d-flex{ role: 'group' } .btn-group.d-flex{ role: 'group' }
= link_to 'New user', new_admin_user_path, class: "btn btn-success" = 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 .col-sm-4
.info-well.dark-well .info-well.dark-well
.well-segment.well-centered .well-segment.well-centered
......
- 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
---
title: Resolve Unable to expand multiple downstream pipelines.
merge_request: 27029
author:
type: fixed
---
title: Add application setting to enable container expiration and retention policies
on pre 12.8 projects
merge_request: 28479
author:
type: added
---
title: Enable log explorer to use the full height of the screen
merge_request: 28312
author:
type: added
---
title: Add status endpoint to Pages Internal API
merge_request: 28743
author:
type: added
---
title: Use rich icons for thw rows on the file tree
merge_request: 28112
author:
type: changed
---
title: Show user statistics in admin area also in CE, and use daily generated data for these statistics
merge_request: 27345
author:
type: changed
...@@ -161,5 +161,7 @@ namespace :admin do ...@@ -161,5 +161,7 @@ namespace :admin do
concerns :clusterable concerns :clusterable
get '/dashboard/stats', to: 'dashboard#stats'
root to: 'dashboard#index' root to: 'dashboard#index'
end end
# 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
...@@ -397,7 +397,8 @@ CREATE TABLE public.application_settings ( ...@@ -397,7 +397,8 @@ CREATE TABLE public.application_settings (
email_restrictions text, email_restrictions text,
npm_package_requests_forwarding boolean DEFAULT true NOT NULL, npm_package_requests_forwarding boolean DEFAULT true NOT NULL,
namespace_storage_size_limit bigint DEFAULT 0 NOT NULL, namespace_storage_size_limit bigint DEFAULT 0 NOT NULL,
seat_link_enabled boolean DEFAULT true NOT NULL 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 CREATE SEQUENCE public.application_settings_id_seq
...@@ -13001,6 +13002,7 @@ COPY "schema_migrations" (version) FROM STDIN; ...@@ -13001,6 +13002,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200330121000 20200330121000
20200330123739 20200330123739
20200330132913 20200330132913
20200331195952
20200331220930 20200331220930
20200402123926 20200402123926
20200402135250 20200402135250
......
...@@ -516,6 +516,10 @@ on how to achieve that. ...@@ -516,6 +516,10 @@ on how to achieve that.
## Use an external container registry with GitLab as an auth endpoint ## 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** **Omnibus GitLab**
You can use GitLab as an auth endpoint with an external container registry. You can use GitLab as an auth endpoint with an external container registry.
......
...@@ -2984,6 +2984,103 @@ type EpicTreeReorderPayload { ...@@ -2984,6 +2984,103 @@ type EpicTreeReorderPayload {
errors: [String!]! 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 { type GrafanaIntegration {
""" """
Timestamp of the issue's creation Timestamp of the issue's creation
...@@ -5435,6 +5532,41 @@ type Namespace { ...@@ -5435,6 +5532,41 @@ type Namespace {
visibility: String 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 { type Note {
""" """
User who wrote this note User who wrote this note
...@@ -6916,6 +7048,16 @@ type Query { ...@@ -6916,6 +7048,16 @@ type Query {
text: String! text: String!
): String! ): 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 Find a group
""" """
......
...@@ -483,6 +483,25 @@ Autogenerated return type of EpicTreeReorder ...@@ -483,6 +483,25 @@ Autogenerated return type of EpicTreeReorder
| `clientMutationId` | String | A unique identifier for the client performing the mutation. | | `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Reasons why the mutation failed. | | `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 ## GrafanaIntegration
| Name | Type | Description | | Name | Type | Description |
......
...@@ -45,6 +45,7 @@ Example response: ...@@ -45,6 +45,7 @@ Example response:
"default_group_visibility" : "private", "default_group_visibility" : "private",
"gravatar_enabled" : true, "gravatar_enabled" : true,
"sign_in_text" : null, "sign_in_text" : null,
"container_expiration_policies_enable_historic_entries": true,
"container_registry_token_expire_delay": 5, "container_registry_token_expire_delay": 5,
"repository_storages": ["default"], "repository_storages": ["default"],
"plantuml_enabled": false, "plantuml_enabled": false,
......
...@@ -15,7 +15,21 @@ tag) with an API call. ...@@ -15,7 +15,21 @@ tag) with an API call.
## Authentication tokens ## 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 ### Trigger token
......
...@@ -147,6 +147,9 @@ The **Total users** is calculated as: **Active users** + **Blocked users**. ...@@ -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 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). [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 ### Administering Groups
You can administer all groups in the GitLab instance from the Admin Area's Groups page. You can administer all groups in the GitLab instance from the Admin Area's Groups page.
......
...@@ -61,7 +61,7 @@ Access the default page for admin area settings by navigating to ...@@ -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. | | [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. | | [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 ## Reporting
......
...@@ -488,7 +488,9 @@ older tags and images are regularly removed from the Container Registry. ...@@ -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. > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/15398) in GitLab 12.8.
NOTE: **Note:** 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 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. older tags and images are regularly removed from the Container Registry.
...@@ -539,6 +541,15 @@ Examples: ...@@ -539,6 +541,15 @@ Examples:
See the API documentation for further details: [Edit project](../../../api/projects.md#edit-project). 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 ## Limitations
Moving or renaming existing Container Registry repositories is not supported Moving or renaming existing Container Registry repositories is not supported
......
...@@ -110,7 +110,7 @@ module API ...@@ -110,7 +110,7 @@ module API
return unless %w[git-receive-pack git-upload-pack git-upload-archive].include?(action) 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), address: Gitlab::GitalyClient.address(container.repository_storage),
token: Gitlab::GitalyClient.token(container.repository_storage), token: Gitlab::GitalyClient.token(container.repository_storage),
features: Feature::Gitaly.server_feature_flags features: Feature::Gitaly.server_feature_flags
......
...@@ -16,6 +16,13 @@ module API ...@@ -16,6 +16,13 @@ module API
namespace 'internal' do namespace 'internal' do
namespace 'pages' 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 desc 'Get GitLab Pages domain configuration by hostname' do
detail 'This feature was introduced in GitLab 12.3.' detail 'This feature was introduced in GitLab 12.3.'
end end
......
...@@ -1320,12 +1320,36 @@ msgstr "" ...@@ -1320,12 +1320,36 @@ msgstr ""
msgid "Admin notes" msgid "Admin notes"
msgstr "" msgstr ""
msgid "AdminArea|Active users"
msgstr ""
msgid "AdminArea|Billable users"
msgstr ""
msgid "AdminArea|Blocked users"
msgstr ""
msgid "AdminArea|Bots" msgid "AdminArea|Bots"
msgstr "" msgstr ""
msgid "AdminArea|Developer"
msgstr ""
msgid "AdminArea|Guest"
msgstr ""
msgid "AdminArea|Included Free in license" msgid "AdminArea|Included Free in license"
msgstr "" msgstr ""
msgid "AdminArea|Maintainer"
msgstr ""
msgid "AdminArea|Owner"
msgstr ""
msgid "AdminArea|Reporter"
msgstr ""
msgid "AdminArea|Stop all jobs" msgid "AdminArea|Stop all jobs"
msgstr "" msgstr ""
...@@ -1338,15 +1362,18 @@ msgstr "" ...@@ -1338,15 +1362,18 @@ msgstr ""
msgid "AdminArea|Stopping jobs failed" msgid "AdminArea|Stopping jobs failed"
msgstr "" msgstr ""
msgid "AdminArea|Users statistics" msgid "AdminArea|Total users"
msgstr "" msgstr ""
msgid "AdminArea|Users total" msgid "AdminArea|Users statistics"
msgstr "" msgstr ""
msgid "AdminArea|Users with highest role" msgid "AdminArea|Users with highest role"
msgstr "" 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." msgid "AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running."
msgstr "" msgstr ""
...@@ -7538,6 +7565,9 @@ msgstr "" ...@@ -7538,6 +7565,9 @@ msgstr ""
msgid "Enable classification control using an external service" msgid "Enable classification control using an external service"
msgstr "" msgstr ""
msgid "Enable container expiration and retention policies for projects created earlier than GitLab 12.7."
msgstr ""
msgid "Enable email restrictions for sign ups" msgid "Enable email restrictions for sign ups"
msgstr "" msgstr ""
...@@ -7934,6 +7964,9 @@ msgstr "" ...@@ -7934,6 +7964,9 @@ msgstr ""
msgid "Environments|Stopping" msgid "Environments|Stopping"
msgstr "" 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?" 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 "" msgstr ""
...@@ -8357,6 +8390,9 @@ msgstr "" ...@@ -8357,6 +8390,9 @@ msgstr ""
msgid "Existing members and groups" msgid "Existing members and groups"
msgstr "" 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" msgid "Existing shares"
msgstr "" msgstr ""
...@@ -10650,9 +10686,6 @@ msgstr "" ...@@ -10650,9 +10686,6 @@ msgstr ""
msgid "IDE|Live Preview" msgid "IDE|Live Preview"
msgstr "" msgstr ""
msgid "IDE|Open in file view"
msgstr ""
msgid "IDE|Preview your web application using Web IDE client-side evaluation." msgid "IDE|Preview your web application using Web IDE client-side evaluation."
msgstr "" msgstr ""
...@@ -12808,9 +12841,6 @@ msgstr "" ...@@ -12808,9 +12841,6 @@ msgstr ""
msgid "Metrics|There was an error fetching the environments data, please try again" msgid "Metrics|There was an error fetching the environments data, please try again"
msgstr "" msgstr ""
msgid "Metrics|There was an error fetching the logs, please try again"
msgstr ""
msgid "Metrics|There was an error getting deployment information." msgid "Metrics|There was an error getting deployment information."
msgstr "" msgstr ""
...@@ -13906,9 +13936,6 @@ msgstr "" ...@@ -13906,9 +13936,6 @@ msgstr ""
msgid "Open in Xcode" msgid "Open in Xcode"
msgstr "" msgstr ""
msgid "Open in file view"
msgstr ""
msgid "Open issues" msgid "Open issues"
msgstr "" msgstr ""
......
...@@ -2,5 +2,13 @@ ...@@ -2,5 +2,13 @@
FactoryBot.define do FactoryBot.define do
factory :users_statistics 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
end end
...@@ -2,14 +2,14 @@ ...@@ -2,14 +2,14 @@
require 'spec_helper' require 'spec_helper'
describe 'admin visits dashboard', :js do describe 'admin visits dashboard' do
include ProjectForksHelper include ProjectForksHelper
before do before do
sign_in(create(:admin)) sign_in(create(:admin))
end end
context 'counting forks' do context 'counting forks', :js do
it 'correctly counts 2 forks of a project' do it 'correctly counts 2 forks of a project' do
project = create(:project) project = create(:project)
project_fork = fork_project(project) project_fork = fork_project(project)
...@@ -25,4 +25,26 @@ describe 'admin visits dashboard', :js do ...@@ -25,4 +25,26 @@ describe 'admin visits dashboard', :js do
expect(page).to have_content('Forks 2') expect(page).to have_content('Forks 2')
end end
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 end
...@@ -174,7 +174,7 @@ describe 'Set up Mattermost slash commands', :js do ...@@ -174,7 +174,7 @@ describe 'Set up Mattermost slash commands', :js do
describe 'stable logo url' do describe 'stable logo url' do
it 'shows a publicly available logo' 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 end
end end
...@@ -47,7 +47,7 @@ describe('EnvironmentLogs', () => { ...@@ -47,7 +47,7 @@ describe('EnvironmentLogs', () => {
const findLogControlButtons = () => wrapper.find({ name: 'log-control-buttons-stub' }); const findLogControlButtons = () => wrapper.find({ name: 'log-control-buttons-stub' });
const findInfiniteScroll = () => wrapper.find({ ref: 'infiniteScroll' }); 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 findLogFooter = () => wrapper.find({ ref: 'logFooter' });
const getInfiniteScrollAttr = attr => parseInt(findInfiniteScroll().attributes(attr), 10); const getInfiniteScrollAttr = attr => parseInt(findInfiniteScroll().attributes(attr), 10);
...@@ -169,16 +169,12 @@ describe('EnvironmentLogs', () => { ...@@ -169,16 +169,12 @@ describe('EnvironmentLogs', () => {
expect(updateControlBtnsMock).not.toHaveBeenCalled(); expect(updateControlBtnsMock).not.toHaveBeenCalled();
}); });
it('shows an infinite scroll with height and no content', () => { it('shows an infinite scroll with no content', () => {
expect(getInfiniteScrollAttr('max-list-height')).toBeGreaterThan(0);
expect(getInfiniteScrollAttr('fetched-items')).toBe(0); expect(getInfiniteScrollAttr('fetched-items')).toBe(0);
}); });
it('shows an infinite scroll container with equal height and max-height ', () => { it('shows an infinite scroll container with no set max-height ', () => {
const height = getInfiniteScrollAttr('max-list-height'); expect(findInfiniteScroll().attributes('max-list-height')).toBeUndefined();
expect(height).toEqual(expect.any(Number));
expect(findInfiniteScroll().attributes('style')).toMatch(`height: ${height}px;`);
}); });
it('shows a logs trace', () => { it('shows a logs trace', () => {
...@@ -270,8 +266,7 @@ describe('EnvironmentLogs', () => { ...@@ -270,8 +266,7 @@ describe('EnvironmentLogs', () => {
expect(findAdvancedFilters().exists()).toBe(true); expect(findAdvancedFilters().exists()).toBe(true);
}); });
it('shows infinite scroll with height and no content', () => { it('shows infinite scroll with content', () => {
expect(getInfiniteScrollAttr('max-list-height')).toBeGreaterThan(0);
expect(getInfiniteScrollAttr('fetched-items')).toBe(mockTrace.length); expect(getInfiniteScrollAttr('fetched-items')).toBe(mockTrace.length);
}); });
......
...@@ -38,7 +38,7 @@ jest.mock('~/logs/utils'); ...@@ -38,7 +38,7 @@ jest.mock('~/logs/utils');
const mockDefaultRange = { const mockDefaultRange = {
start: '2020-01-10T18:00:00.000Z', start: '2020-01-10T18:00:00.000Z',
end: '2020-01-10T10:00:00.000Z', end: '2020-01-10T19:00:00.000Z',
}; };
const mockFixedRange = { const mockFixedRange = {
start: '2020-01-09T18:06:20.000Z', start: '2020-01-09T18:06:20.000Z',
...@@ -145,9 +145,6 @@ describe('Logs Store actions', () => { ...@@ -145,9 +145,6 @@ describe('Logs Store actions', () => {
{ type: types.RECEIVE_ENVIRONMENTS_DATA_ERROR }, { type: types.RECEIVE_ENVIRONMENTS_DATA_ERROR },
], ],
[], [],
() => {
expect(flash).toHaveBeenCalledTimes(1);
},
); );
}); });
}); });
...@@ -186,6 +183,7 @@ describe('Logs Store actions', () => { ...@@ -186,6 +183,7 @@ describe('Logs Store actions', () => {
it('should commit logs and pod data when there is pod name defined', () => { it('should commit logs and pod data when there is pod name defined', () => {
state.pods.current = mockPodName; state.pods.current = mockPodName;
state.timeRange.current = mockFixedRange;
return testAction(fetchLogs, null, state, expectedMutations, expectedActions, () => { return testAction(fetchLogs, null, state, expectedMutations, expectedActions, () => {
expect(latestGetParams()).toMatchObject({ expect(latestGetParams()).toMatchObject({
...@@ -214,22 +212,26 @@ describe('Logs Store actions', () => { ...@@ -214,22 +212,26 @@ describe('Logs Store actions', () => {
state.search = mockSearch; state.search = mockSearch;
state.timeRange.current = 'INVALID_TIME_RANGE'; state.timeRange.current = 'INVALID_TIME_RANGE';
expectedMutations.splice(1, 0, {
type: types.SHOW_TIME_RANGE_INVALID_WARNING,
});
return testAction(fetchLogs, null, state, expectedMutations, expectedActions, () => { return testAction(fetchLogs, null, state, expectedMutations, expectedActions, () => {
expect(latestGetParams()).toEqual({ expect(latestGetParams()).toEqual({
pod_name: mockPodName, pod_name: mockPodName,
search: mockSearch, 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', () => { 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, () => { 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', () => { ...@@ -249,6 +251,7 @@ describe('Logs Store actions', () => {
it('should commit logs and pod data when there is pod name defined', () => { it('should commit logs and pod data when there is pod name defined', () => {
state.pods.current = mockPodName; state.pods.current = mockPodName;
state.timeRange.current = mockFixedRange;
expectedActions = []; expectedActions = [];
...@@ -293,6 +296,10 @@ describe('Logs Store actions', () => { ...@@ -293,6 +296,10 @@ describe('Logs Store actions', () => {
state.search = mockSearch; state.search = mockSearch;
state.timeRange.current = 'INVALID_TIME_RANGE'; state.timeRange.current = 'INVALID_TIME_RANGE';
expectedMutations.splice(1, 0, {
type: types.SHOW_TIME_RANGE_INVALID_WARNING,
});
return testAction( return testAction(
fetchMoreLogsPrepend, fetchMoreLogsPrepend,
null, null,
...@@ -304,15 +311,12 @@ describe('Logs Store actions', () => { ...@@ -304,15 +311,12 @@ describe('Logs Store actions', () => {
pod_name: mockPodName, pod_name: mockPodName,
search: mockSearch, 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', () => { it('should commit logs and pod data when no pod name defined', () => {
state.timeRange.current = mockDefaultRange; state.timeRange.current = defaultTimeRange;
return testAction( return testAction(
fetchMoreLogsPrepend, fetchMoreLogsPrepend,
...@@ -321,7 +325,10 @@ describe('Logs Store actions', () => { ...@@ -321,7 +325,10 @@ describe('Logs Store actions', () => {
expectedMutations, expectedMutations,
expectedActions, 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', () => { ...@@ -357,6 +364,7 @@ describe('Logs Store actions', () => {
it('fetchLogs should commit logs and pod errors', () => { it('fetchLogs should commit logs and pod errors', () => {
state.environments.options = mockEnvironments; state.environments.options = mockEnvironments;
state.environments.current = mockEnvName; state.environments.current = mockEnvName;
state.timeRange.current = defaultTimeRange;
return testAction( return testAction(
fetchLogs, fetchLogs,
...@@ -377,6 +385,7 @@ describe('Logs Store actions', () => { ...@@ -377,6 +385,7 @@ describe('Logs Store actions', () => {
it('fetchMoreLogsPrepend should commit logs and pod errors', () => { it('fetchMoreLogsPrepend should commit logs and pod errors', () => {
state.environments.options = mockEnvironments; state.environments.options = mockEnvironments;
state.environments.current = mockEnvName; state.environments.current = mockEnvName;
state.timeRange.current = defaultTimeRange;
return testAction( return testAction(
fetchMoreLogsPrepend, fetchMoreLogsPrepend,
......
...@@ -67,6 +67,7 @@ describe('Logs Store Mutations', () => { ...@@ -67,6 +67,7 @@ describe('Logs Store Mutations', () => {
options: [], options: [],
isLoading: false, isLoading: false,
current: null, current: null,
fetchError: true,
}); });
}); });
}); });
...@@ -83,6 +84,7 @@ describe('Logs Store Mutations', () => { ...@@ -83,6 +84,7 @@ describe('Logs Store Mutations', () => {
expect(state.logs).toEqual({ expect(state.logs).toEqual({
lines: [], lines: [],
cursor: null, cursor: null,
fetchError: false,
isLoading: true, isLoading: true,
isComplete: false, isComplete: false,
}); });
...@@ -101,6 +103,7 @@ describe('Logs Store Mutations', () => { ...@@ -101,6 +103,7 @@ describe('Logs Store Mutations', () => {
isLoading: false, isLoading: false,
cursor: mockCursor, cursor: mockCursor,
isComplete: false, isComplete: false,
fetchError: false,
}); });
}); });
...@@ -115,6 +118,7 @@ describe('Logs Store Mutations', () => { ...@@ -115,6 +118,7 @@ describe('Logs Store Mutations', () => {
isLoading: false, isLoading: false,
cursor: null, cursor: null,
isComplete: true, isComplete: true,
fetchError: false,
}); });
}); });
}); });
...@@ -128,6 +132,7 @@ describe('Logs Store Mutations', () => { ...@@ -128,6 +132,7 @@ describe('Logs Store Mutations', () => {
isLoading: false, isLoading: false,
cursor: null, cursor: null,
isComplete: false, isComplete: false,
fetchError: true,
}); });
}); });
}); });
...@@ -152,6 +157,7 @@ describe('Logs Store Mutations', () => { ...@@ -152,6 +157,7 @@ describe('Logs Store Mutations', () => {
isLoading: false, isLoading: false,
cursor: mockCursor, cursor: mockCursor,
isComplete: false, isComplete: false,
fetchError: false,
}); });
}); });
...@@ -171,6 +177,7 @@ describe('Logs Store Mutations', () => { ...@@ -171,6 +177,7 @@ describe('Logs Store Mutations', () => {
isLoading: false, isLoading: false,
cursor: mockNextCursor, cursor: mockNextCursor,
isComplete: false, isComplete: false,
fetchError: false,
}); });
}); });
...@@ -185,6 +192,7 @@ describe('Logs Store Mutations', () => { ...@@ -185,6 +192,7 @@ describe('Logs Store Mutations', () => {
isLoading: false, isLoading: false,
cursor: null, cursor: null,
isComplete: true, isComplete: true,
fetchError: false,
}); });
}); });
}); });
...@@ -194,6 +202,7 @@ describe('Logs Store Mutations', () => { ...@@ -194,6 +202,7 @@ describe('Logs Store Mutations', () => {
mutations[types.RECEIVE_LOGS_DATA_PREPEND_ERROR](state); mutations[types.RECEIVE_LOGS_DATA_PREPEND_ERROR](state);
expect(state.logs.isLoading).toBe(false); expect(state.logs.isLoading).toBe(false);
expect(state.logs.fetchError).toBe(true);
}); });
}); });
......
...@@ -7,17 +7,16 @@ exports[`Repository table row component renders table row 1`] = ` ...@@ -7,17 +7,16 @@ exports[`Repository table row component renders table row 1`] = `
<td <td
class="tree-item-file-name cursor-default position-relative" class="tree-item-file-name cursor-default position-relative"
> >
<!---->
<a <a
class="tree-item-link str-truncated" class="tree-item-link str-truncated"
data-qa-selector="file_name_link" data-qa-selector="file_name_link"
href="https://test.com" href="https://test.com"
> >
<i <file-icon-stub
aria-label="file" class="mr-1 position-relative text-secondary"
class="fa fa-fw mr-1 fa-file-text-o" cssclasses="position-relative file-icon"
role="img" filename="test"
size="16"
/> />
<span <span
class="position-relative" class="position-relative"
...@@ -60,17 +59,16 @@ exports[`Repository table row component renders table row for path with special ...@@ -60,17 +59,16 @@ exports[`Repository table row component renders table row for path with special
<td <td
class="tree-item-file-name cursor-default position-relative" class="tree-item-file-name cursor-default position-relative"
> >
<!---->
<a <a
class="tree-item-link str-truncated" class="tree-item-link str-truncated"
data-qa-selector="file_name_link" data-qa-selector="file_name_link"
href="https://test.com" href="https://test.com"
> >
<i <file-icon-stub
aria-label="file" class="mr-1 position-relative text-secondary"
class="fa fa-fw mr-1 fa-file-text-o" cssclasses="position-relative file-icon"
role="img" filename="test"
size="16"
/> />
<span <span
class="position-relative" class="position-relative"
......
import { shallowMount, RouterLinkStub } from '@vue/test-utils'; 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 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 vm;
let $router; let $router;
...@@ -188,7 +188,8 @@ describe('Repository table row component', () => { ...@@ -188,7 +188,8 @@ describe('Repository table row component', () => {
vm.setData({ commit: { lockLabel: 'Locked by Root', committedDate: '2019-01-01' } }); vm.setData({ commit: { lockLabel: 'Locked by Root', committedDate: '2019-01-01' } });
return vm.vm.$nextTick().then(() => { 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', () => { ...@@ -202,6 +203,6 @@ describe('Repository table row component', () => {
loadingPath: 'test', loadingPath: 'test',
}); });
expect(vm.find(GlLoadingIcon).exists()).toBe(true); expect(vm.find(FileIcon).props('loading')).toBe(true);
}); });
}); });
import { shallowMount } from '@vue/test-utils'; 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 FileIcon from '~/vue_shared/components/file_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
describe('File Icon component', () => { describe('File Icon component', () => {
let wrapper; let wrapper;
...@@ -48,7 +47,7 @@ describe('File Icon component', () => { ...@@ -48,7 +47,7 @@ describe('File Icon component', () => {
}); });
expect(findIcon().exists()).toBe(false); 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', () => { it('should render a loading icon', () => {
......
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();
});
});
});
...@@ -159,7 +159,6 @@ describe('graph component', () => { ...@@ -159,7 +159,6 @@ describe('graph component', () => {
expect(component.$emit).toHaveBeenCalledWith( expect(component.$emit).toHaveBeenCalledWith(
'onClickTriggeredBy', 'onClickTriggeredBy',
component.pipeline,
component.pipeline.triggered_by[0], component.pipeline.triggered_by[0],
); );
}); });
...@@ -196,7 +195,6 @@ describe('graph component', () => { ...@@ -196,7 +195,6 @@ describe('graph component', () => {
expect(component.$emit).toHaveBeenCalledWith( expect(component.$emit).toHaveBeenCalledWith(
'onClickTriggered', 'onClickTriggered',
component.pipeline,
component.pipeline.triggered[0], component.pipeline.triggered[0],
); );
}); });
......
...@@ -34,6 +34,10 @@ describe ApplicationSetting do ...@@ -34,6 +34,10 @@ describe ApplicationSetting do
it { is_expected.to allow_value("dev.gitlab.com").for(:commit_email_hostname) } 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.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("myemail@gitlab.com").for(:lets_encrypt_notification_email) }
it { is_expected.to allow_value(nil).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) } it { is_expected.not_to allow_value("notanemail").for(:lets_encrypt_notification_email) }
......
...@@ -53,7 +53,7 @@ describe Ci::Group do ...@@ -53,7 +53,7 @@ describe Ci::Group do
it 'calls the status from the object itself' do it 'calls the status from the object itself' do
expect(jobs.first).to receive(:detailed_status) expect(jobs.first).to receive(:detailed_status)
expect(subject.detailed_status(double(:user))) subject.detailed_status(double(:user))
end end
end end
......
...@@ -526,14 +526,14 @@ describe Ci::Runner do ...@@ -526,14 +526,14 @@ describe Ci::Runner do
it 'sets a new last_update value when it is called the first time' do it 'sets a new last_update value when it is called the first time' do
last_update = runner.ensure_runner_queue_value last_update = runner.ensure_runner_queue_value
expect_value_in_queues.to eq(last_update) expect(value_in_queues).to eq(last_update)
end end
it 'does not change if it is not expired and called again' do it 'does not change if it is not expired and called again' do
last_update = runner.ensure_runner_queue_value last_update = runner.ensure_runner_queue_value
expect(runner.ensure_runner_queue_value).to eq(last_update) 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 end
context 'updates runner queue after changing editable value' do context 'updates runner queue after changing editable value' do
...@@ -544,7 +544,7 @@ describe Ci::Runner do ...@@ -544,7 +544,7 @@ describe Ci::Runner do
end end
it 'sets a new last_update value' do 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
end end
...@@ -556,14 +556,14 @@ describe Ci::Runner do ...@@ -556,14 +556,14 @@ describe Ci::Runner do
end end
it 'has an old last_update value' do 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
end end
def expect_value_in_queues def value_in_queues
Gitlab::Redis::SharedState.with do |redis| Gitlab::Redis::SharedState.with do |redis|
runner_queue_key = runner.send(:runner_queue_key) runner_queue_key = runner.send(:runner_queue_key)
expect(redis.get(runner_queue_key)) redis.get(runner_queue_key)
end end
end end
end end
......
...@@ -2,7 +2,36 @@ ...@@ -2,7 +2,36 @@
require 'spec_helper' 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 describe '.create_current_stats!' do
before do before do
create_list(:user_highest_role, 4) create_list(:user_highest_role, 4)
...@@ -40,4 +69,16 @@ RSpec.describe UsersStatistics do ...@@ -40,4 +69,16 @@ RSpec.describe UsersStatistics do
end end
end 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 end
...@@ -7,7 +7,7 @@ describe Ci::PipelinePresenter do ...@@ -7,7 +7,7 @@ describe Ci::PipelinePresenter do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:current_user) { user } let(:current_user) { user }
let(:project) { create(:project) } let(:project) { create(:project, :test_repo) }
let(:pipeline) { create(:ci_pipeline, project: project) } let(:pipeline) { create(:ci_pipeline, project: project) }
subject(:presenter) do subject(:presenter) do
...@@ -87,34 +87,32 @@ describe Ci::PipelinePresenter do ...@@ -87,34 +87,32 @@ describe Ci::PipelinePresenter do
end end
describe '#name' do describe '#name' do
before do
allow(pipeline).to receive(:merge_request_event_type) { event_type }
end
subject { presenter.name } subject { presenter.name }
context 'when pipeline is detached merge request pipeline' do context 'for a detached merge request pipeline' do
let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) } let(:event_type) { :detached }
let(:pipeline) { merge_request.all_pipelines.last }
it { is_expected.to eq('Detached merge request pipeline') } it { is_expected.to eq('Detached merge request pipeline') }
end end
context 'when pipeline is merge request pipeline' do context 'for a merged result pipeline' do
let(:merge_request) { create(:merge_request, :with_merge_request_pipeline) } let(:event_type) { :merged_result }
let(:pipeline) { merge_request.all_pipelines.last }
it { is_expected.to eq('Merged result pipeline') } it { is_expected.to eq('Merged result pipeline') }
end end
context 'when pipeline is merge train pipeline' do context 'for a merge train pipeline' do
let(:pipeline) { create(:ci_pipeline, project: project) } let(:event_type) { :merge_train }
before do
allow(pipeline).to receive(:merge_request_event_type) { :merge_train }
end
it { is_expected.to eq('Merge train pipeline') } it { is_expected.to eq('Merge train pipeline') }
end end
context 'when pipeline is branch pipeline' do context 'when pipeline is branch pipeline' do
let(:pipeline) { create(:ci_pipeline, project: project) } let(:event_type) { nil }
it { is_expected.to eq('Pipeline') } it { is_expected.to eq('Pipeline') }
end end
...@@ -145,8 +143,6 @@ describe Ci::PipelinePresenter do ...@@ -145,8 +143,6 @@ describe Ci::PipelinePresenter do
end end
context 'when pipeline is branch pipeline' do context 'when pipeline is branch pipeline' do
let(:pipeline) { create(:ci_pipeline, project: project) }
context 'when ref exists in the repository' do context 'when ref exists in the repository' do
before do before do
allow(pipeline).to receive(:ref_exists?) { true } allow(pipeline).to receive(:ref_exists?) { true }
...@@ -165,7 +161,7 @@ describe Ci::PipelinePresenter do ...@@ -165,7 +161,7 @@ describe Ci::PipelinePresenter do
end end
end end
context 'when ref exists in the repository' do context 'when ref does not exist in the repository' do
before do before do
allow(pipeline).to receive(:ref_exists?) { false } allow(pipeline).to receive(:ref_exists?) { false }
end end
...@@ -188,12 +184,17 @@ describe Ci::PipelinePresenter do ...@@ -188,12 +184,17 @@ describe Ci::PipelinePresenter do
describe '#all_related_merge_request_text' do describe '#all_related_merge_request_text' do
subject { presenter.all_related_merge_request_text } 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 context 'with zero related merge requests (branch pipeline)' do
it { is_expected.to eq('No related merge requests found.') } it { is_expected.to eq('No related merge requests found.') }
end end
context 'with one related merge request' do 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 { it {
is_expected.to eq("1 related merge request: " \ is_expected.to eq("1 related merge request: " \
...@@ -202,8 +203,9 @@ describe Ci::PipelinePresenter do ...@@ -202,8 +203,9 @@ describe Ci::PipelinePresenter do
end end
context 'with two related merge requests' do context 'with two related merge requests' do
let!(:mr_1) { create(:merge_request, project: project, source_project: project, target_branch: 'staging') } before do
let!(:mr_2) { create(:merge_request, project: project, source_project: project, target_branch: 'feature') } allow(pipeline).to receive(:all_merge_requests).and_return(MergeRequest.where(id: [mr_1.id, mr_2.id]))
end
it { it {
is_expected.to eq("2 related merge requests: " \ is_expected.to eq("2 related merge requests: " \
...@@ -223,22 +225,25 @@ describe Ci::PipelinePresenter do ...@@ -223,22 +225,25 @@ describe Ci::PipelinePresenter do
end end
describe '#all_related_merge_requests' do 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 it 'memoizes the returned relation' do
query_count = ActiveRecord::QueryRecorder.new do expect(pipeline).to receive(:all_merge_requests_by_recency).exactly(1).time.and_call_original
3.times { presenter.send(:all_related_merge_requests).count } 2.times { presenter.send(:all_related_merge_requests).count }
end.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 end
context 'permissions' do context 'permissions' do
let!(:merge_request) do let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline, source_project: project) }
create(:merge_request, project: project, source_project: project) let(:pipeline) { merge_request.all_pipelines.take }
end
subject(:all_related_merge_requests) do
presenter.send(:all_related_merge_requests)
end
shared_examples 'private merge requests' do shared_examples 'private merge requests' do
context 'when not logged in' do context 'when not logged in' do
...@@ -315,61 +320,51 @@ describe Ci::PipelinePresenter do ...@@ -315,61 +320,51 @@ describe Ci::PipelinePresenter do
describe '#link_to_merge_request' do describe '#link_to_merge_request' do
subject { presenter.link_to_merge_request } subject { presenter.link_to_merge_request }
let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) } context 'with a related merge request' do
let(:pipeline) { merge_request.all_pipelines.last } 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 it 'returns a correct link' do
is_expected is_expected.to include(project_merge_request_path(project, merge_request))
.to include(project_merge_request_path(merge_request.project, merge_request)) end
end end
context 'when pipeline is branch pipeline' do context 'when pipeline is branch pipeline' do
let(:pipeline) { create(:ci_pipeline, project: project) } it { is_expected.to be_nil }
it 'returns nothing' do
is_expected.to be_nil
end
end end
end end
describe '#link_to_merge_request_source_branch' do describe '#link_to_merge_request_source_branch' do
subject { presenter.link_to_merge_request_source_branch } subject { presenter.link_to_merge_request_source_branch }
let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) } context 'with a related merge request' do
let(:pipeline) { merge_request.all_pipelines.last } 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 it 'returns a correct link' do
is_expected is_expected.to include(project_commits_path(project, merge_request.source_branch))
.to include(project_commits_path(merge_request.source_project, end
merge_request.source_branch))
end end
context 'when pipeline is branch pipeline' do context 'when pipeline is branch pipeline' do
let(:pipeline) { create(:ci_pipeline, project: project) } it { is_expected.to be_nil }
it 'returns nothing' do
is_expected.to be_nil
end
end end
end end
describe '#link_to_merge_request_target_branch' do describe '#link_to_merge_request_target_branch' do
subject { presenter.link_to_merge_request_target_branch } subject { presenter.link_to_merge_request_target_branch }
let(:merge_request) { create(:merge_request, :with_merge_request_pipeline) } context 'with a related merge request' do
let(:pipeline) { merge_request.all_pipelines.last } 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 it 'returns a correct link' do
is_expected is_expected.to include(project_commits_path(project, merge_request.target_branch))
.to include(project_commits_path(merge_request.target_project, merge_request.target_branch)) end
end end
context 'when pipeline is branch pipeline' do context 'when pipeline is branch pipeline' do
let(:pipeline) { create(:ci_pipeline, project: project) } it { is_expected.to be_nil }
it 'returns nothing' do
is_expected.to be_nil
end
end end
end end
end end
...@@ -3,13 +3,36 @@ ...@@ -3,13 +3,36 @@
require 'spec_helper' require 'spec_helper'
describe API::Internal::Pages do describe API::Internal::Pages do
describe "GET /internal/pages" do let(:auth_headers) do
let(:pages_secret) { SecureRandom.random_bytes(Gitlab::Pages::SECRET_LENGTH) } 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 it 'responds with 401 Unauthorized' do
allow(Gitlab::Pages).to receive(:secret).and_return(pages_secret) 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
end
describe "GET /internal/pages" do
def query_host(host, headers = {}) def query_host(host, headers = {})
get api("/internal/pages"), headers: headers, params: { host: host } get api("/internal/pages"), headers: headers, params: { host: host }
end end
......
...@@ -22,19 +22,19 @@ describe Ci::ExpirePipelineCacheService do ...@@ -22,19 +22,19 @@ describe Ci::ExpirePipelineCacheService do
end end
it 'invalidates Etag caching for merge request pipelines if pipeline runs on any commit of that source branch' do 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, :with_detached_merge_request_pipeline)
merge_request = create(:merge_request, source_project: project, source_branch: pipeline.ref) project = merge_request.target_project
merge_request_pipelines_path = "/#{project.full_path}/-/merge_requests/#{merge_request.iid}/pipelines.json" merge_request_pipelines_path = "/#{project.full_path}/-/merge_requests/#{merge_request.iid}/pipelines.json"
allow_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch) 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) 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 end
it 'updates the cached status for a project' do it 'updates the cached status for a project' do
expect(Gitlab::Cache::Ci::ProjectPipelineStatus).to receive(:update_for_pipeline) expect(Gitlab::Cache::Ci::ProjectPipelineStatus).to receive(:update_for_pipeline).with(pipeline)
.with(pipeline)
subject.execute(pipeline) subject.execute(pipeline)
end end
......
...@@ -8,10 +8,6 @@ describe MergeRequests::AddTodoWhenBuildFailsService do ...@@ -8,10 +8,6 @@ describe MergeRequests::AddTodoWhenBuildFailsService do
let(:sha) { '1234567890abcdef1234567890abcdef12345678' } let(:sha) { '1234567890abcdef1234567890abcdef12345678' }
let(:ref) { merge_request.source_branch } let(:ref) { merge_request.source_branch }
let(:pipeline) do
create(:ci_pipeline, ref: ref, project: project, sha: sha)
end
let(:service) do let(:service) do
described_class.new(project, user, commit_message: 'Awesome message') described_class.new(project, user, commit_message: 'Awesome message')
end end
...@@ -19,12 +15,11 @@ describe MergeRequests::AddTodoWhenBuildFailsService do ...@@ -19,12 +15,11 @@ describe MergeRequests::AddTodoWhenBuildFailsService do
let(:todo_service) { spy('todo service') } let(:todo_service) { spy('todo service') }
let(:merge_request) do let(:merge_request) do
create(:merge_request, merge_user: user, create(:merge_request, :with_detached_merge_request_pipeline, :opened, merge_user: user)
source_branch: 'master', end
target_branch: 'feature',
source_project: project, let(:pipeline) do
target_project: project, merge_request.all_pipelines.take
state: 'opened')
end end
before do before do
......
...@@ -15,7 +15,7 @@ describe Users::DestroyService do ...@@ -15,7 +15,7 @@ describe Users::DestroyService do
it 'deletes the user' do it 'deletes the user' do
user_data = service.execute(user) 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 { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
expect { Namespace.find(namespace.id) }.to raise_error(ActiveRecord::RecordNotFound) expect { Namespace.find(namespace.id) }.to raise_error(ActiveRecord::RecordNotFound)
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment