Commit 603c7d4c authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 120f4aae
...@@ -44,7 +44,6 @@ const Api = { ...@@ -44,7 +44,6 @@ const Api = {
mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines', mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines',
adminStatisticsPath: '/api/:version/application/statistics', adminStatisticsPath: '/api/:version/application/statistics',
pipelineSinglePath: '/api/:version/projects/:id/pipelines/:pipeline_id', pipelineSinglePath: '/api/:version/projects/:id/pipelines/:pipeline_id',
lsifPath: '/api/:version/projects/:id/commits/:commit_id/lsif/info',
environmentsPath: '/api/:version/projects/:id/environments', environmentsPath: '/api/:version/projects/:id/environments',
group(groupId, callback) { group(groupId, callback) {
...@@ -474,14 +473,6 @@ const Api = { ...@@ -474,14 +473,6 @@ const Api = {
return axios.get(url); return axios.get(url);
}, },
lsifData(projectPath, commitId, paths) {
const url = Api.buildUrl(this.lsifPath)
.replace(':id', encodeURIComponent(projectPath))
.replace(':commit_id', commitId);
return axios.get(url, { params: { paths } });
},
environments(id) { environments(id) {
const url = Api.buildUrl(this.environmentsPath).replace(':id', encodeURIComponent(id)); const url = Api.buildUrl(this.environmentsPath).replace(':id', encodeURIComponent(id));
return axios.get(url); return axios.get(url);
......
...@@ -7,7 +7,7 @@ export default { ...@@ -7,7 +7,7 @@ export default {
Popover, Popover,
}, },
computed: { computed: {
...mapState(['currentDefinition', 'currentDefinitionPosition']), ...mapState(['currentDefinition', 'currentDefinitionPosition', 'definitionPathPrefix']),
}, },
mounted() { mounted() {
this.blobViewer = document.querySelector('.blob-viewer'); this.blobViewer = document.querySelector('.blob-viewer');
...@@ -39,5 +39,6 @@ export default { ...@@ -39,5 +39,6 @@ export default {
v-if="currentDefinition" v-if="currentDefinition"
:position="currentDefinitionPosition" :position="currentDefinitionPosition"
:data="currentDefinition" :data="currentDefinition"
:definition-path-prefix="definitionPathPrefix"
/> />
</template> </template>
...@@ -14,6 +14,10 @@ export default { ...@@ -14,6 +14,10 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
definitionPathPrefix: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -27,6 +31,11 @@ export default { ...@@ -27,6 +31,11 @@ export default {
top: `${this.position.y + this.position.height}px`, top: `${this.position.y + this.position.height}px`,
}; };
}, },
definitionPath() {
return (
this.data.definition_path && `${this.definitionPathPrefix}/${this.data.definition_path}`
);
},
}, },
watch: { watch: {
position: { position: {
...@@ -67,8 +76,8 @@ export default { ...@@ -67,8 +76,8 @@ export default {
{{ hover.value }} {{ hover.value }}
</p> </p>
</div> </div>
<div v-if="data.definition_url" class="popover-body"> <div v-if="definitionPath" class="popover-body">
<gl-button :href="data.definition_url" target="_blank" class="w-100" variant="default"> <gl-button :href="definitionPath" target="_blank" class="w-100" variant="default">
{{ __('Go to definition') }} {{ __('Go to definition') }}
</gl-button> </gl-button>
</div> </div>
......
import api from '~/api'; import axios from '~/lib/utils/axios_utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { getCurrentHoverElement, setCurrentHoverElement, addInteractionClass } from '../utils'; import { getCurrentHoverElement, setCurrentHoverElement, addInteractionClass } from '../utils';
...@@ -12,11 +12,10 @@ export default { ...@@ -12,11 +12,10 @@ export default {
fetchData({ commit, dispatch, state }) { fetchData({ commit, dispatch, state }) {
commit(types.REQUEST_DATA); commit(types.REQUEST_DATA);
api axios
.lsifData(state.projectPath, state.commitId, [state.blobPath]) .get(state.codeNavUrl)
.then(({ data }) => { .then(({ data }) => {
const dataForPath = data[state.blobPath]; const normalizedData = data.reduce((acc, d) => {
const normalizedData = dataForPath.reduce((acc, d) => {
if (d.hover) { if (d.hover) {
acc[`${d.start_line}:${d.start_char}`] = d; acc[`${d.start_line}:${d.start_char}`] = d;
addInteractionClass(d); addInteractionClass(d);
......
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
[types.SET_INITIAL_DATA](state, { projectPath, commitId, blobPath }) { [types.SET_INITIAL_DATA](state, { codeNavUrl, definitionPathPrefix }) {
state.projectPath = projectPath; state.codeNavUrl = codeNavUrl;
state.commitId = commitId; state.definitionPathPrefix = definitionPathPrefix;
state.blobPath = blobPath;
}, },
[types.REQUEST_DATA](state) { [types.REQUEST_DATA](state) {
state.loading = true; state.loading = true;
......
<script> <script>
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import _ from 'underscore'; import { debounce } from 'lodash';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import Item from './item.vue'; import Item from './item.vue';
...@@ -39,7 +39,7 @@ export default { ...@@ -39,7 +39,7 @@ export default {
loadBranches() { loadBranches() {
this.fetchBranches({ search: this.search }); this.fetchBranches({ search: this.search });
}, },
searchBranches: _.debounce(function debounceSearch() { searchBranches: debounce(function debounceSearch() {
this.loadBranches(); this.loadBranches();
}, 250), }, 250),
focusSearch() { focusSearch() {
......
<script> <script>
import _ from 'underscore'; import { escape as esc } from 'lodash';
import { mapState, mapGetters, createNamespacedHelpers } from 'vuex'; import { mapState, mapGetters, createNamespacedHelpers } from 'vuex';
import { sprintf, s__ } from '~/locale'; import { sprintf, s__ } from '~/locale';
import consts from '../../stores/modules/commit/constants'; import consts from '../../stores/modules/commit/constants';
...@@ -22,7 +22,7 @@ export default { ...@@ -22,7 +22,7 @@ export default {
commitToCurrentBranchText() { commitToCurrentBranchText() {
return sprintf( return sprintf(
s__('IDE|Commit to %{branchName} branch'), s__('IDE|Commit to %{branchName} branch'),
{ branchName: `<strong class="monospace">${_.escape(this.currentBranchId)}</strong>` }, { branchName: `<strong class="monospace">${esc(this.currentBranchId)}</strong>` },
false, false,
); );
}, },
......
<script> <script>
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import _ from 'underscore'; import { throttle } from 'lodash';
import { __ } from '../../../locale'; import { __ } from '../../../locale';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
import Icon from '../../../vue_shared/components/icon.vue'; import Icon from '../../../vue_shared/components/icon.vue';
...@@ -53,7 +53,7 @@ export default { ...@@ -53,7 +53,7 @@ export default {
this.$refs.buildTrace.scrollTo(0, 0); this.$refs.buildTrace.scrollTo(0, 0);
} }
}, },
scrollBuildLog: _.throttle(function buildLogScrollDebounce() { scrollBuildLog: throttle(function buildLogScrollDebounce() {
const { scrollTop } = this.$refs.buildTrace; const { scrollTop } = this.$refs.buildTrace;
const { offsetHeight, scrollHeight } = this.$refs.buildTrace; const { offsetHeight, scrollHeight } = this.$refs.buildTrace;
......
<script> <script>
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import _ from 'underscore'; import { debounce } from 'lodash';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
...@@ -59,7 +59,7 @@ export default { ...@@ -59,7 +59,7 @@ export default {
loadMergeRequests() { loadMergeRequests() {
this.fetchMergeRequests({ type: this.type, search: this.search }); this.fetchMergeRequests({ type: this.type, search: this.search });
}, },
searchMergeRequests: _.debounce(function debounceSearch() { searchMergeRequests: debounce(function debounceSearch() {
this.loadMergeRequests(); this.loadMergeRequests();
}, 250), }, 250),
onSearchFocus() { onSearchFocus() {
......
<script> <script>
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import _ from 'underscore';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import ResizablePanel from '../resizable_panel.vue'; import ResizablePanel from '../resizable_panel.vue';
...@@ -55,7 +54,7 @@ export default { ...@@ -55,7 +54,7 @@ export default {
return this.extensionTabs.filter(tab => tab.show); return this.extensionTabs.filter(tab => tab.show);
}, },
tabViews() { tabViews() {
return _.flatten(this.tabs.map(tab => tab.views)); return this.tabs.map(tab => tab.views).flat();
}, },
aliveTabViews() { aliveTabViews() {
return this.tabViews.filter(view => this.isAliveView(view.name)); return this.tabViews.filter(view => this.isAliveView(view.name));
......
<script> <script>
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore'; import { escape as esc } from 'lodash';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import { sprintf, __ } from '../../../locale'; import { sprintf, __ } from '../../../locale';
import Icon from '../../../vue_shared/components/icon.vue'; import Icon from '../../../vue_shared/components/icon.vue';
...@@ -35,7 +35,7 @@ export default { ...@@ -35,7 +35,7 @@ export default {
return sprintf( return sprintf(
__('You can test your .gitlab-ci.yml in %{linkStart}CI Lint%{linkEnd}.'), __('You can test your .gitlab-ci.yml in %{linkStart}CI Lint%{linkEnd}.'),
{ {
linkStart: `<a href="${_.escape(this.currentProject.web_url)}/-/ci/lint">`, linkStart: `<a href="${esc(this.currentProject.web_url)}/-/ci/lint">`,
linkEnd: '</a>', linkEnd: '</a>',
}, },
false, false,
......
<script> <script>
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore'; import { isEmpty } from 'lodash';
import { Manager } from 'smooshpack'; import { Manager } from 'smooshpack';
import { listen } from 'codesandbox-api'; import { listen } from 'codesandbox-api';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
...@@ -78,7 +78,7 @@ export default { ...@@ -78,7 +78,7 @@ export default {
.then(() => this.initPreview()); .then(() => this.initPreview());
}, },
beforeDestroy() { beforeDestroy() {
if (!_.isEmpty(this.manager)) { if (!isEmpty(this.manager)) {
this.manager.listener(); this.manager.listener();
} }
this.manager = {}; this.manager = {};
...@@ -125,7 +125,7 @@ export default { ...@@ -125,7 +125,7 @@ export default {
clearTimeout(this.timeout); clearTimeout(this.timeout);
this.timeout = setTimeout(() => { this.timeout = setTimeout(() => {
if (_.isEmpty(this.manager)) { if (isEmpty(this.manager)) {
this.initPreview(); this.initPreview();
return; return;
......
import Vue from 'vue'; import Vue from 'vue';
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import _ from 'underscore';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import { identity } from 'lodash';
import ide from './components/ide.vue'; import ide from './components/ide.vue';
import store from './stores'; import store from './stores';
import router from './ide_router'; import router from './ide_router';
...@@ -31,7 +31,7 @@ Vue.use(Translate); ...@@ -31,7 +31,7 @@ Vue.use(Translate);
export function initIde(el, options = {}) { export function initIde(el, options = {}) {
if (!el) return null; if (!el) return null;
const { rootComponent = ide, extendStore = _.identity } = options; const { rootComponent = ide, extendStore = identity } = options;
return new Vue({ return new Vue({
el, el,
......
import { Range } from 'monaco-editor'; import { Range } from 'monaco-editor';
import { throttle } from 'underscore'; import { throttle } from 'lodash';
import DirtyDiffWorker from './diff_worker'; import DirtyDiffWorker from './diff_worker';
import Disposable from '../common/disposable'; import Disposable from '../common/disposable';
......
import _ from 'underscore'; import { debounce } from 'lodash';
import { editor as monacoEditor, KeyCode, KeyMod } from 'monaco-editor'; import { editor as monacoEditor, KeyCode, KeyMod } from 'monaco-editor';
import store from '../stores'; import store from '../stores';
import DecorationsController from './decorations/controller'; import DecorationsController from './decorations/controller';
...@@ -38,7 +38,7 @@ export default class Editor { ...@@ -38,7 +38,7 @@ export default class Editor {
setupThemes(); setupThemes();
this.debouncedUpdate = _.debounce(() => { this.debouncedUpdate = debounce(() => {
this.updateDimensions(); this.updateDimensions();
}, 200); }, 200);
} }
......
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
import _ from 'underscore'; import { escape as esc } from 'lodash';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import flash from '~/flash'; import flash from '~/flash';
...@@ -296,7 +296,7 @@ export const getBranchData = ({ commit, state }, { projectId, branchId, force = ...@@ -296,7 +296,7 @@ export const getBranchData = ({ commit, state }, { projectId, branchId, force =
sprintf( sprintf(
__('Branch not loaded - %{branchId}'), __('Branch not loaded - %{branchId}'),
{ {
branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`, branchId: `<strong>${esc(projectId)}/${esc(branchId)}</strong>`,
}, },
false, false,
), ),
......
import _ from 'underscore'; import { escape as esc } from 'lodash';
import flash from '~/flash'; import flash from '~/flash';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import service from '../../services'; import service from '../../services';
...@@ -73,7 +73,7 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => { ...@@ -73,7 +73,7 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => {
text: sprintf( text: sprintf(
__("Branch %{branchName} was not found in this project's repository."), __("Branch %{branchName} was not found in this project's repository."),
{ {
branchName: `<strong>${_.escape(branchId)}</strong>`, branchName: `<strong>${esc(branchId)}</strong>`,
}, },
false, false,
), ),
...@@ -154,7 +154,7 @@ export const openBranch = ({ dispatch, state, getters }, { projectId, branchId, ...@@ -154,7 +154,7 @@ export const openBranch = ({ dispatch, state, getters }, { projectId, branchId,
sprintf( sprintf(
__('An error occurred while getting files for - %{branchId}'), __('An error occurred while getting files for - %{branchId}'),
{ {
branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`, branchId: `<strong>${esc(projectId)}/${esc(branchId)}</strong>`,
}, },
false, false,
), ),
......
import _ from 'underscore'; import { defer } from 'lodash';
import { __ } from '../../../locale'; import { __ } from '../../../locale';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
...@@ -71,7 +71,7 @@ export const getFiles = ({ state, commit, dispatch }, payload = {}) => ...@@ -71,7 +71,7 @@ export const getFiles = ({ state, commit, dispatch }, payload = {}) =>
// Defer setting the directory data because this triggers some intense rendering. // Defer setting the directory data because this triggers some intense rendering.
// The entries is all we need to load the file editor. // The entries is all we need to load the file editor.
_.defer(() => dispatch('setDirectoryData', { projectId, branchId, treeList })); defer(() => dispatch('setDirectoryData', { projectId, branchId, treeList }));
resolve(); resolve();
}) })
......
...@@ -7,17 +7,15 @@ import { ...@@ -7,17 +7,15 @@ import {
GlAlert, GlAlert,
GlDropdown, GlDropdown,
GlDropdownHeader, GlDropdownHeader,
GlDropdownDivider,
GlDropdownItem, GlDropdownItem,
GlFormGroup,
GlSearchBoxByClick,
GlInfiniteScroll, GlInfiniteScroll,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { s__ } from '~/locale';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue'; import LogSimpleFilters from './log_simple_filters.vue';
import LogAdvancedFilters from './log_advanced_filters.vue';
import LogControlButtons from './log_control_buttons.vue'; import LogControlButtons from './log_control_buttons.vue';
import { timeRanges, defaultTimeRange } from '~/vue_shared/constants'; import { defaultTimeRange } from '~/vue_shared/constants';
import { timeRangeFromUrl } from '~/monitoring/utils'; import { timeRangeFromUrl } from '~/monitoring/utils';
import { formatDate } from '../utils'; import { formatDate } from '../utils';
...@@ -28,12 +26,10 @@ export default { ...@@ -28,12 +26,10 @@ export default {
GlAlert, GlAlert,
GlDropdown, GlDropdown,
GlDropdownHeader, GlDropdownHeader,
GlDropdownDivider,
GlDropdownItem, GlDropdownItem,
GlFormGroup,
GlSearchBoxByClick,
GlInfiniteScroll, GlInfiniteScroll,
DateTimePicker, LogSimpleFilters,
LogAdvancedFilters,
LogControlButtons, LogControlButtons,
}, },
filters: { filters: {
...@@ -63,49 +59,22 @@ export default { ...@@ -63,49 +59,22 @@ export default {
traceHeight: 600, traceHeight: 600,
data() { data() {
return { return {
searchQuery: '',
timeRanges,
isElasticStackCalloutDismissed: false, isElasticStackCalloutDismissed: false,
scrollDownButtonDisabled: true, scrollDownButtonDisabled: true,
}; };
}, },
computed: { computed: {
...mapState('environmentLogs', ['environments', 'timeRange', 'logs', 'pods']), ...mapState('environmentLogs', ['environments', 'timeRange', 'logs', 'pods']),
...mapGetters('environmentLogs', ['trace']), ...mapGetters('environmentLogs', ['trace', 'showAdvancedFilters']),
timeRangeModel: {
get() {
return this.timeRange.selected;
},
set(val) {
this.setTimeRange(val);
},
},
showLoader() { showLoader() {
return this.logs.isLoading; return this.logs.isLoading;
}, },
advancedFeaturesEnabled() {
const environment = this.environments.options.find(
({ name }) => name === this.environments.current,
);
return environment && environment.enable_advanced_logs_querying;
},
disableAdvancedControls() {
return this.environments.isLoading || !this.advancedFeaturesEnabled;
},
shouldShowElasticStackCallout() { shouldShowElasticStackCallout() {
return !this.isElasticStackCalloutDismissed && this.disableAdvancedControls; return (
}, !this.isElasticStackCalloutDismissed &&
(this.environments.isLoading || !this.showAdvancedFilters)
podDropdownText() { );
if (this.pods.current) {
return this.pods.current;
} else if (this.advancedFeaturesEnabled) {
// "All pods" is a valid option when advanced querying is available
return s__('Environments|All pods');
}
return s__('Environments|No pod selected');
}, },
}, },
mounted() { mounted() {
...@@ -121,7 +90,6 @@ export default { ...@@ -121,7 +90,6 @@ export default {
...mapActions('environmentLogs', [ ...mapActions('environmentLogs', [
'setInitData', 'setInitData',
'setSearch', 'setSearch',
'setTimeRange',
'showPodLogs', 'showPodLogs',
'showEnvironment', 'showEnvironment',
'fetchEnvironments', 'fetchEnvironments',
...@@ -131,9 +99,6 @@ export default { ...@@ -131,9 +99,6 @@ export default {
isCurrentEnvironment(envName) { isCurrentEnvironment(envName) {
return envName === this.environments.current; return envName === this.environments.current;
}, },
isCurrentPod(podName) {
return podName === this.pods.current;
},
topReached() { topReached() {
if (!this.logs.isLoading) { if (!this.logs.isLoading) {
this.fetchMoreLogsPrepend(); this.fetchMoreLogsPrepend();
...@@ -167,20 +132,13 @@ export default { ...@@ -167,20 +132,13 @@ export default {
</strong> </strong>
</a> </a>
</gl-alert> </gl-alert>
<div class="top-bar js-top-bar d-flex"> <div class="top-bar d-md-flex border bg-secondary-50 pt-2 pr-1 pb-0 pl-2">
<div class="row mx-n1"> <div class="flex-grow-0">
<gl-form-group
id="environments-dropdown-fg"
label-size="sm"
label-for="environments-dropdown"
class="col-3 px-1"
>
<gl-dropdown <gl-dropdown
id="environments-dropdown" id="environments-dropdown"
:text="environments.current" :text="environments.current"
:disabled="environments.isLoading" :disabled="environments.isLoading"
class="d-flex gl-h-32 js-environments-dropdown" class="mb-2 gl-h-32 pr-2 d-flex d-md-block js-environments-dropdown"
toggle-class="dropdown-menu-toggle"
> >
<gl-dropdown-header class="text-center"> <gl-dropdown-header class="text-center">
{{ s__('Environments|Select environment') }} {{ s__('Environments|Select environment') }}
...@@ -199,91 +157,24 @@ export default { ...@@ -199,91 +157,24 @@ export default {
</div> </div>
</gl-dropdown-item> </gl-dropdown-item>
</gl-dropdown> </gl-dropdown>
</gl-form-group>
<gl-form-group
id="pods-dropdown-fg"
label-size="sm"
label-for="pods-dropdown"
class="col-3 px-1"
>
<gl-dropdown
id="pods-dropdown"
:text="podDropdownText"
:disabled="environments.isLoading"
class="d-flex gl-h-32 js-pods-dropdown"
toggle-class="dropdown-menu-toggle"
>
<gl-dropdown-header class="text-center">
{{ s__('Environments|Filter by pod') }}
</gl-dropdown-header>
<template v-if="advancedFeaturesEnabled">
<gl-dropdown-item key="all-pods" @click="showPodLogs(null)">
<div class="d-flex">
<gl-icon
:class="{ invisible: !isCurrentPod(null) }"
name="status_success_borderless"
/>
<div class="flex-grow-1">{{ s__('Environments|All pods') }}</div>
</div> </div>
</gl-dropdown-item>
<gl-dropdown-divider />
</template>
<gl-dropdown-item v-if="!pods.options.length" :disabled="true"> <log-advanced-filters
<span class="text-muted"> v-if="showAdvancedFilters"
{{ s__('Environments|No pods to display') }} ref="log-advanced-filters"
</span> class="d-md-flex flex-grow-1"
</gl-dropdown-item> :disabled="environments.isLoading"
<gl-dropdown-item
v-for="podName in pods.options"
:key="podName"
class="text-nowrap"
@click="showPodLogs(podName)"
>
<div class="d-flex">
<gl-icon
:class="{ invisible: !isCurrentPod(podName) }"
name="status_success_borderless"
/>
<div class="flex-grow-1">{{ podName }}</div>
</div>
</gl-dropdown-item>
</gl-dropdown>
</gl-form-group>
<gl-form-group id="search-fg" label-size="sm" label-for="search" class="col-3 px-1">
<gl-search-box-by-click
v-model.trim="searchQuery"
:disabled="disableAdvancedControls"
:placeholder="s__('Environments|Search')"
class="js-logs-search"
type="search"
autofocus
@submit="setSearch(searchQuery)"
/> />
</gl-form-group> <log-simple-filters
v-else
<gl-form-group ref="log-simple-filters"
id="dates-fg" class="d-md-flex flex-grow-1"
label-size="sm" :disabled="environments.isLoading"
label-for="time-window-dropdown"
class="col-3 px-1"
>
<date-time-picker
ref="dateTimePicker"
v-model="timeRangeModel"
class="w-100 gl-h-32"
right
:disabled="disableAdvancedControls"
:options="timeRanges"
/> />
</gl-form-group>
</div>
<log-control-buttons <log-control-buttons
ref="scrollButtons" ref="scrollButtons"
class="controllers" class="flex-grow-0 pr-2 mb-2 controllers"
:scroll-down-button-disabled="scrollDownButtonDisabled" :scroll-down-button-disabled="scrollDownButtonDisabled"
@refresh="showPodLogs(pods.current)" @refresh="showPodLogs(pods.current)"
@scrollDown="scrollDown" @scrollDown="scrollDown"
......
<script>
import { s__ } from '~/locale';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import { mapActions, mapState } from 'vuex';
import {
GlIcon,
GlDropdown,
GlDropdownHeader,
GlDropdownDivider,
GlDropdownItem,
GlSearchBoxByClick,
} from '@gitlab/ui';
import { timeRanges } from '~/vue_shared/constants';
export default {
components: {
GlIcon,
GlDropdown,
GlDropdownHeader,
GlDropdownDivider,
GlDropdownItem,
GlSearchBoxByClick,
DateTimePicker,
},
props: {
disabled: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
timeRanges,
searchQuery: '',
};
},
computed: {
...mapState('environmentLogs', ['timeRange', 'pods']),
timeRangeModel: {
get() {
return this.timeRange.selected;
},
set(val) {
this.setTimeRange(val);
},
},
podDropdownText() {
return this.pods.current || s__('Environments|All pods');
},
},
methods: {
...mapActions('environmentLogs', ['setSearch', 'showPodLogs', 'setTimeRange']),
isCurrentPod(podName) {
return podName === this.pods.current;
},
},
};
</script>
<template>
<div>
<gl-dropdown
ref="podsDropdown"
:text="podDropdownText"
:disabled="disabled"
class="mb-2 gl-h-32 pr-2 d-flex d-md-block flex-grow-0 qa-pods-dropdown"
>
<gl-dropdown-header class="text-center">
{{ s__('Environments|Filter by pod') }}
</gl-dropdown-header>
<gl-dropdown-item v-if="!pods.options.length" disabled>
<span ref="noPodsMsg" class="text-muted">
{{ s__('Environments|No pods to display') }}
</span>
</gl-dropdown-item>
<template v-else>
<gl-dropdown-item ref="allPodsOption" key="all-pods" @click="showPodLogs(null)">
<div class="d-flex">
<gl-icon
:class="{ invisible: pods.current !== null }"
name="status_success_borderless"
/>
<div class="flex-grow-1">{{ s__('Environments|All pods') }}</div>
</div>
</gl-dropdown-item>
<gl-dropdown-divider />
<gl-dropdown-item
v-for="podName in pods.options"
:key="podName"
class="text-nowrap"
@click="showPodLogs(podName)"
>
<div class="d-flex">
<gl-icon
:class="{ invisible: !isCurrentPod(podName) }"
name="status_success_borderless"
/>
<div class="flex-grow-1">{{ podName }}</div>
</div>
</gl-dropdown-item>
</template>
</gl-dropdown>
<gl-search-box-by-click
ref="searchBox"
v-model.trim="searchQuery"
:disabled="disabled"
:placeholder="s__('Environments|Search')"
class="mb-2 pr-2 flex-grow-1"
type="search"
autofocus
@submit="setSearch(searchQuery)"
/>
<date-time-picker
ref="dateTimePicker"
v-model="timeRangeModel"
:disabled="disabled"
:options="timeRanges"
class="mb-2 gl-h-32 pr-2 d-block date-time-picker-wrapper"
right
/>
</div>
</template>
<script>
import { s__ } from '~/locale';
import { mapActions, mapState } from 'vuex';
import { GlIcon, GlDropdown, GlDropdownHeader, GlDropdownItem } from '@gitlab/ui';
export default {
components: {
GlIcon,
GlDropdown,
GlDropdownHeader,
GlDropdownItem,
},
props: {
disabled: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
searchQuery: '',
};
},
computed: {
...mapState('environmentLogs', ['pods']),
podDropdownText() {
return this.pods.current || s__('Environments|No pod selected');
},
},
methods: {
...mapActions('environmentLogs', ['showPodLogs']),
isCurrentPod(podName) {
return podName === this.pods.current;
},
},
};
</script>
<template>
<div>
<gl-dropdown
ref="podsDropdown"
:text="podDropdownText"
:disabled="disabled"
class="mb-2 gl-h-32 pr-2 d-flex d-md-block flex-grow-0 qa-pods-dropdown"
>
<gl-dropdown-header class="text-center">
{{ s__('Environments|Select pod') }}
</gl-dropdown-header>
<gl-dropdown-item v-if="!pods.options.length" disabled>
<span ref="noPodsMsg" class="text-muted">
{{ s__('Environments|No pods to display') }}
</span>
</gl-dropdown-item>
<gl-dropdown-item
v-for="podName in pods.options"
:key="podName"
class="text-nowrap"
@click="showPodLogs(podName)"
>
<div class="d-flex">
<gl-icon
:class="{ invisible: !isCurrentPod(podName) }"
name="status_success_borderless"
/>
<div class="flex-grow-1">{{ podName }}</div>
</div>
</gl-dropdown-item>
</gl-dropdown>
</div>
</template>
...@@ -5,5 +5,9 @@ const mapTrace = ({ timestamp = null, pod = '', message = '' }) => ...@@ -5,5 +5,9 @@ const mapTrace = ({ timestamp = null, pod = '', message = '' }) =>
export const trace = state => state.logs.lines.map(mapTrace).join('\n'); export const trace = state => state.logs.lines.map(mapTrace).join('\n');
// prevent babel-plugin-rewire from generating an invalid default during karma tests export const showAdvancedFilters = state => {
export default () => {}; const environment = state.environments.options.find(
({ name }) => name === state.environments.current,
);
return Boolean(environment?.enable_advanced_logs_querying);
};
...@@ -379,25 +379,19 @@ ...@@ -379,25 +379,19 @@
} }
.top-bar { .top-bar {
@include build-trace-top-bar($gl-line-height * 3); .date-time-picker-wrapper,
position: relative; .dropdown-toggle {
top: 0; @include media-breakpoint-up(md) {
width: 140px;
.dropdown-menu-toggle { }
width: 200px;
@include media-breakpoint-up(sm) { @include media-breakpoint-up(lg) {
width: 300px; width: 160px;
} }
} }
.controllers { .controllers {
@include build-controllers(16px, flex-end, true, 2); @include build-controllers(16px, flex-end, false, 2);
}
.refresh-control {
@include build-controllers(16px, flex-end, true, 0);
margin-left: 2px;
} }
} }
......
...@@ -43,6 +43,7 @@ ...@@ -43,6 +43,7 @@
.border-color-blue-300 { border-color: $blue-300; } .border-color-blue-300 { border-color: $blue-300; }
.border-color-default { border-color: $border-color; } .border-color-default { border-color: $border-color; }
.border-bottom-color-default { border-bottom-color: $border-color; } .border-bottom-color-default { border-bottom-color: $border-color; }
.border-radius-default { border-radius: $border-radius-default; }
.box-shadow-default { box-shadow: 0 2px 4px 0 $black-transparent; } .box-shadow-default { box-shadow: 0 2px 4px 0 $black-transparent; }
.gl-children-ml-sm-3 > * { .gl-children-ml-sm-3 > * {
......
...@@ -3,11 +3,10 @@ ...@@ -3,11 +3,10 @@
class Admin::ServicesController < Admin::ApplicationController class Admin::ServicesController < Admin::ApplicationController
include ServiceParams include ServiceParams
before_action :whitelist_query_limiting, only: [:index]
before_action :service, only: [:edit, :update] before_action :service, only: [:edit, :update]
def index def index
@services = services_templates @services = Service.find_or_create_templates
end end
def edit def edit
...@@ -30,22 +29,9 @@ class Admin::ServicesController < Admin::ApplicationController ...@@ -30,22 +29,9 @@ class Admin::ServicesController < Admin::ApplicationController
private private
# rubocop: disable CodeReuse/ActiveRecord
def services_templates
Service.available_services_names.map do |service_name|
service_template = "#{service_name}_service".camelize.constantize
service_template.where(template: true).first_or_create
end
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def service def service
@service ||= Service.find_by(id: params[:id], template: true) @service ||= Service.find_by(id: params[:id], template: true)
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42430')
end
end end
...@@ -208,11 +208,24 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -208,11 +208,24 @@ class Projects::BlobController < Projects::ApplicationController
.last_for_path(@repository, @ref, @path).sha .last_for_path(@repository, @ref, @path).sha
end end
def set_code_navigation_build
return if Feature.disabled?(:code_navigation, @project)
artifact =
Ci::JobArtifact
.for_sha(@blob.commit_id, @project.id)
.for_job_name(Ci::Build::CODE_NAVIGATION_JOB_NAME)
.last
@code_navigation_build = artifact&.job
end
def show_html def show_html
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit } environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
environment_params[:find_latest] = true environment_params[:find_latest] = true
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
@last_commit = @repository.last_commit_for_path(@commit.id, @blob.path) @last_commit = @repository.last_commit_for_path(@commit.id, @blob.path)
set_code_navigation_build
render 'show' render 'show'
end end
......
...@@ -26,7 +26,7 @@ module MilestonesHelper ...@@ -26,7 +26,7 @@ module MilestonesHelper
end end
end end
def milestones_label_path(opts = {}) def milestones_issues_path(opts = {})
if @project if @project
project_issues_path(@project, opts) project_issues_path(@project, opts)
elsif @group elsif @group
...@@ -283,6 +283,27 @@ module MilestonesHelper ...@@ -283,6 +283,27 @@ module MilestonesHelper
can?(current_user, :admin_milestone, @project.group) can?(current_user, :admin_milestone, @project.group)
end end
end end
def display_issues_count_warning?(milestone)
milestone_visible_issues_count(milestone) > Milestone::DISPLAY_ISSUES_LIMIT
end
def milestone_issues_count_message(milestone)
total_count = milestone_visible_issues_count(milestone)
limit = Milestone::DISPLAY_ISSUES_LIMIT
link_options = { milestone_title: @milestone.title }
message = _('Showing %{limit} of %{total_count} issues. ') % { limit: limit, total_count: total_count }
message += link_to(_('View all issues'), milestones_issues_path(link_options))
message.html_safe
end
private
def milestone_visible_issues_count(milestone)
@milestone_visible_issues_count ||= milestone.issues_visible_to_user(current_user).size
end
end end
MilestonesHelper.prepend_if_ee('EE::MilestonesHelper') MilestonesHelper.prepend_if_ee('EE::MilestonesHelper')
...@@ -33,6 +33,8 @@ module Ci ...@@ -33,6 +33,8 @@ module Ci
scheduler_failure: 2 scheduler_failure: 2
}.freeze }.freeze
CODE_NAVIGATION_JOB_NAME = 'code_navigation'
has_one :deployment, as: :deployable, class_name: 'Deployment' has_one :deployment, as: :deployable, class_name: 'Deployment'
has_one :resource, class_name: 'Ci::Resource', inverse_of: :build has_one :resource, class_name: 'Ci::Resource', inverse_of: :build
has_many :trace_sections, class_name: 'Ci::BuildTraceSection' has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
......
...@@ -31,7 +31,8 @@ module Ci ...@@ -31,7 +31,8 @@ module Ci
metrics: 'metrics.txt', metrics: 'metrics.txt',
lsif: 'lsif.json', lsif: 'lsif.json',
dotenv: '.env', dotenv: '.env',
cobertura: 'cobertura-coverage.xml' cobertura: 'cobertura-coverage.xml',
terraform: 'tfplan.json'
}.freeze }.freeze
INTERNAL_TYPES = { INTERNAL_TYPES = {
...@@ -59,7 +60,8 @@ module Ci ...@@ -59,7 +60,8 @@ module Ci
dast: :raw, dast: :raw,
license_management: :raw, license_management: :raw,
license_scanning: :raw, license_scanning: :raw,
performance: :raw performance: :raw,
terraform: :raw
}.freeze }.freeze
TYPE_AND_FORMAT_PAIRS = INTERNAL_TYPES.merge(REPORT_TYPES).freeze TYPE_AND_FORMAT_PAIRS = INTERNAL_TYPES.merge(REPORT_TYPES).freeze
...@@ -80,6 +82,7 @@ module Ci ...@@ -80,6 +82,7 @@ module Ci
scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) } scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
scope :with_files_stored_remotely, -> { where(file_store: ::JobArtifactUploader::Store::REMOTE) } scope :with_files_stored_remotely, -> { where(file_store: ::JobArtifactUploader::Store::REMOTE) }
scope :for_sha, ->(sha, project_id) { joins(job: :pipeline).where(ci_pipelines: { sha: sha, project_id: project_id }) } scope :for_sha, ->(sha, project_id) { joins(job: :pipeline).where(ci_pipelines: { sha: sha, project_id: project_id }) }
scope :for_job_name, ->(name) { joins(:job).where(ci_builds: { name: name }) }
scope :with_file_types, -> (file_types) do scope :with_file_types, -> (file_types) do
types = self.file_types.select { |file_type| file_types.include?(file_type) }.values types = self.file_types.select { |file_type| file_types.include?(file_type) }.values
...@@ -129,7 +132,8 @@ module Ci ...@@ -129,7 +132,8 @@ module Ci
network_referee: 14, ## runner referees network_referee: 14, ## runner referees
lsif: 15, # LSIF data for code navigation lsif: 15, # LSIF data for code navigation
dotenv: 16, dotenv: 16,
cobertura: 17 cobertura: 17,
terraform: 18 # Transformed json
} }
enum file_format: { enum file_format: {
......
# frozen_string_literal: true # frozen_string_literal: true
module Milestoneish module Milestoneish
DISPLAY_ISSUES_LIMIT = 3000
def total_issues_count def total_issues_count
@total_issues_count ||= Milestones::IssuesCountService.new(self).count @total_issues_count ||= Milestones::IssuesCountService.new(self).count
end end
...@@ -55,7 +57,15 @@ module Milestoneish ...@@ -55,7 +57,15 @@ module Milestoneish
end end
def sorted_issues(user) def sorted_issues(user)
issues_visible_to_user(user).preload_associated_models.sort_by_attribute('label_priority') # This method is used on milestone view to filter opened assigned, opened unassigned and closed issues columns.
# We want a limit of DISPLAY_ISSUES_LIMIT for total issues present on all columns.
limited_ids =
issues_visible_to_user(user).sort_by_attribute('label_priority').limit(DISPLAY_ISSUES_LIMIT)
Issue
.where(id: Issue.select(:id).from(limited_ids))
.preload_associated_models
.sort_by_attribute('label_priority')
end end
def sorted_merge_requests(user) def sorted_merge_requests(user)
......
...@@ -70,7 +70,7 @@ class Issue < ApplicationRecord ...@@ -70,7 +70,7 @@ class Issue < ApplicationRecord
scope :order_closed_date_desc, -> { reorder(closed_at: :desc) } scope :order_closed_date_desc, -> { reorder(closed_at: :desc) }
scope :order_created_at_desc, -> { reorder(created_at: :desc) } scope :order_created_at_desc, -> { reorder(created_at: :desc) }
scope :preload_associated_models, -> { preload(:labels, project: :namespace) } scope :preload_associated_models, -> { preload(:assignees, :labels, project: :namespace) }
scope :with_api_entity_associations, -> { preload(:timelogs, :assignees, :author, :notes, :labels, project: [:route, { namespace: :route }] ) } scope :with_api_entity_associations, -> { preload(:timelogs, :assignees, :author, :notes, :labels, project: [:route, { namespace: :route }] ) }
scope :public_only, -> { where(confidential: false) } scope :public_only, -> { where(confidential: false) }
......
...@@ -46,6 +46,7 @@ class Service < ApplicationRecord ...@@ -46,6 +46,7 @@ class Service < ApplicationRecord
scope :active, -> { where(active: true) } scope :active, -> { where(active: true) }
scope :without_defaults, -> { where(default: false) } scope :without_defaults, -> { where(default: false) }
scope :by_type, -> (type) { where(type: type) } scope :by_type, -> (type) { where(type: type) }
scope :templates, -> { where(template: true, type: available_services_types) }
scope :push_hooks, -> { where(push_events: true, active: true) } scope :push_hooks, -> { where(push_events: true, active: true) }
scope :tag_push_hooks, -> { where(tag_push_events: true, active: true) } scope :tag_push_hooks, -> { where(tag_push_events: true, active: true) }
...@@ -259,14 +260,32 @@ class Service < ApplicationRecord ...@@ -259,14 +260,32 @@ class Service < ApplicationRecord
self.category == :issue_tracker self.category == :issue_tracker
end end
# Find all service templates; if some of them do not exist, create them
# within a transaction to perform the lowest possible SQL queries.
def self.find_or_create_templates
create_nonexistent_templates
templates
end
private_class_method def self.create_nonexistent_templates
nonexistent_services = available_services_types - templates.map(&:type)
return if nonexistent_services.empty?
transaction do
nonexistent_services.each do |service_type|
service_type.constantize.create(template: true)
end
end
end
def self.available_services_names def self.available_services_names
service_names = %w[ service_names = %w[
alerts alerts
asana asana
assembla assembla
bamboo bamboo
buildkite
bugzilla bugzilla
buildkite
campfire campfire
custom_issue_tracker custom_issue_tracker
discord discord
...@@ -278,20 +297,20 @@ class Service < ApplicationRecord ...@@ -278,20 +297,20 @@ class Service < ApplicationRecord
hipchat hipchat
irker irker
jira jira
mattermost_slash_commands
mattermost mattermost
mattermost_slash_commands
microsoft_teams
packagist packagist
pipelines_email pipelines_email
pivotaltracker pivotaltracker
prometheus prometheus
pushover pushover
redmine redmine
youtrack
slack_slash_commands
slack slack
slack_slash_commands
teamcity teamcity
microsoft_teams
unify_circuit unify_circuit
youtrack
] ]
if Rails.env.development? if Rails.env.development?
...@@ -301,6 +320,10 @@ class Service < ApplicationRecord ...@@ -301,6 +320,10 @@ class Service < ApplicationRecord
service_names.sort_by(&:downcase) service_names.sort_by(&:downcase)
end end
def self.available_services_types
available_services_names.map { |service_name| "#{service_name}_service".camelize }
end
def self.build_from_template(project_id, template) def self.build_from_template(project_id, template)
service = template.dup service = template.dup
......
...@@ -26,14 +26,14 @@ module ContentTypeWhitelist ...@@ -26,14 +26,14 @@ module ContentTypeWhitelist
# Here we override and extend CarrierWave's method that does not parse the # Here we override and extend CarrierWave's method that does not parse the
# magic headers. # magic headers.
def check_content_type_whitelist!(new_file) def check_content_type_whitelist!(new_file)
new_file.content_type = mime_magic_content_type(new_file.path) if content_type_whitelist
content_type = mime_magic_content_type(new_file.path)
if content_type_whitelist && !whitelisted_content_type?(new_file.content_type) unless whitelisted_content_type?(content_type)
message = I18n.translate(:"errors.messages.content_type_whitelist_error", allowed_types: Array(content_type_whitelist).join(", ")) message = I18n.translate(:"errors.messages.content_type_whitelist_error", allowed_types: Array(content_type_whitelist).join(", "))
raise CarrierWave::IntegrityError, message raise CarrierWave::IntegrityError, message
end end
end
super(new_file)
end end
def whitelisted_content_type?(content_type) def whitelisted_content_type?(content_type)
......
...@@ -9,8 +9,9 @@ ...@@ -9,8 +9,9 @@
= render "projects/blob/auxiliary_viewer", blob: blob = render "projects/blob/auxiliary_viewer", blob: blob
#blob-content-holder.blob-content-holder #blob-content-holder.blob-content-holder
- if native_code_navigation_enabled?(@project) - if @code_navigation_build
#js-code-navigation{ data: { commit_id: blob.commit_id, blob_path: blob.path, project_path: @project.full_path } } - code_nav_url = raw_project_job_artifacts_url(@project, @code_navigation_build, path: "lsif/#{blob.path}")
#js-code-navigation{ data: { code_nav_url: "#{code_nav_url}.json", definition_path_prefix: project_blob_path(@project, @ref) } }
%article.file-holder %article.file-holder
= render 'projects/blob/header', blob: blob = render 'projects/blob/header', blob: blob
= render 'projects/blob/content', blob: blob = render 'projects/blob/content', blob: blob
- args = { show_project_name: local_assigns.fetch(:show_project_name, false), - args = { show_project_name: local_assigns.fetch(:show_project_name, false),
show_full_project_name: local_assigns.fetch(:show_full_project_name, false) } show_full_project_name: local_assigns.fetch(:show_full_project_name, false) }
- if display_issues_count_warning?(@milestone)
.flash-container
.flash-warning#milestone-issue-count-warning
= milestone_issues_count_message(@milestone)
.row.prepend-top-default .row.prepend-top-default
.col-md-4 .col-md-4
= render 'shared/milestones/issuables', args.merge(title: 'Unstarted Issues (open and unassigned)', issuables: issues.opened.unassigned, id: 'unassigned', show_counter: true) = render 'shared/milestones/issuables', args.merge(title: 'Unstarted Issues (open and unassigned)', issuables: issues.opened.unassigned, id: 'unassigned', show_counter: true)
......
...@@ -3,12 +3,12 @@ ...@@ -3,12 +3,12 @@
- options = { milestone_title: @milestone.title, label_name: label.title } - options = { milestone_title: @milestone.title, label_name: label.title }
%li.no-border %li.no-border
= render_label(label, tooltip: false, link: milestones_label_path(options)) = render_label(label, tooltip: false, link: milestones_issues_path(options))
%span.prepend-description-left %span.prepend-description-left
= markdown_field(label, :description) = markdown_field(label, :description)
.float-right.d-none.d-lg-block.d-xl-block .float-right.d-none.d-lg-block.d-xl-block
= link_to milestones_label_path(options.merge(state: 'opened')), class: 'btn btn-transparent btn-action' do = link_to milestones_issues_path(options.merge(state: 'opened')), class: 'btn btn-transparent btn-action' do
- pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), 'open issue' - pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), 'open issue'
= link_to milestones_label_path(options.merge(state: 'closed')), class: 'btn btn-transparent btn-action' do = link_to milestones_issues_path(options.merge(state: 'closed')), class: 'btn btn-transparent btn-action' do
- pluralize milestone_issues_by_label_count(@milestone, label, state: :closed), 'closed issue' - pluralize milestone_issues_by_label_count(@milestone, label, state: :closed), 'closed issue'
---
title: Improve logs filters on mobile, simplify kubernetes API logs filters
merge_request: 27484
author:
type: added
---
title: Reduce number of SQL queries for service templates
merge_request: 27396
author:
type: performance
---
title: Optimize ci builds counters in usage data
merge_request: 27770
author:
type: performance
---
title: Leave upload Content-Type unchaged
merge_request: 27864
author:
type: fixed
---
title: Limits issues displayed on milestones
merge_request: 23102
author:
type: performance
---
title: Log Redis call count and duration to log files
merge_request: 27735
author:
type: other
...@@ -28,6 +28,8 @@ def check_changelog_yaml(path) ...@@ -28,6 +28,8 @@ def check_changelog_yaml(path)
if yaml["merge_request"].nil? && !helper.security_mr? if yaml["merge_request"].nil? && !helper.security_mr?
message "Consider setting `merge_request` to #{gitlab.mr_json["iid"]} in #{gitlab.html_link(path)}. #{SEE_DOC}" message "Consider setting `merge_request` to #{gitlab.mr_json["iid"]} in #{gitlab.html_link(path)}. #{SEE_DOC}"
elsif yaml["merge_request"] != gitlab.mr_json["iid"]
fail "Merge request ID was not set to #{gitlab.mr_json["iid"]}! #{SEE_DOC}"
end end
rescue Psych::SyntaxError, Psych::DisallowedClass, Psych::BadAlias rescue Psych::SyntaxError, Psych::DisallowedClass, Psych::BadAlias
# YAML could not be parsed, fail the build. # YAML could not be parsed, fail the build.
......
# frozen_string_literal: true
class AddIndexOnUserAndCreatedAtToCiBuilds < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
INDEX_NAME = 'index_ci_builds_on_user_id_and_created_at_and_type_eq_ci_build'
def up
add_concurrent_index :ci_builds, [:user_id, :created_at], where: "type = 'Ci::Build'", name: INDEX_NAME
end
def down
remove_concurrent_index :ci_builds, INDEX_NAME
end
end
...@@ -8603,6 +8603,8 @@ CREATE INDEX index_ci_builds_on_upstream_pipeline_id ON public.ci_builds USING b ...@@ -8603,6 +8603,8 @@ CREATE INDEX index_ci_builds_on_upstream_pipeline_id ON public.ci_builds USING b
CREATE INDEX index_ci_builds_on_user_id ON public.ci_builds USING btree (user_id); CREATE INDEX index_ci_builds_on_user_id ON public.ci_builds USING btree (user_id);
CREATE INDEX index_ci_builds_on_user_id_and_created_at_and_type_eq_ci_build ON public.ci_builds USING btree (user_id, created_at) WHERE ((type)::text = 'Ci::Build'::text);
CREATE INDEX index_ci_builds_project_id_and_status_for_live_jobs_partial2 ON public.ci_builds USING btree (project_id, status) WHERE (((type)::text = 'Ci::Build'::text) AND ((status)::text = ANY (ARRAY[('running'::character varying)::text, ('pending'::character varying)::text, ('created'::character varying)::text]))); CREATE INDEX index_ci_builds_project_id_and_status_for_live_jobs_partial2 ON public.ci_builds USING btree (project_id, status) WHERE (((type)::text = 'Ci::Build'::text) AND ((status)::text = ANY (ARRAY[('running'::character varying)::text, ('pending'::character varying)::text, ('created'::character varying)::text])));
CREATE UNIQUE INDEX index_ci_builds_runner_session_on_build_id ON public.ci_builds_runner_session USING btree (build_id); CREATE UNIQUE INDEX index_ci_builds_runner_session_on_build_id ON public.ci_builds_runner_session USING btree (build_id);
...@@ -12746,5 +12748,6 @@ INSERT INTO "schema_migrations" (version) VALUES ...@@ -12746,5 +12748,6 @@ INSERT INTO "schema_migrations" (version) VALUES
('20200318165448'), ('20200318165448'),
('20200318175008'), ('20200318175008'),
('20200319203901'), ('20200319203901'),
('20200323075043'); ('20200323075043'),
('20200323122201');
...@@ -757,10 +757,7 @@ Repositories may be moved from one storage location using the [Repository ...@@ -757,10 +757,7 @@ Repositories may be moved from one storage location using the [Repository
API](../../api/projects.html#edit-project): API](../../api/projects.html#edit-project):
```shell ```shell
curl --request PUT \ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --data "repository_storage=praefect" https://example.gitlab.com/api/v4/projects/123
--header "PRIVATE-TOKEN: <your_access_token>" \
--data "repository_storage=praefect" \
https://example.gitlab.com/api/v4/projects/123
``` ```
## Debugging Praefect ## Debugging Praefect
......
...@@ -255,7 +255,10 @@ sudo gitlab-rake gitlab:exclusive_lease:clear[project_housekeeping:4] ...@@ -255,7 +255,10 @@ sudo gitlab-rake gitlab:exclusive_lease:clear[project_housekeeping:4]
## Display status of database migrations ## Display status of database migrations
To check the status of migrations, you can use the following rake task: See the [upgrade documentation](../../update/README.md#checking-for-background-migrations-before-upgrading)
for how to check that migrations are complete when upgrading GitLab.
To check the status of specific migrations, you can use the following rake task:
```shell ```shell
sudo gitlab-rake db:migrate:status sudo gitlab-rake db:migrate:status
......
...@@ -867,43 +867,7 @@ end ...@@ -867,43 +867,7 @@ end
## Sidekiq ## Sidekiq
### Kill a worker's Sidekiq jobs This content has been moved to the [Troubleshooting Sidekiq docs](./sidekiq.md).
```ruby
queue = Sidekiq::Queue.new('repository_import')
queue.each { |job| job.delete if <condition>}
```
`<condition>` probably includes references to job arguments, which depend on the type of job in question.
| queue | worker | job args |
| ----- | ------ | -------- |
| repository_import | RepositoryImportWorker | project_id |
| update_merge_requests | UpdateMergeRequestsWorker | project_id, user_id, oldrev, newrev, ref |
**Example:** Delete all UpdateMergeRequestsWorker jobs associated with a merge request on project_id 125,
merging branch `ref/heads/my_branch`.
```ruby
queue = Sidekiq::Queue.new('update_merge_requests')
queue.each { |job| job.delete if job.args[0]==125 and job.args[4]=='ref/heads/my_branch'}
```
**Note:** Running jobs will not be killed. Stop Sidekiq before doing this, to get all matching jobs.
### Enable debug logging of Sidekiq
```ruby
gitlab_rails['env'] = {
'SIDEKIQ_LOG_ARGUMENTS' => "1"
}
```
Then `gitlab-ctl reconfigure; gitlab-ctl restart sidekiq`. The Sidekiq logs will now include additional data for troubleshooting.
### Sidekiq kill signals
See <https://github.com/mperham/sidekiq/wiki/Signals#ttin>.
## Redis ## Redis
......
...@@ -180,6 +180,13 @@ detach ...@@ -180,6 +180,13 @@ detach
exit exit
``` ```
## Sidekiq kill signals
TTIN was described above as the signal to print backtraces for logging, however
Sidekiq responds to other signals as well. For example, TSTP and TERM can be used
to gracefully shut Sidekiq down, see
[the Sidekiq Signals docs](https://github.com/mperham/sidekiq/wiki/Signals#ttin).
## Check for blocking queries ## Check for blocking queries
Sometimes the speed at which Sidekiq processes jobs can be so fast that it can Sometimes the speed at which Sidekiq processes jobs can be so fast that it can
...@@ -260,9 +267,34 @@ end ...@@ -260,9 +267,34 @@ end
### Remove Sidekiq jobs for given parameters (destructive) ### Remove Sidekiq jobs for given parameters (destructive)
The general method to kill jobs conditionally is the following:
```ruby
queue = Sidekiq::Queue.new('<queue name>')
queue.each { |job| job.delete if <condition>}
```
NOTE: **Note:** This will remove jobs that are queued but not started, running jobs will not be killed. Have a look at the section below for cancelling running jobs.
In the method above, `<queue-name>` is the name of the queue that contains the job(s) you want to delete and `<condition>` will decide which jobs get deleted.
Commonly, `<condition>` references the job arguments, which depend on the type of job in question. To find the arguments for a specific queue, you can have a look at the `perform` function of the related worker file, commonly found at `/app/workers/<queue-name>_worker.rb`.
For example, `repository_import` has `project_id` as the job argument, while `update_merge_requests` has `project_id, user_id, oldrev, newrev, ref`.
NOTE: **Note:** Arguments need to be referenced by their sequence id using `job.args[<id>]` because `job.args` is a list of all arguments provided to the Sidekiq job.
Here are some examples:
```ruby
queue = Sidekiq::Queue.new('update_merge_requests')
# In this example, we want to remove any update_merge_requests jobs
# for the Project with ID 125 and ref `ref/heads/my_branch`
queue.each { |job| job.delete if job.args[0] == 125 and job.args[4] == 'ref/heads/my_branch' }
```
```ruby ```ruby
# for jobs like this: # Cancelling jobs like: `RepositoryImportWorker.new.perform_async(100)`
# RepositoryImportWorker.new.perform_async(100)
id_list = [100] id_list = [100]
queue = Sidekiq::Queue.new('repository_import') queue = Sidekiq::Queue.new('repository_import')
......
...@@ -150,8 +150,6 @@ For example: ...@@ -150,8 +150,6 @@ For example:
## Structure ## Structure
### Organize by topic, not by type
Because we want documentation to be a SSOT, we should [organize by topic, not by type](#organize-by-topic-not-by-type). Because we want documentation to be a SSOT, we should [organize by topic, not by type](#organize-by-topic-not-by-type).
### Folder structure overview ### Folder structure overview
...@@ -619,6 +617,22 @@ do not use this option until further notice. ...@@ -619,6 +617,22 @@ do not use this option until further notice.
## Links ## Links
Links are important in GitLab documentation. They allow you to [link instead of summarizing](#link-instead-of-summarize)
to help preserve an [SSoT](#why-a-single-source-of-truth) within GitLab documentation.
We include guidance for links in the following categories:
- How to set up [anchor links](#anchor-links) for headings.
- How to set up [criteria](#basic-link-criteria) for configuring a link.
- What to set up when [linking to a `help`](../documentation/index.md#linking-to-help) page.
- How to set up [links to internal documentation](#links-to-internal-documentation) for cross-references.
- When to use [links requiring permissions](#links-requiring-permissions).
- How to set up a [link to a video](#link-to-video).
- How to [include links with version text](#text-for-documentation-requiring-version-text).
- How to [link to specific lines of code](#link-to-specific-lines-of-code)
### Basic link criteria
- Use inline link Markdown markup `[Text](https://example.com)`. - Use inline link Markdown markup `[Text](https://example.com)`.
It's easier to read, review, and maintain. **Do not** use `[Text][identifier]`. It's easier to read, review, and maintain. **Do not** use `[Text][identifier]`.
...@@ -688,6 +702,19 @@ Example: ...@@ -688,6 +702,19 @@ Example:
For more information, see the [confidential issue](../../user/project/issues/confidential_issues.md) `https://gitlab.com/gitlab-org/gitlab-foss/issues/<issue_number>`. For more information, see the [confidential issue](../../user/project/issues/confidential_issues.md) `https://gitlab.com/gitlab-org/gitlab-foss/issues/<issue_number>`.
``` ```
### Link to specific lines of code
When linking to specifics lines within a file, link to a commit instead of to the branch.
Lines of code change through time, therefore, linking to a line by using the commit link
ensures the user lands on the line you're referring to.
- **Do:** `[link to line 3](https://gitlab.com/gitlab-org/gitlab/-/blob/11f17c56d8b7f0b752562d78a4298a3a95b5ce66/.gitlab/issue_templates/Feature%20proposal.md#L3)`
- **Don't:** `[link to line 3](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20proposal.md#L3).`
If that linked expression is no longer in that line of the file due to further commits, you
can still search the file for that query. In this case, update the document to ensure it
links to the most recent version of the file.
## Navigation ## Navigation
To indicate the steps of navigation through the UI: To indicate the steps of navigation through the UI:
...@@ -1361,7 +1388,7 @@ on this document. Further explanation is given below. ...@@ -1361,7 +1388,7 @@ on this document. Further explanation is given below.
- Every method must have the REST API request. For example: - Every method must have the REST API request. For example:
``` ```plaintext
GET /projects/:id/repository/branches GET /projects/:id/repository/branches
``` ```
......
...@@ -122,13 +122,15 @@ If using GitLab 12.9 and newer, run: ...@@ -122,13 +122,15 @@ If using GitLab 12.9 and newer, run:
sudo gitlab-rails runner -e production 'puts Gitlab::BackgroundMigration.remaining' sudo gitlab-rails runner -e production 'puts Gitlab::BackgroundMigration.remaining'
``` ```
If using GitLab 12.8 and older, run the following using a Rails console: If using GitLab 12.8 and older, run the following using a [Rails console](../administration/troubleshooting/debug.md#starting-a-rails-console):
```ruby ```ruby
puts Sidekiq::Queue.new("background_migration").size puts Sidekiq::Queue.new("background_migration").size
Sidekiq::ScheduledSet.new.select { |r| r.klass == 'BackgroundMigrationWorker' }.size Sidekiq::ScheduledSet.new.select { |r| r.klass == 'BackgroundMigrationWorker' }.size
``` ```
---
**For installations from source** **For installations from source**
If using GitLab 12.9 and newer, run: If using GitLab 12.9 and newer, run:
...@@ -138,13 +140,16 @@ cd /home/git/gitlab ...@@ -138,13 +140,16 @@ cd /home/git/gitlab
sudo -u git -H bundle exec rails runner -e production 'puts Gitlab::BackgroundMigration.remaining' sudo -u git -H bundle exec rails runner -e production 'puts Gitlab::BackgroundMigration.remaining'
``` ```
If using GitLab 12.8 and older, run the following using a Rails console: If using GitLab 12.8 and older, run the following using a [Rails console](../administration/troubleshooting/debug.md#starting-a-rails-console):
```ruby ```ruby
puts Sidekiq::Queue.new("background_migration").size puts Sidekiq::Queue.new("background_migration").size
Sidekiq::ScheduledSet.new.select { |r| r.klass == 'BackgroundMigrationWorker' }.size Sidekiq::ScheduledSet.new.select { |r| r.klass == 'BackgroundMigrationWorker' }.size
``` ```
There is also a [rake task](../administration/raketasks/maintenance.md#display-status-of-database-migrations)
for displaying the status of each database migration.
## Upgrading to a new major version ## Upgrading to a new major version
Major versions are reserved for backwards incompatible changes. We recommend that Major versions are reserved for backwards incompatible changes. We recommend that
......
...@@ -10,10 +10,13 @@ to perform various actions. ...@@ -10,10 +10,13 @@ to perform various actions.
All statistics are opt-out. You can enable/disable them in the All statistics are opt-out. You can enable/disable them in the
**Admin Area > Settings > Metrics and profiling** section **Usage statistics**. **Admin Area > Settings > Metrics and profiling** section **Usage statistics**.
NOTE: **Note:** ## Network configuration
Allow network traffic from your GitLab instance to IP address `104.196.17.203:443`, to send Allow network traffic from your GitLab instance to IP address `104.196.17.203:443`, to send
usage statistics to GitLab Inc. usage statistics to GitLab Inc.
If your GitLab instance is behind a proxy, set the appropriate [proxy configuration variables](https://docs.gitlab.com/omnibus/settings/environment-variables.html).
## Version Check **(CORE ONLY)** ## Version Check **(CORE ONLY)**
If enabled, version check will inform you if a new version is available and the If enabled, version check will inform you if a new version is available and the
......
...@@ -506,9 +506,11 @@ To use SAST in an offline environment, you need: ...@@ -506,9 +506,11 @@ To use SAST in an offline environment, you need:
NOTE: **Note:** NOTE: **Note:**
GitLab Runner has a [default `pull policy` of `always`](https://docs.gitlab.com/runner/executors/docker.html#using-the-always-pull-policy), GitLab Runner has a [default `pull policy` of `always`](https://docs.gitlab.com/runner/executors/docker.html#using-the-always-pull-policy),
meaning the runner may try to pull remote images even if a local copy is available. Set GitLab meaning the runner will try to pull Docker images from the GitLab container registry even if a local
Runner's [`pull_policy` to `if-not-present`](https://docs.gitlab.com/runner/executors/docker.html#using-the-if-not-present-pull-policy) copy is available. GitLab Runner's [`pull_policy` can be set to `if-not-present`](https://docs.gitlab.com/runner/executors/docker.html#using-the-if-not-present-pull-policy)
in an offline environment if you prefer using only locally available Docker images. in an offline environment if you prefer using only locally available Docker images. However, we
recommend keeping the pull policy setting to `always` as it will better enable updated scanners to
be utilized within your CI/CD pipelines.
### Make GitLab SAST analyzer images available inside your Docker registry ### Make GitLab SAST analyzer images available inside your Docker registry
......
...@@ -14,7 +14,7 @@ module Gitlab ...@@ -14,7 +14,7 @@ module Gitlab
ALLOWED_KEYS = ALLOWED_KEYS =
%i[junit codequality sast dependency_scanning container_scanning %i[junit codequality sast dependency_scanning container_scanning
dast performance license_management license_scanning metrics lsif dast performance license_management license_scanning metrics lsif
dotenv cobertura].freeze dotenv cobertura terraform].freeze
attributes ALLOWED_KEYS attributes ALLOWED_KEYS
...@@ -36,6 +36,7 @@ module Gitlab ...@@ -36,6 +36,7 @@ module Gitlab
validates :lsif, array_of_strings_or_string: true validates :lsif, array_of_strings_or_string: true
validates :dotenv, array_of_strings_or_string: true validates :dotenv, array_of_strings_or_string: true
validates :cobertura, array_of_strings_or_string: true validates :cobertura, array_of_strings_or_string: true
validates :terraform, array_of_strings_or_string: true
end end
end end
......
...@@ -5,30 +5,11 @@ module Gitlab ...@@ -5,30 +5,11 @@ module Gitlab
module GrapeLogging module GrapeLogging
module Loggers module Loggers
class PerfLogger < ::GrapeLogging::Loggers::Base class PerfLogger < ::GrapeLogging::Loggers::Base
def parameters(_, _) include ::Gitlab::InstrumentationHelper
gitaly_data.merge(rugged_data)
end
def gitaly_data
gitaly_calls = Gitlab::GitalyClient.get_request_count
return {} if gitaly_calls.zero? def parameters(_, _)
payload = {}
{ payload.tap { add_instrumentation_data(payload) }
gitaly_calls: Gitlab::GitalyClient.get_request_count,
gitaly_duration: Gitlab::GitalyClient.query_time_ms
}
end
def rugged_data
rugged_calls = Gitlab::RuggedInstrumentation.query_count
return {} if rugged_calls.zero?
{
rugged_calls: rugged_calls,
rugged_duration_ms: Gitlab::RuggedInstrumentation.query_time_ms
}
end end
end end
end end
......
# frozen_string_literal: true
require 'redis'
module Gitlab
module Instrumentation
module RedisInterceptor
def call(*args, &block)
start = Time.now
super(*args, &block)
ensure
duration = (Time.now - start)
if ::RequestStore.active?
::Gitlab::Instrumentation::Redis.increment_request_count
::Gitlab::Instrumentation::Redis.add_duration(duration)
::Gitlab::Instrumentation::Redis.add_call_details(duration, args)
end
end
end
class Redis
REDIS_REQUEST_COUNT = :redis_request_count
REDIS_CALL_DURATION = :redis_call_duration
REDIS_CALL_DETAILS = :redis_call_details
def self.get_request_count
::RequestStore[REDIS_REQUEST_COUNT] || 0
end
def self.increment_request_count
::RequestStore[REDIS_REQUEST_COUNT] ||= 0
::RequestStore[REDIS_REQUEST_COUNT] += 1
end
def self.detail_store
::RequestStore[REDIS_CALL_DETAILS] ||= []
end
def self.query_time_ms
(self.query_time * 1000).round(2)
end
def self.query_time
::RequestStore[REDIS_CALL_DURATION] || 0
end
def self.add_duration(duration)
total_time = query_time + duration
::RequestStore[REDIS_CALL_DURATION] = total_time
end
def self.add_call_details(duration, args)
return unless Gitlab::PerformanceBar.enabled_for_request?
# redis-rb passes an array (e.g. [:get, key])
return unless args.length == 1
detail_store << {
cmd: args.first,
duration: duration,
backtrace: ::Gitlab::BacktraceCleaner.clean_backtrace(caller)
}
end
end
end
end
class ::Redis::Client
prepend ::Gitlab::Instrumentation::RedisInterceptor
end
...@@ -4,7 +4,7 @@ module Gitlab ...@@ -4,7 +4,7 @@ module Gitlab
module InstrumentationHelper module InstrumentationHelper
extend self extend self
KEYS = %i(gitaly_calls gitaly_duration rugged_calls rugged_duration_ms).freeze KEYS = %i(gitaly_calls gitaly_duration rugged_calls rugged_duration_ms redis_calls redis_duration_ms).freeze
def add_instrumentation_data(payload) def add_instrumentation_data(payload)
gitaly_calls = Gitlab::GitalyClient.get_request_count gitaly_calls = Gitlab::GitalyClient.get_request_count
...@@ -20,6 +20,13 @@ module Gitlab ...@@ -20,6 +20,13 @@ module Gitlab
payload[:rugged_calls] = rugged_calls payload[:rugged_calls] = rugged_calls
payload[:rugged_duration_ms] = Gitlab::RuggedInstrumentation.query_time_ms payload[:rugged_duration_ms] = Gitlab::RuggedInstrumentation.query_time_ms
end end
redis_calls = Gitlab::Instrumentation::Redis.get_request_count
if redis_calls > 0
payload[:redis_calls] = redis_calls
payload[:redis_duration_ms] = Gitlab::Instrumentation::Redis.query_time_ms
end
end end
# Returns the queuing duration for a Sidekiq job in seconds, as a float, if the # Returns the queuing duration for a Sidekiq job in seconds, as a float, if the
......
...@@ -17,7 +17,7 @@ module Peek ...@@ -17,7 +17,7 @@ module Peek
end end
def detail_store def detail_store
::Gitlab::SafeRequestStore["#{key}_call_details"] ||= [] ::Gitlab::SafeRequestStore["#{key}_call_details".to_sym] ||= []
end end
private private
......
# frozen_string_literal: true # frozen_string_literal: true
require 'redis'
module Gitlab
module Peek
module RedisInstrumented
def call(*args, &block)
start = Time.now
super(*args, &block)
ensure
duration = (Time.now - start)
add_call_details(duration, args)
end
private
def add_call_details(duration, args)
return unless Gitlab::PerformanceBar.enabled_for_request?
# redis-rb passes an array (e.g. [:get, key])
return unless args.length == 1
detail_store << {
cmd: args.first,
duration: duration,
backtrace: ::Gitlab::BacktraceCleaner.clean_backtrace(caller)
}
end
def detail_store
::Gitlab::SafeRequestStore['redis_call_details'] ||= []
end
end
end
end
module Peek module Peek
module Views module Views
class RedisDetailed < DetailedView class RedisDetailed < DetailedView
...@@ -63,7 +29,3 @@ module Peek ...@@ -63,7 +29,3 @@ module Peek
end end
end end
end end
class Redis::Client
prepend Gitlab::Peek::RedisInstrumented
end
...@@ -220,9 +220,6 @@ msgstr "" ...@@ -220,9 +220,6 @@ msgstr ""
msgid "%{authorsName}'s thread" msgid "%{authorsName}'s thread"
msgstr "" msgstr ""
msgid "%{buy_now_link_start}Buy now!%{link_end}"
msgstr ""
msgid "%{commit_author_link} authored %{commit_timeago}" msgid "%{commit_author_link} authored %{commit_timeago}"
msgstr "" msgstr ""
...@@ -446,6 +443,9 @@ msgstr "" ...@@ -446,6 +443,9 @@ msgstr ""
msgid "%{state} epics" msgid "%{state} epics"
msgstr "" msgstr ""
msgid "%{strongStart}Note:%{strongEnd} Once a custom stage has been added you can re-order stages by dragging them into the desired position."
msgstr ""
msgid "%{strong_start}%{branch_count}%{strong_end} Branch" msgid "%{strong_start}%{branch_count}%{strong_end} Branch"
msgid_plural "%{strong_start}%{branch_count}%{strong_end} Branches" msgid_plural "%{strong_start}%{branch_count}%{strong_end} Branches"
msgstr[0] "" msgstr[0] ""
...@@ -584,10 +584,12 @@ msgstr "" ...@@ -584,10 +584,12 @@ msgstr ""
msgid "+ %{numberOfHiddenAssignees} more" msgid "+ %{numberOfHiddenAssignees} more"
msgstr "" msgstr ""
msgid "+%{approvers} more approvers" msgid "+%d more"
msgstr "" msgid_plural "+%d more"
msgstr[0] ""
msgstr[1] ""
msgid "+%{extraOptionCount} more" msgid "+%{approvers} more approvers"
msgstr "" msgstr ""
msgid "+%{tags} more" msgid "+%{tags} more"
...@@ -2427,12 +2429,6 @@ msgstr "" ...@@ -2427,12 +2429,6 @@ msgstr ""
msgid "Ascending" msgid "Ascending"
msgstr "" msgstr ""
msgid "Ask an admin to upload a new license to ensure uninterrupted service."
msgstr ""
msgid "Ask an admin to upload a new license to restore service."
msgstr ""
msgid "Ask your group maintainer to set up a group Runner." msgid "Ask your group maintainer to set up a group Runner."
msgstr "" msgstr ""
...@@ -7825,6 +7821,9 @@ msgstr "" ...@@ -7825,6 +7821,9 @@ msgstr ""
msgid "Environments|Select environment" msgid "Environments|Select environment"
msgstr "" msgstr ""
msgid "Environments|Select pod"
msgstr ""
msgid "Environments|Show all" msgid "Environments|Show all"
msgstr "" msgstr ""
...@@ -8983,9 +8982,6 @@ msgstr "" ...@@ -8983,9 +8982,6 @@ msgstr ""
msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)" msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
msgstr "" msgstr ""
msgid "For renewal instructions %{link_start}view our Licensing FAQ.%{link_end}"
msgstr ""
msgid "Forgot your password?" msgid "Forgot your password?"
msgstr "" msgstr ""
...@@ -13412,6 +13408,9 @@ msgstr "" ...@@ -13412,6 +13408,9 @@ msgstr ""
msgid "No webhooks found, add one in the form above." msgid "No webhooks found, add one in the form above."
msgstr "" msgstr ""
msgid "No worries, you can still use all the %{strong}%{plan_name}%{strong_close} features for now. You have %{remaining_days} to renew your subscription."
msgstr ""
msgid "No, directly import the existing email addresses and usernames." msgid "No, directly import the existing email addresses and usernames."
msgstr "" msgstr ""
...@@ -16210,12 +16209,6 @@ msgstr "" ...@@ -16210,12 +16209,6 @@ msgstr ""
msgid "Pushes" msgid "Pushes"
msgstr "" msgstr ""
msgid "Pushing code and creation of issues and merge requests has been disabled."
msgstr ""
msgid "Pushing code and creation of issues and merge requests will be disabled on %{disabled_on}."
msgstr ""
msgid "PushoverService|%{user_name} deleted branch \"%{ref}\"." msgid "PushoverService|%{user_name} deleted branch \"%{ref}\"."
msgstr "" msgstr ""
...@@ -18227,6 +18220,9 @@ msgid_plural "Showing %d events" ...@@ -18227,6 +18220,9 @@ msgid_plural "Showing %d events"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "Showing %{limit} of %{total_count} issues. "
msgstr ""
msgid "Showing %{pageSize} of %{total} issues" msgid "Showing %{pageSize} of %{total} issues"
msgstr "" msgstr ""
...@@ -20177,6 +20173,9 @@ msgstr "" ...@@ -20177,6 +20173,9 @@ msgstr ""
msgid "There was an error updating the dashboard, branch named: %{branch} already exists." msgid "There was an error updating the dashboard, branch named: %{branch} already exists."
msgstr "" msgstr ""
msgid "There was an error updating the stage order. Please try reloading the page."
msgstr ""
msgid "There was an error when reseting email token." msgid "There was an error when reseting email token."
msgstr "" msgstr ""
...@@ -21654,12 +21653,6 @@ msgstr "" ...@@ -21654,12 +21653,6 @@ msgstr ""
msgid "Upload a certificate for your domain with all intermediates" msgid "Upload a certificate for your domain with all intermediates"
msgstr "" msgstr ""
msgid "Upload a new license in the admin area to ensure uninterrupted service."
msgstr ""
msgid "Upload a new license in the admin area to restore service."
msgstr ""
msgid "Upload a private key for your certificate" msgid "Upload a private key for your certificate"
msgstr "" msgstr ""
...@@ -22236,6 +22229,9 @@ msgstr "" ...@@ -22236,6 +22229,9 @@ msgstr ""
msgid "View Documentation" msgid "View Documentation"
msgstr "" msgstr ""
msgid "View all issues"
msgstr ""
msgid "View blame prior to this change" msgid "View blame prior to this change"
msgstr "" msgstr ""
...@@ -23053,6 +23049,9 @@ msgstr "" ...@@ -23053,6 +23049,9 @@ msgstr ""
msgid "You could not create a new trigger." msgid "You could not create a new trigger."
msgstr "" msgstr ""
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
msgid "You do not have any subscriptions yet" msgid "You do not have any subscriptions yet"
msgstr "" msgstr ""
...@@ -23272,6 +23271,9 @@ msgstr "" ...@@ -23272,6 +23271,9 @@ msgstr ""
msgid "YouTube" msgid "YouTube"
msgstr "" msgstr ""
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
msgid "Your Commit Email will be used for web based operations, such as edits and merges." msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr "" msgstr ""
...@@ -23392,15 +23394,9 @@ msgstr "" ...@@ -23392,15 +23394,9 @@ msgstr ""
msgid "Your issues will be imported in the background. Once finished, you'll get a confirmation email." msgid "Your issues will be imported in the background. Once finished, you'll get a confirmation email."
msgstr "" msgstr ""
msgid "Your license expired on %{expires_at}."
msgstr ""
msgid "Your license is valid from" msgid "Your license is valid from"
msgstr "" msgstr ""
msgid "Your license will expire in %{remaining_days}."
msgstr ""
msgid "Your message here" msgid "Your message here"
msgstr "" msgstr ""
...@@ -23431,10 +23427,13 @@ msgstr "" ...@@ -23431,10 +23427,13 @@ msgstr ""
msgid "Your request for access has been queued for review." msgid "Your request for access has been queued for review."
msgstr "" msgstr ""
msgid "Your trial license expired on %{expires_at}." msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr "" msgstr ""
msgid "Your trial license will expire in %{remaining_days}." msgid "Your subscription will expire in %{remaining_days}"
msgstr "" msgstr ""
msgid "Zoom meeting added" msgid "Zoom meeting added"
......
...@@ -39,6 +39,7 @@ module QA ...@@ -39,6 +39,7 @@ module QA
autoload :MailHog, 'qa/runtime/mail_hog' autoload :MailHog, 'qa/runtime/mail_hog'
autoload :IPAddress, 'qa/runtime/ip_address' autoload :IPAddress, 'qa/runtime/ip_address'
autoload :Search, 'qa/runtime/search' autoload :Search, 'qa/runtime/search'
autoload :Project, 'qa/runtime/project'
autoload :ApplicationSettings, 'qa/runtime/application_settings' autoload :ApplicationSettings, 'qa/runtime/application_settings'
module API module API
......
# frozen_string_literal: true
module QA
module Runtime
module Project
extend self
extend Support::Api
def create_project(project_name, api_client, project_description = 'default')
project = Resource::Project.fabricate_via_api! do |project|
project.add_name_uuid = false
project.name = project_name
project.description = project_description
project.api_client = api_client
project.visibility = 'public'
end
project
end
def push_file_to_project(target_project, file_name, file_content)
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = target_project
push.file_name = file_name
push.file_content = file_content
end
end
def set_project_visibility(api_client, project_id, visibility)
request = Runtime::API::Request.new(api_client, "/projects/#{project_id}")
response = put request.url, visibility: visibility
response.code.equal?(QA::Support::Api::HTTP_STATUS_OK)
end
end
end
end
...@@ -42,6 +42,22 @@ module QA ...@@ -42,6 +42,22 @@ module QA
end end
end end
def elasticsearch_on?(api_client)
elasticsearch_state_request = Runtime::API::Request.new(api_client, '/application/settings')
response = get elasticsearch_state_request.url
parse_body(response)[:elasticsearch_search] && parse_body(response)[:elasticsearch_indexing]
end
def disable_elasticsearch(api_client)
disable_elasticsearch_request = Runtime::API::Request.new(api_client, '/application/settings')
put disable_elasticsearch_request.url, elasticsearch_search: false, elasticsearch_indexing: false
end
def create_search_request(api_client, scope, search_term)
Runtime::API::Request.new(api_client, '/search', scope: scope, search: search_term)
end
def find_code(file_name, search_term) def find_code(file_name, search_term)
find_target_in_scope('blobs', search_term) do |record| find_target_in_scope('blobs', search_term) do |record|
record[:filename] == file_name && record[:data].include?(search_term) record[:filename] == file_name && record[:data].include?(search_term)
......
...@@ -10,13 +10,8 @@ describe Admin::ServicesController do ...@@ -10,13 +10,8 @@ describe Admin::ServicesController do
end end
describe 'GET #edit' do describe 'GET #edit' do
let!(:project) { create(:project) }
Service.available_services_names.each do |service_name|
context "#{service_name}" do
let!(:service) do let!(:service) do
service_template = "#{service_name}_service".camelize.constantize create(:jira_service, :template)
service_template.where(template: true).first_or_create
end end
it 'successfully displays the template' do it 'successfully displays the template' do
...@@ -25,8 +20,6 @@ describe Admin::ServicesController do ...@@ -25,8 +20,6 @@ describe Admin::ServicesController do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end end
end end
end
end
describe "#update" do describe "#update" do
let(:project) { create(:project) } let(:project) { create(:project) }
......
...@@ -8,18 +8,17 @@ describe Projects::BlobController do ...@@ -8,18 +8,17 @@ describe Projects::BlobController do
let(:project) { create(:project, :public, :repository) } let(:project) { create(:project, :public, :repository) }
describe "GET show" do describe "GET show" do
def request
get(:show, params: { namespace_id: project.namespace, project_id: project, id: id })
end
render_views render_views
context 'with file path' do context 'with file path' do
before do before do
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
get(:show, request
params: {
namespace_id: project.namespace,
project_id: project,
id: id
})
end end
context "valid branch, valid file" do context "valid branch, valid file" do
...@@ -119,6 +118,32 @@ describe Projects::BlobController do ...@@ -119,6 +118,32 @@ describe Projects::BlobController do
end end
end end
end end
context 'when there is an artifact with code navigation data' do
let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id) }
let!(:job) { create(:ci_build, pipeline: pipeline, name: Ci::Build::CODE_NAVIGATION_JOB_NAME) }
let!(:artifact) { create(:ci_job_artifact, :lsif, job: job) }
let(:id) { 'master/README.md' }
it 'assigns code_navigation_build variable' do
request
expect(assigns[:code_navigation_build]).to eq(job)
end
context 'when code_navigation feature is disabled' do
before do
stub_feature_flags(code_navigation: false)
end
it 'does not assign code_navigation_build variable' do
request
expect(assigns[:code_navigation_build]).to be_nil
end
end
end
end end
describe 'GET diff' do describe 'GET diff' do
......
...@@ -25,6 +25,37 @@ describe "User views milestone" do ...@@ -25,6 +25,37 @@ describe "User views milestone" do
expect { visit_milestone }.not_to exceed_query_limit(control) expect { visit_milestone }.not_to exceed_query_limit(control)
end end
context 'limiting milestone issues' do
before_all do
2.times do
create(:issue, milestone: milestone, project: project)
create(:issue, milestone: milestone, project: project, assignees: [user])
create(:issue, milestone: milestone, project: project, state: :closed)
end
end
context 'when issues on milestone are over DISPLAY_ISSUES_LIMIT' do
it "limits issues to display and shows warning" do
stub_const('Milestoneish::DISPLAY_ISSUES_LIMIT', 3)
visit(project_milestone_path(project, milestone))
expect(page).to have_selector('.issuable-row', count: 3)
expect(page).to have_selector('#milestone-issue-count-warning', text: 'Showing 3 of 6 issues. View all issues')
expect(page).to have_link('View all issues', href: project_issues_path(project, { milestone_title: milestone.title }))
end
end
context 'when issues on milestone are below DISPLAY_ISSUES_LIMIT' do
it 'does not display warning' do
visit(project_milestone_path(project, milestone))
expect(page).not_to have_selector('#milestone-issue-count-warning', text: 'Showing 3 of 6 issues. View all issues')
expect(page).to have_selector('.issuable-row', count: 6)
end
end
end
private private
def visit_milestone def visit_milestone
......
...@@ -29,7 +29,7 @@ describe 'Environment > Pod Logs', :js do ...@@ -29,7 +29,7 @@ describe 'Environment > Pod Logs', :js do
wait_for_requests wait_for_requests
page.within('.js-environments-dropdown') do page.within('.js-environments-dropdown') do
toggle = find(".dropdown-menu-toggle:not([disabled])") toggle = find(".dropdown-toggle:not([disabled])")
expect(toggle).to have_content(environment.name) expect(toggle).to have_content(environment.name)
...@@ -47,8 +47,8 @@ describe 'Environment > Pod Logs', :js do ...@@ -47,8 +47,8 @@ describe 'Environment > Pod Logs', :js do
wait_for_requests wait_for_requests
page.within('.js-pods-dropdown') do page.within('.qa-pods-dropdown') do
find(".dropdown-menu-toggle:not([disabled])").click find(".dropdown-toggle:not([disabled])").click
dropdown_items = find(".dropdown-menu").all(".dropdown-item:not([disabled])") dropdown_items = find(".dropdown-menu").all(".dropdown-item:not([disabled])")
expect(dropdown_items.size).to eq(1) expect(dropdown_items.size).to eq(1)
......
...@@ -16,6 +16,7 @@ function factory(initialState = {}) { ...@@ -16,6 +16,7 @@ function factory(initialState = {}) {
state: { state: {
...createState(), ...createState(),
...initialState, ...initialState,
definitionPathPrefix: 'https://test.com/blob/master',
}, },
actions: { actions: {
fetchData, fetchData,
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Popover from '~/code_navigation/components/popover.vue'; import Popover from '~/code_navigation/components/popover.vue';
const DEFINITION_PATH_PREFIX = 'http:/';
const MOCK_CODE_DATA = Object.freeze({ const MOCK_CODE_DATA = Object.freeze({
hover: [ hover: [
{ {
...@@ -8,7 +10,7 @@ const MOCK_CODE_DATA = Object.freeze({ ...@@ -8,7 +10,7 @@ const MOCK_CODE_DATA = Object.freeze({
value: 'console.log', value: 'console.log',
}, },
], ],
definition_url: 'http://test.com', definition_path: 'test.com',
}); });
const MOCK_DOCS_DATA = Object.freeze({ const MOCK_DOCS_DATA = Object.freeze({
...@@ -18,13 +20,13 @@ const MOCK_DOCS_DATA = Object.freeze({ ...@@ -18,13 +20,13 @@ const MOCK_DOCS_DATA = Object.freeze({
value: 'console.log', value: 'console.log',
}, },
], ],
definition_url: 'http://test.com', definition_path: 'test.com',
}); });
let wrapper; let wrapper;
function factory(position, data) { function factory(position, data, definitionPathPrefix) {
wrapper = shallowMount(Popover, { propsData: { position, data } }); wrapper = shallowMount(Popover, { propsData: { position, data, definitionPathPrefix } });
} }
describe('Code navigation popover component', () => { describe('Code navigation popover component', () => {
...@@ -33,14 +35,14 @@ describe('Code navigation popover component', () => { ...@@ -33,14 +35,14 @@ describe('Code navigation popover component', () => {
}); });
it('renders popover', () => { it('renders popover', () => {
factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA); factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA, DEFINITION_PATH_PREFIX);
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
}); });
describe('code output', () => { describe('code output', () => {
it('renders code output', () => { it('renders code output', () => {
factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA); factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA, DEFINITION_PATH_PREFIX);
expect(wrapper.find({ ref: 'code-output' }).exists()).toBe(true); expect(wrapper.find({ ref: 'code-output' }).exists()).toBe(true);
expect(wrapper.find({ ref: 'doc-output' }).exists()).toBe(false); expect(wrapper.find({ ref: 'doc-output' }).exists()).toBe(false);
...@@ -49,7 +51,7 @@ describe('Code navigation popover component', () => { ...@@ -49,7 +51,7 @@ describe('Code navigation popover component', () => {
describe('documentation output', () => { describe('documentation output', () => {
it('renders code output', () => { it('renders code output', () => {
factory({ x: 0, y: 0, height: 0 }, MOCK_DOCS_DATA); factory({ x: 0, y: 0, height: 0 }, MOCK_DOCS_DATA, DEFINITION_PATH_PREFIX);
expect(wrapper.find({ ref: 'code-output' }).exists()).toBe(false); expect(wrapper.find({ ref: 'code-output' }).exists()).toBe(false);
expect(wrapper.find({ ref: 'doc-output' }).exists()).toBe(true); expect(wrapper.find({ ref: 'doc-output' }).exists()).toBe(true);
......
...@@ -27,12 +27,10 @@ describe('Code navigation actions', () => { ...@@ -27,12 +27,10 @@ describe('Code navigation actions', () => {
describe('fetchData', () => { describe('fetchData', () => {
let mock; let mock;
const state = {
projectPath: 'gitlab-org/gitlab', const codeNavUrl =
commitId: '123', 'gitlab-org/gitlab-shell/-/jobs/1114/artifacts/raw/lsif/cmd/check/main.go.json';
blobPath: 'index', const state = { codeNavUrl };
};
const apiUrl = '/api/1/projects/gitlab-org%2Fgitlab/commits/123/lsif/info';
beforeEach(() => { beforeEach(() => {
window.gon = { api_version: '1' }; window.gon = { api_version: '1' };
...@@ -45,8 +43,7 @@ describe('Code navigation actions', () => { ...@@ -45,8 +43,7 @@ describe('Code navigation actions', () => {
describe('success', () => { describe('success', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(apiUrl).replyOnce(200, { mock.onGet(codeNavUrl).replyOnce(200, [
index: [
{ {
start_line: 0, start_line: 0,
start_char: 0, start_char: 0,
...@@ -57,8 +54,7 @@ describe('Code navigation actions', () => { ...@@ -57,8 +54,7 @@ describe('Code navigation actions', () => {
start_char: 0, start_char: 0,
hover: null, hover: null,
}, },
], ]);
});
}); });
it('commits REQUEST_DATA_SUCCESS with normalized data', done => { it('commits REQUEST_DATA_SUCCESS with normalized data', done => {
...@@ -106,7 +102,7 @@ describe('Code navigation actions', () => { ...@@ -106,7 +102,7 @@ describe('Code navigation actions', () => {
describe('error', () => { describe('error', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(apiUrl).replyOnce(500); mock.onGet(codeNavUrl).replyOnce(500);
}); });
it('dispatches requestDataError', done => { it('dispatches requestDataError', done => {
......
...@@ -11,14 +11,12 @@ describe('Code navigation mutations', () => { ...@@ -11,14 +11,12 @@ describe('Code navigation mutations', () => {
describe('SET_INITIAL_DATA', () => { describe('SET_INITIAL_DATA', () => {
it('sets initial data', () => { it('sets initial data', () => {
mutations.SET_INITIAL_DATA(state, { mutations.SET_INITIAL_DATA(state, {
projectPath: 'test', codeNavUrl: 'https://test.com/builds/1005',
commitId: '123', definitionPathPrefix: 'https://test.com/blob/master',
blobPath: 'index.js',
}); });
expect(state.projectPath).toBe('test'); expect(state.codeNavUrl).toBe('https://test.com/builds/1005');
expect(state.commitId).toBe('123'); expect(state.definitionPathPrefix).toBe('https://test.com/blob/master');
expect(state.blobPath).toBe('index.js');
}); });
}); });
......
...@@ -9,6 +9,8 @@ import { branches } from '../../mock_data'; ...@@ -9,6 +9,8 @@ import { branches } from '../../mock_data';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
jest.mock('lodash/debounce', () => jest.fn);
describe('IDE branches search list', () => { describe('IDE branches search list', () => {
let wrapper; let wrapper;
const fetchBranchesMock = jest.fn(); const fetchBranchesMock = jest.fn();
......
import Vue from 'vue'; import { GlSprintf, GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { GlSprintf, GlIcon, GlDropdown, GlDropdownItem, GlSearchBoxByClick } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import EnvironmentLogs from '~/logs/components/environment_logs.vue'; import EnvironmentLogs from '~/logs/components/environment_logs.vue';
import { createStore } from '~/logs/stores'; import { createStore } from '~/logs/stores';
...@@ -13,7 +11,6 @@ import { ...@@ -13,7 +11,6 @@ import {
mockLogsResult, mockLogsResult,
mockTrace, mockTrace,
mockPodName, mockPodName,
mockSearch,
mockEnvironmentsEndpoint, mockEnvironmentsEndpoint,
mockDocumentationPath, mockDocumentationPath,
} from '../mock_data'; } from '../mock_data';
...@@ -29,7 +26,6 @@ jest.mock('lodash/throttle', () => ...@@ -29,7 +26,6 @@ jest.mock('lodash/throttle', () =>
); );
describe('EnvironmentLogs', () => { describe('EnvironmentLogs', () => {
let EnvironmentLogsComponent;
let store; let store;
let dispatch; let dispatch;
let wrapper; let wrapper;
...@@ -44,13 +40,9 @@ describe('EnvironmentLogs', () => { ...@@ -44,13 +40,9 @@ describe('EnvironmentLogs', () => {
const updateControlBtnsMock = jest.fn(); const updateControlBtnsMock = jest.fn();
const findEnvironmentsDropdown = () => wrapper.find('.js-environments-dropdown'); const findEnvironmentsDropdown = () => wrapper.find('.js-environments-dropdown');
const findPodsDropdown = () => wrapper.find('.js-pods-dropdown');
const findPodsDropdownItems = () => const findSimpleFilters = () => wrapper.find({ ref: 'log-simple-filters' });
findPodsDropdown() const findAdvancedFilters = () => wrapper.find({ ref: 'log-advanced-filters' });
.findAll(GlDropdownItem)
.filter(itm => !itm.attributes('disabled'));
const findSearchBar = () => wrapper.find('.js-logs-search');
const findTimeRangePicker = () => wrapper.find({ ref: 'dateTimePicker' });
const findInfoAlert = () => wrapper.find('.js-elasticsearch-alert'); const findInfoAlert = () => wrapper.find('.js-elasticsearch-alert');
const findLogControlButtons = () => wrapper.find({ name: 'log-control-buttons-stub' }); const findLogControlButtons = () => wrapper.find({ name: 'log-control-buttons-stub' });
...@@ -79,7 +71,7 @@ describe('EnvironmentLogs', () => { ...@@ -79,7 +71,7 @@ describe('EnvironmentLogs', () => {
}; };
const initWrapper = () => { const initWrapper = () => {
wrapper = shallowMount(EnvironmentLogsComponent, { wrapper = shallowMount(EnvironmentLogs, {
propsData, propsData,
store, store,
stubs: { stubs: {
...@@ -111,7 +103,6 @@ describe('EnvironmentLogs', () => { ...@@ -111,7 +103,6 @@ describe('EnvironmentLogs', () => {
beforeEach(() => { beforeEach(() => {
store = createStore(); store = createStore();
state = store.state.environmentLogs; state = store.state.environmentLogs;
EnvironmentLogsComponent = Vue.extend(EnvironmentLogs);
jest.spyOn(store, 'dispatch').mockResolvedValue(); jest.spyOn(store, 'dispatch').mockResolvedValue();
...@@ -132,17 +123,10 @@ describe('EnvironmentLogs', () => { ...@@ -132,17 +123,10 @@ describe('EnvironmentLogs', () => {
expect(wrapper.isVueInstance()).toBe(true); expect(wrapper.isVueInstance()).toBe(true);
expect(wrapper.isEmpty()).toBe(false); expect(wrapper.isEmpty()).toBe(false);
// top bar
expect(findEnvironmentsDropdown().is(GlDropdown)).toBe(true); expect(findEnvironmentsDropdown().is(GlDropdown)).toBe(true);
expect(findPodsDropdown().is(GlDropdown)).toBe(true); expect(findSimpleFilters().exists()).toBe(true);
expect(findLogControlButtons().exists()).toBe(true); expect(findLogControlButtons().exists()).toBe(true);
expect(findSearchBar().exists()).toBe(true);
expect(findSearchBar().is(GlSearchBoxByClick)).toBe(true);
expect(findTimeRangePicker().exists()).toBe(true);
expect(findTimeRangePicker().is(DateTimePicker)).toBe(true);
// log trace
expect(findInfiniteScroll().exists()).toBe(true); expect(findInfiniteScroll().exists()).toBe(true);
expect(findLogTrace().exists()).toBe(true); expect(findLogTrace().exists()).toBe(true);
}); });
...@@ -181,20 +165,6 @@ describe('EnvironmentLogs', () => { ...@@ -181,20 +165,6 @@ describe('EnvironmentLogs', () => {
expect(findEnvironmentsDropdown().findAll(GlDropdownItem).length).toBe(0); expect(findEnvironmentsDropdown().findAll(GlDropdownItem).length).toBe(0);
}); });
it('displays a disabled pods dropdown', () => {
expect(findPodsDropdown().attributes('disabled')).toBe('true');
expect(findPodsDropdownItems()).toHaveLength(0);
});
it('displays a disabled search bar', () => {
expect(findSearchBar().exists()).toBe(true);
expect(findSearchBar().attributes('disabled')).toBe('true');
});
it('displays a disabled time window dropdown', () => {
expect(findTimeRangePicker().attributes('disabled')).toBe('true');
});
it('does not update buttons state', () => { it('does not update buttons state', () => {
expect(updateControlBtnsMock).not.toHaveBeenCalled(); expect(updateControlBtnsMock).not.toHaveBeenCalled();
}); });
...@@ -237,17 +207,14 @@ describe('EnvironmentLogs', () => { ...@@ -237,17 +207,14 @@ describe('EnvironmentLogs', () => {
initWrapper(); initWrapper();
}); });
it('displays a disabled time window dropdown', () => {
expect(findTimeRangePicker().attributes('disabled')).toBe('true');
});
it('displays a disabled search bar', () => {
expect(findSearchBar().attributes('disabled')).toBe('true');
});
it('displays an alert to upgrade to ES', () => { it('displays an alert to upgrade to ES', () => {
expect(findInfoAlert().exists()).toBe(true); expect(findInfoAlert().exists()).toBe(true);
}); });
it('displays simple filters for kubernetes logs API', () => {
expect(findSimpleFilters().exists()).toBe(true);
expect(findAdvancedFilters().exists()).toBe(false);
});
}); });
describe('state with data', () => { describe('state with data', () => {
...@@ -271,21 +238,6 @@ describe('EnvironmentLogs', () => { ...@@ -271,21 +238,6 @@ describe('EnvironmentLogs', () => {
updateControlBtnsMock.mockReset(); updateControlBtnsMock.mockReset();
}); });
it('displays an enabled search bar', () => {
expect(findSearchBar().attributes('disabled')).toBeFalsy();
// input a query and click `search`
findSearchBar().vm.$emit('input', mockSearch);
findSearchBar().vm.$emit('submit');
expect(dispatch).toHaveBeenCalledWith(`${module}/setInitData`, expect.any(Object));
expect(dispatch).toHaveBeenCalledWith(`${module}/setSearch`, mockSearch);
});
it('displays an enabled time window dropdown', () => {
expect(findTimeRangePicker().attributes('disabled')).toBeFalsy();
});
it('does not display an alert to upgrade to ES', () => { it('does not display an alert to upgrade to ES', () => {
expect(findInfoAlert().exists()).toBe(false); expect(findInfoAlert().exists()).toBe(false);
}); });
...@@ -306,24 +258,16 @@ describe('EnvironmentLogs', () => { ...@@ -306,24 +258,16 @@ describe('EnvironmentLogs', () => {
const item = items.at(i); const item = items.at(i);
if (item.text() !== mockEnvName) { if (item.text() !== mockEnvName) {
expect(item.find(GlIcon).classes()).toContain('invisible'); expect(item.find(GlIcon).classes('invisible')).toBe(true);
} else { } else {
// selected expect(item.find(GlIcon).classes('invisible')).toBe(false);
expect(item.find(GlIcon).classes()).not.toContain('invisible');
} }
}); });
}); });
it('populates pods dropdown', () => { it('displays advanced filters for elasticsearch logs API', () => {
const items = findPodsDropdownItems(); expect(findSimpleFilters().exists()).toBe(false);
expect(findAdvancedFilters().exists()).toBe(true);
expect(findPodsDropdown().props('text')).toBe(mockPodName);
expect(items.length).toBe(mockPods.length + 1);
expect(items.at(0).text()).toBe('All pods');
mockPods.forEach((pod, i) => {
const item = items.at(i + 1);
expect(item.text()).toBe(pod);
});
}); });
it('shows infinite scroll with height and no content', () => { it('shows infinite scroll with height and no content', () => {
...@@ -331,19 +275,6 @@ describe('EnvironmentLogs', () => { ...@@ -331,19 +275,6 @@ describe('EnvironmentLogs', () => {
expect(getInfiniteScrollAttr('fetched-items')).toBe(mockTrace.length); expect(getInfiniteScrollAttr('fetched-items')).toBe(mockTrace.length);
}); });
it('dropdown has one pod selected', () => {
const items = findPodsDropdownItems();
mockPods.forEach((pod, i) => {
const item = items.at(i);
if (item.text() !== mockPodName) {
expect(item.find(GlIcon).classes()).toContain('invisible');
} else {
// selected
expect(item.find(GlIcon).classes()).not.toContain('invisible');
}
});
});
it('populates logs trace', () => { it('populates logs trace', () => {
const trace = findLogTrace(); const trace = findLogTrace();
expect(trace.text().split('\n').length).toBe(mockTrace.length); expect(trace.text().split('\n').length).toBe(mockTrace.length);
...@@ -371,17 +302,6 @@ describe('EnvironmentLogs', () => { ...@@ -371,17 +302,6 @@ describe('EnvironmentLogs', () => {
); );
}); });
it('pod name, trace is refreshed', () => {
const items = findPodsDropdownItems();
const index = 2; // any pod
expect(dispatch).not.toHaveBeenCalledWith(`${module}/showPodLogs`, expect.anything());
items.at(index + 1).vm.$emit('click');
expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, mockPods[index]);
});
it('refresh button, trace is refreshed', () => { it('refresh button, trace is refreshed', () => {
expect(dispatch).not.toHaveBeenCalledWith(`${module}/showPodLogs`, expect.anything()); expect(dispatch).not.toHaveBeenCalledWith(`${module}/showPodLogs`, expect.anything());
......
import { GlIcon, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { defaultTimeRange } from '~/vue_shared/constants';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { createStore } from '~/logs/stores';
import { mockPods, mockSearch } from '../mock_data';
import LogAdvancedFilters from '~/logs/components/log_advanced_filters.vue';
const module = 'environmentLogs';
describe('LogAdvancedFilters', () => {
let store;
let dispatch;
let wrapper;
let state;
const findPodsDropdown = () => wrapper.find({ ref: 'podsDropdown' });
const findPodsNoPodsText = () => wrapper.find({ ref: 'noPodsMsg' });
const findPodsDropdownItems = () =>
findPodsDropdown()
.findAll(GlDropdownItem)
.filter(item => !item.is('[disabled]'));
const findPodsDropdownItemsSelected = () =>
findPodsDropdownItems()
.filter(item => {
return !item.find(GlIcon).classes('invisible');
})
.at(0);
const findSearchBox = () => wrapper.find({ ref: 'searchBox' });
const findTimeRangePicker = () => wrapper.find({ ref: 'dateTimePicker' });
const mockStateLoading = () => {
state.timeRange.selected = defaultTimeRange;
state.timeRange.current = convertToFixedRange(defaultTimeRange);
state.pods.options = [];
state.pods.current = null;
};
const mockStateWithData = () => {
state.timeRange.selected = defaultTimeRange;
state.timeRange.current = convertToFixedRange(defaultTimeRange);
state.pods.options = mockPods;
state.pods.current = null;
};
const initWrapper = (propsData = {}) => {
wrapper = shallowMount(LogAdvancedFilters, {
propsData: {
...propsData,
},
store,
});
};
beforeEach(() => {
store = createStore();
state = store.state.environmentLogs;
jest.spyOn(store, 'dispatch').mockResolvedValue();
dispatch = store.dispatch;
});
afterEach(() => {
store.dispatch.mockReset();
if (wrapper) {
wrapper.destroy();
}
});
it('displays UI elements', () => {
initWrapper();
expect(wrapper.isVueInstance()).toBe(true);
expect(wrapper.isEmpty()).toBe(false);
expect(findPodsDropdown().exists()).toBe(true);
expect(findSearchBox().exists()).toBe(true);
expect(findTimeRangePicker().exists()).toBe(true);
});
describe('disabled state', () => {
beforeEach(() => {
mockStateLoading();
initWrapper({
disabled: true,
});
});
it('displays disabled filters', () => {
expect(findPodsDropdown().props('text')).toBe('All pods');
expect(findPodsDropdown().attributes('disabled')).toBeTruthy();
expect(findSearchBox().attributes('disabled')).toBeTruthy();
expect(findTimeRangePicker().attributes('disabled')).toBeTruthy();
});
});
describe('when the state is loading', () => {
beforeEach(() => {
mockStateLoading();
initWrapper();
});
it('displays a enabled filters', () => {
expect(findPodsDropdown().props('text')).toBe('All pods');
expect(findPodsDropdown().attributes('disabled')).toBeFalsy();
expect(findSearchBox().attributes('disabled')).toBeFalsy();
expect(findTimeRangePicker().attributes('disabled')).toBeFalsy();
});
it('displays an empty pods dropdown', () => {
expect(findPodsNoPodsText().exists()).toBe(true);
expect(findPodsDropdownItems()).toHaveLength(0);
});
});
describe('when the state has data', () => {
beforeEach(() => {
mockStateWithData();
initWrapper();
});
it('displays an enabled pods dropdown', () => {
expect(findPodsDropdown().attributes('disabled')).toBeFalsy();
expect(findPodsDropdown().props('text')).toBe('All pods');
});
it('displays options in a pods dropdown', () => {
const items = findPodsDropdownItems();
expect(items).toHaveLength(mockPods.length + 1);
});
it('displays "all pods" selected in a pods dropdown', () => {
const selected = findPodsDropdownItemsSelected();
expect(selected.text()).toBe('All pods');
});
it('displays options in date time picker', () => {
const options = findTimeRangePicker().props('options');
expect(options).toEqual(expect.any(Array));
expect(options.length).toBeGreaterThan(0);
});
describe('when the user interacts', () => {
it('clicks on a all options, showPodLogs is dispatched with null', () => {
const items = findPodsDropdownItems();
items.at(0).vm.$emit('click');
expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, null);
});
it('clicks on a pod name, showPodLogs is dispatched with pod name', () => {
const items = findPodsDropdownItems();
const index = 2; // any pod
items.at(index + 1).vm.$emit('click'); // skip "All pods" option
expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, mockPods[index]);
});
it('clicks on search, a serches is done', () => {
expect(findSearchBox().attributes('disabled')).toBeFalsy();
// input a query and click `search`
findSearchBox().vm.$emit('input', mockSearch);
findSearchBox().vm.$emit('submit');
expect(dispatch).toHaveBeenCalledWith(`${module}/setSearch`, mockSearch);
});
it('selects a new time range', () => {
expect(findTimeRangePicker().attributes('disabled')).toBeFalsy();
const mockRange = { start: 'START_DATE', end: 'END_DATE' };
findTimeRangePicker().vm.$emit('input', mockRange);
expect(dispatch).toHaveBeenCalledWith(`${module}/setTimeRange`, mockRange);
});
});
});
});
import { GlIcon, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createStore } from '~/logs/stores';
import { mockPods, mockPodName } from '../mock_data';
import LogSimpleFilters from '~/logs/components/log_simple_filters.vue';
const module = 'environmentLogs';
describe('LogSimpleFilters', () => {
let store;
let dispatch;
let wrapper;
let state;
const findPodsDropdown = () => wrapper.find({ ref: 'podsDropdown' });
const findPodsNoPodsText = () => wrapper.find({ ref: 'noPodsMsg' });
const findPodsDropdownItems = () =>
findPodsDropdown()
.findAll(GlDropdownItem)
.filter(item => !item.is('[disabled]'));
const mockPodsLoading = () => {
state.pods.options = [];
state.pods.current = null;
};
const mockPodsLoaded = () => {
state.pods.options = mockPods;
state.pods.current = mockPodName;
};
const initWrapper = (propsData = {}) => {
wrapper = shallowMount(LogSimpleFilters, {
propsData: {
...propsData,
},
store,
});
};
beforeEach(() => {
store = createStore();
state = store.state.environmentLogs;
jest.spyOn(store, 'dispatch').mockResolvedValue();
dispatch = store.dispatch;
});
afterEach(() => {
store.dispatch.mockReset();
if (wrapper) {
wrapper.destroy();
}
});
it('displays UI elements', () => {
initWrapper();
expect(wrapper.isVueInstance()).toBe(true);
expect(wrapper.isEmpty()).toBe(false);
expect(findPodsDropdown().exists()).toBe(true);
});
describe('disabled state', () => {
beforeEach(() => {
mockPodsLoading();
initWrapper({
disabled: true,
});
});
it('displays a disabled pods dropdown', () => {
expect(findPodsDropdown().props('text')).toBe('No pod selected');
expect(findPodsDropdown().attributes('disabled')).toBeTruthy();
});
});
describe('loading state', () => {
beforeEach(() => {
mockPodsLoading();
initWrapper();
});
it('displays an enabled pods dropdown', () => {
expect(findPodsDropdown().attributes('disabled')).toBeFalsy();
expect(findPodsDropdown().props('text')).toBe('No pod selected');
});
it('displays an empty pods dropdown', () => {
expect(findPodsNoPodsText().exists()).toBe(true);
expect(findPodsDropdownItems()).toHaveLength(0);
});
});
describe('pods available state', () => {
beforeEach(() => {
mockPodsLoaded();
initWrapper();
});
it('displays an enabled pods dropdown', () => {
expect(findPodsDropdown().attributes('disabled')).toBeFalsy();
expect(findPodsDropdown().props('text')).toBe(mockPods[0]);
});
it('displays a pods dropdown with items', () => {
expect(findPodsNoPodsText().exists()).toBe(false);
expect(findPodsDropdownItems()).toHaveLength(mockPods.length);
});
it('dropdown has one pod selected', () => {
const items = findPodsDropdownItems();
mockPods.forEach((pod, i) => {
const item = items.at(i);
if (item.text() !== mockPodName) {
expect(item.find(GlIcon).classes('invisible')).toBe(true);
} else {
expect(item.find(GlIcon).classes('invisible')).toBe(false);
}
});
});
it('when the user clicks on a pod, showPodLogs is dispatched', () => {
const items = findPodsDropdownItems();
const index = 2; // any pod
expect(dispatch).not.toHaveBeenCalledWith(`${module}/showPodLogs`, expect.anything());
items.at(index).vm.$emit('click');
expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, mockPods[index]);
});
});
});
import * as getters from '~/logs/stores/getters'; import { trace, showAdvancedFilters } from '~/logs/stores/getters';
import logsPageState from '~/logs/stores/state'; import logsPageState from '~/logs/stores/state';
import { mockLogsResult, mockTrace } from '../mock_data'; import { mockLogsResult, mockTrace, mockEnvName, mockEnvironments } from '../mock_data';
describe('Logs Store getters', () => { describe('Logs Store getters', () => {
let state; let state;
...@@ -13,7 +13,7 @@ describe('Logs Store getters', () => { ...@@ -13,7 +13,7 @@ describe('Logs Store getters', () => {
describe('trace', () => { describe('trace', () => {
describe('when state is initialized', () => { describe('when state is initialized', () => {
it('returns an empty string', () => { it('returns an empty string', () => {
expect(getters.trace(state)).toEqual(''); expect(trace(state)).toEqual('');
}); });
}); });
...@@ -23,7 +23,7 @@ describe('Logs Store getters', () => { ...@@ -23,7 +23,7 @@ describe('Logs Store getters', () => {
}); });
it('returns an empty string', () => { it('returns an empty string', () => {
expect(getters.trace(state)).toEqual(''); expect(trace(state)).toEqual('');
}); });
}); });
...@@ -33,7 +33,42 @@ describe('Logs Store getters', () => { ...@@ -33,7 +33,42 @@ describe('Logs Store getters', () => {
}); });
it('returns an empty string', () => { it('returns an empty string', () => {
expect(getters.trace(state)).toEqual(mockTrace.join('\n')); expect(trace(state)).toEqual(mockTrace.join('\n'));
});
});
});
describe('showAdvancedFilters', () => {
describe('when no environments are set', () => {
beforeEach(() => {
state.environments.current = mockEnvName;
state.environments.options = [];
});
it('returns false', () => {
expect(showAdvancedFilters(state)).toBe(false);
});
});
describe('when the environment supports filters', () => {
beforeEach(() => {
state.environments.current = mockEnvName;
state.environments.options = mockEnvironments;
});
it('returns true', () => {
expect(showAdvancedFilters(state)).toBe(true);
});
});
describe('when the environment does not support filters', () => {
beforeEach(() => {
state.environments.options = mockEnvironments;
state.environments.current = mockEnvironments[1].name;
});
it('returns true', () => {
expect(showAdvancedFilters(state)).toBe(false);
}); });
}); });
}); });
......
...@@ -46,6 +46,7 @@ describe Gitlab::Ci::Config::Entry::Reports do ...@@ -46,6 +46,7 @@ describe Gitlab::Ci::Config::Entry::Reports do
:lsif | 'lsif.json' :lsif | 'lsif.json'
:dotenv | 'build.dotenv' :dotenv | 'build.dotenv'
:cobertura | 'cobertura-coverage.xml' :cobertura | 'cobertura-coverage.xml'
:terraform | 'tfplan.json'
end end
with_them do with_them do
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::GrapeLogging::Loggers::PerfLogger do
subject { described_class.new }
describe ".parameters" do
let(:mock_request) { OpenStruct.new(env: {}) }
describe 'when no performance datais are present' do
it 'returns an empty Hash' do
expect(subject.parameters(mock_request, nil)).to eq({})
end
end
describe 'when Redis calls are present', :request_store do
it 'returns a Hash with Redis information' do
Gitlab::Redis::SharedState.with { |redis| redis.get('perf-logger-test') }
payload = subject.parameters(mock_request, nil)
expect(payload[:redis_calls]).to eq(1)
expect(payload[:redis_duration_ms]).to be >= 0
end
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
require 'fast_spec_helper' require 'spec_helper'
require 'rspec-parameterized' require 'rspec-parameterized'
describe Gitlab::InstrumentationHelper do describe Gitlab::InstrumentationHelper do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
describe '.add_instrumentation_data', :request_store do
let(:payload) { {} }
subject { described_class.add_instrumentation_data(payload) }
it 'adds nothing' do
subject
expect(payload).to eq({})
end
context 'when Gitaly calls are made' do
it 'adds Gitaly data and omits Redis data' do
project = create(:project)
RequestStore.clear!
project.repository.exists?
subject
expect(payload[:gitaly_calls]).to eq(1)
expect(payload[:gitaly_duration]).to be >= 0
expect(payload[:redis_calls]).to be_nil
expect(payload[:redis_duration_ms]).to be_nil
end
end
context 'when Redis calls are made' do
it 'adds Redis data and omits Gitaly data' do
Gitlab::Redis::Cache.with { |redis| redis.get('test-instrumentation') }
subject
expect(payload[:redis_calls]).to eq(1)
expect(payload[:redis_duration_ms]).to be >= 0
expect(payload[:gitaly_calls]).to be_nil
expect(payload[:gitaly_duration]).to be_nil
end
end
end
describe '.queue_duration_for_job' do describe '.queue_duration_for_job' do
where(:enqueued_at, :created_at, :time_now, :expected_duration) do where(:enqueued_at, :created_at, :time_now, :expected_duration) do
"2019-06-01T00:00:00.000+0000" | nil | "2019-06-01T02:00:00.000+0000" | 2.hours.to_f "2019-06-01T00:00:00.000+0000" | nil | "2019-06-01T02:00:00.000+0000" | 2.hours.to_f
......
...@@ -175,26 +175,30 @@ describe Gitlab::SidekiqLogging::StructuredLogger do ...@@ -175,26 +175,30 @@ describe Gitlab::SidekiqLogging::StructuredLogger do
end end
end end
context 'with Gitaly and Rugged calls' do context 'with Gitaly, Rugged, and Redis calls' do
let(:timing_data) do let(:timing_data) do
{ {
gitaly_calls: 10, gitaly_calls: 10,
gitaly_duration: 10000, gitaly_duration: 10000,
rugged_calls: 1, rugged_calls: 1,
rugged_duration_ms: 5000 rugged_duration_ms: 5000,
redis_calls: 3,
redis_duration_ms: 1234
} }
end end
before do let(:expected_end_payload) do
job.merge!(timing_data) end_payload.except('args').merge(timing_data)
end end
it 'logs with Gitaly and Rugged timing data' do it 'logs with Gitaly and Rugged timing data' do
Timecop.freeze(timestamp) do Timecop.freeze(timestamp) do
expect(logger).to receive(:info).with(start_payload.except('args')).ordered expect(logger).to receive(:info).with(start_payload.except('args')).ordered
expect(logger).to receive(:info).with(end_payload.except('args')).ordered expect(logger).to receive(:info).with(expected_end_payload).ordered
subject.call(job, 'test_queue') { } subject.call(job, 'test_queue') do
job.merge!(timing_data)
end
end end
end end
end end
......
...@@ -140,6 +140,18 @@ describe Ci::JobArtifact do ...@@ -140,6 +140,18 @@ describe Ci::JobArtifact do
end end
end end
describe '.for_job_name' do
it 'returns job artifacts for a given job name' do
first_job = create(:ci_build, name: 'first')
second_job = create(:ci_build, name: 'second')
first_artifact = create(:ci_job_artifact, job: first_job)
second_artifact = create(:ci_job_artifact, job: second_job)
expect(described_class.for_job_name(first_job.name)).to eq([first_artifact])
expect(described_class.for_job_name(second_job.name)).to eq([second_artifact])
end
end
describe 'callbacks' do describe 'callbacks' do
subject { create(:ci_job_artifact, :archive) } subject { create(:ci_job_artifact, :archive) }
......
...@@ -33,17 +33,34 @@ describe Milestone, 'Milestoneish' do ...@@ -33,17 +33,34 @@ describe Milestone, 'Milestoneish' do
end end
describe '#sorted_issues' do describe '#sorted_issues' do
it 'sorts issues by label priority' do before do
issue.labels << label_1 issue.labels << label_1
security_issue_1.labels << label_2 security_issue_1.labels << label_2
closed_issue_1.labels << label_3 closed_issue_1.labels << label_3
end
it 'sorts issues by label priority' do
issues = milestone.sorted_issues(member) issues = milestone.sorted_issues(member)
expect(issues.first).to eq(issue) expect(issues.first).to eq(issue)
expect(issues.second).to eq(security_issue_1) expect(issues.second).to eq(security_issue_1)
expect(issues.third).not_to eq(closed_issue_1) expect(issues.third).not_to eq(closed_issue_1)
end end
it 'limits issue count and keeps the ordering' do
stub_const('Milestoneish::DISPLAY_ISSUES_LIMIT', 4)
issues = milestone.sorted_issues(member)
# Cannot use issues.count here because it is sorting
# by a virtual column 'highest_priority' and it will break
# the query.
total_issues_count = issues.opened.unassigned.length + issues.opened.assigned.length + issues.closed.length
expect(issues.length).to eq(4)
expect(total_issues_count).to eq(4)
expect(issues.first).to eq(issue)
expect(issues.second).to eq(security_issue_1)
expect(issues.third).not_to eq(closed_issue_1)
end
end end
context 'attributes visibility' do context 'attributes visibility' do
......
...@@ -149,9 +149,58 @@ describe Service do ...@@ -149,9 +149,58 @@ describe Service do
end end
end end
describe "Template" do describe 'template' do
let(:project) { create(:project) } let(:project) { create(:project) }
shared_examples 'retrieves service templates' do
it 'returns the available service templates' do
expect(Service.find_or_create_templates.pluck(:type)).to match_array(Service.available_services_types)
end
end
describe '.find_or_create_templates' do
it 'creates service templates' do
expect { Service.find_or_create_templates }.to change { Service.count }.from(0).to(Service.available_services_names.size)
end
it_behaves_like 'retrieves service templates'
context 'with all existing templates' do
before do
Service.insert_all(
Service.available_services_types.map { |type| { template: true, type: type } }
)
end
it 'does not create service templates' do
expect { Service.find_or_create_templates }.to change { Service.count }.by(0)
end
it_behaves_like 'retrieves service templates'
context 'with a previous existing service (Previous) and a new service (Asana)' do
before do
Service.insert(type: 'PreviousService', template: true)
Service.delete_by(type: 'AsanaService', template: true)
end
it_behaves_like 'retrieves service templates'
end
end
context 'with a few existing templates' do
before do
create(:jira_service, :template)
end
it 'creates the rest of the service templates' do
expect { Service.find_or_create_templates }.to change { Service.count }.from(1).to(Service.available_services_names.size)
end
it_behaves_like 'retrieves service templates'
end
end
describe '.build_from_template' do describe '.build_from_template' do
context 'when template is invalid' do context 'when template is invalid' do
it 'sets service template to inactive when template is invalid' do it 'sets service template to inactive when template is invalid' do
......
...@@ -34,6 +34,7 @@ describe Ci::RetryBuildService do ...@@ -34,6 +34,7 @@ describe Ci::RetryBuildService do
job_artifacts_container_scanning job_artifacts_dast job_artifacts_container_scanning job_artifacts_dast
job_artifacts_license_management job_artifacts_license_scanning job_artifacts_license_management job_artifacts_license_scanning
job_artifacts_performance job_artifacts_lsif job_artifacts_performance job_artifacts_lsif
job_artifacts_terraform
job_artifacts_codequality job_artifacts_metrics scheduled_at job_artifacts_codequality job_artifacts_metrics scheduled_at
job_variables waiting_for_resource_at job_artifacts_metrics_referee job_variables waiting_for_resource_at job_artifacts_metrics_referee
job_artifacts_network_referee job_artifacts_dotenv job_artifacts_network_referee job_artifacts_dotenv
......
...@@ -26,3 +26,15 @@ shared_examples 'accepted carrierwave upload' do ...@@ -26,3 +26,15 @@ shared_examples 'accepted carrierwave upload' do
expect { uploader.cache!(fixture_file) }.to change { uploader.file }.from(nil).to(kind_of(CarrierWave::SanitizedFile)) expect { uploader.cache!(fixture_file) }.to change { uploader.file }.from(nil).to(kind_of(CarrierWave::SanitizedFile))
end end
end end
# @param path [String] the path to file to upload. E.g. File.join('spec', 'fixtures', 'sanitized.svg')
# @param uploader [CarrierWave::Uploader::Base] uploader to handle the upload.
# @param content_type [String] the upload file content type after cache
shared_examples 'upload with content type' do |content_type|
let(:fixture_file) { fixture_file_upload(path, content_type) }
it 'will not change upload file content type' do
uploader.cache!(fixture_file)
expect(uploader.file.content_type).to eq(content_type)
end
end
...@@ -18,6 +18,7 @@ describe ContentTypeWhitelist do ...@@ -18,6 +18,7 @@ describe ContentTypeWhitelist do
let(:path) { File.join('spec', 'fixtures', 'rails_sample.jpg') } let(:path) { File.join('spec', 'fixtures', 'rails_sample.jpg') }
it_behaves_like 'accepted carrierwave upload' it_behaves_like 'accepted carrierwave upload'
it_behaves_like 'upload with content type', 'image/jpeg'
end end
context 'upload non-whitelisted file content type' do context 'upload non-whitelisted file content type' do
......
...@@ -97,5 +97,12 @@ describe JobArtifactUploader do ...@@ -97,5 +97,12 @@ describe JobArtifactUploader do
it_behaves_like "migrates", to_store: described_class::Store::REMOTE it_behaves_like "migrates", to_store: described_class::Store::REMOTE
it_behaves_like "migrates", from_store: described_class::Store::REMOTE, to_store: described_class::Store::LOCAL it_behaves_like "migrates", from_store: described_class::Store::REMOTE, to_store: described_class::Store::LOCAL
# CI job artifacts usually are shown as text/plain, but they contain
# escape characters so MIME detectors usually fail to determine what
# the Content-Type is.
it 'does not set Content-Type' do
expect(uploader.file.content_type).to be_blank
end
end end
end end
File mode changed from 100644 to 100755
File mode changed from 100644 to 100755
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