summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-03-24 18:07:55 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-03-24 18:07:55 +0000
commit603c7d4cac5e28bc1c75e50c23ed2cbe56f1aafc (patch)
tree907f5b8ee1b6f5aad396e95e3327a08400b9e8ea
parent120f4aaedc8fe830a3f572491d240d8ee6addefb (diff)
downloadgitlab-ce-603c7d4cac5e28bc1c75e50c23ed2cbe56f1aafc.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/api.js9
-rw-r--r--app/assets/javascripts/code_navigation/components/app.vue3
-rw-r--r--app/assets/javascripts/code_navigation/components/popover.vue13
-rw-r--r--app/assets/javascripts/code_navigation/store/actions.js9
-rw-r--r--app/assets/javascripts/code_navigation/store/mutations.js7
-rw-r--r--app/assets/javascripts/ide/components/branches/search_list.vue4
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue4
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail.vue4
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/list.vue4
-rw-r--r--app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue3
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue4
-rw-r--r--app/assets/javascripts/ide/components/preview/clientside.vue6
-rw-r--r--app/assets/javascripts/ide/index.js4
-rw-r--r--app/assets/javascripts/ide/lib/diff/controller.js2
-rw-r--r--app/assets/javascripts/ide/lib/editor.js4
-rw-r--r--app/assets/javascripts/ide/stores/actions.js4
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js6
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js4
-rw-r--r--app/assets/javascripts/logs/components/environment_logs.vue205
-rw-r--r--app/assets/javascripts/logs/components/log_advanced_filters.vue128
-rw-r--r--app/assets/javascripts/logs/components/log_simple_filters.vue73
-rw-r--r--app/assets/javascripts/logs/stores/getters.js8
-rw-r--r--app/assets/stylesheets/pages/builds.scss22
-rw-r--r--app/assets/stylesheets/utilities.scss1
-rw-r--r--app/controllers/admin/services_controller.rb16
-rw-r--r--app/controllers/projects/blob_controller.rb13
-rw-r--r--app/helpers/milestones_helper.rb23
-rw-r--r--app/models/ci/build.rb2
-rw-r--r--app/models/ci/job_artifact.rb10
-rw-r--r--app/models/concerns/milestoneish.rb12
-rw-r--r--app/models/issue.rb2
-rw-r--r--app/models/service.rb33
-rw-r--r--app/uploaders/content_type_whitelist.rb12
-rw-r--r--app/views/projects/blob/_blob.html.haml5
-rw-r--r--app/views/shared/milestones/_issues_tab.html.haml5
-rw-r--r--app/views/shared/milestones/_labels_tab.html.haml6
-rw-r--r--changelogs/unreleased/207912-implementing-filtered-search-advanced-filters.yml5
-rw-r--r--changelogs/unreleased/20820-service-templates-performance.yml5
-rw-r--r--changelogs/unreleased/208884-optimize-ci_builds-counters-in-usage-data.yml5
-rw-r--r--changelogs/unreleased/212264-gcs-job-log-is-stored-with-content-type-invalid-invalid.yml5
-rw-r--r--changelogs/unreleased/issue_39453.yml5
-rw-r--r--changelogs/unreleased/sh-log-redis-calls.yml5
-rw-r--r--danger/changelog/Dangerfile2
-rw-r--r--db/migrate/20200323122201_add_index_on_user_and_created_at_to_ci_builds.rb19
-rw-r--r--db/structure.sql5
-rw-r--r--doc/administration/gitaly/praefect.md5
-rw-r--r--doc/administration/raketasks/maintenance.md5
-rw-r--r--doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md38
-rw-r--r--doc/administration/troubleshooting/sidekiq.md36
-rw-r--r--doc/development/documentation/styleguide.md33
-rw-r--r--doc/update/README.md9
-rw-r--r--doc/user/admin_area/settings/usage_statistics.md5
-rw-r--r--doc/user/application_security/sast/index.md8
-rw-r--r--lib/gitlab/ci/config/entry/reports.rb3
-rw-r--r--lib/gitlab/grape_logging/loggers/perf_logger.rb27
-rw-r--r--lib/gitlab/instrumentation/redis.rb70
-rw-r--r--lib/gitlab/instrumentation_helper.rb9
-rw-r--r--lib/peek/views/detailed_view.rb2
-rw-r--r--lib/peek/views/redis_detailed.rb38
-rw-r--r--locale/gitlab.pot69
-rw-r--r--qa/qa.rb1
-rw-r--r--qa/qa/runtime/project.rb35
-rw-r--r--qa/qa/runtime/search.rb16
-rw-r--r--spec/controllers/admin/services_controller_spec.rb19
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb37
-rw-r--r--spec/features/milestones/user_views_milestone_spec.rb31
-rw-r--r--spec/features/projects/environments_pod_logs_spec.rb6
-rw-r--r--spec/frontend/code_navigation/components/app_spec.js1
-rw-r--r--spec/frontend/code_navigation/components/popover_spec.js16
-rw-r--r--spec/frontend/code_navigation/store/actions_spec.js38
-rw-r--r--spec/frontend/code_navigation/store/mutations_spec.js10
-rw-r--r--spec/frontend/ide/components/branches/search_list_spec.js2
-rw-r--r--spec/frontend/logs/components/environment_logs_spec.js112
-rw-r--r--spec/frontend/logs/components/log_advanced_filters_spec.js185
-rw-r--r--spec/frontend/logs/components/log_simple_filters_spec.js138
-rw-r--r--spec/frontend/logs/stores/getters_spec.js45
-rw-r--r--spec/lib/gitlab/ci/config/entry/reports_spec.rb1
-rw-r--r--spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb28
-rw-r--r--spec/lib/gitlab/instrumentation_helper_spec.rb42
-rw-r--r--spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb16
-rw-r--r--spec/models/ci/job_artifact_spec.rb12
-rw-r--r--spec/models/concerns/milestoneish_spec.rb19
-rw-r--r--spec/models/service_spec.rb51
-rw-r--r--spec/services/ci/retry_build_service_spec.rb1
-rw-r--r--spec/support/shared_examples/uploaders/upload_type_shared_examples.rb12
-rw-r--r--spec/uploaders/content_type_whitelist_spec.rb1
-rw-r--r--spec/uploaders/job_artifact_uploader_spec.rb7
-rwxr-xr-x[-rw-r--r--]vendor/gitignore/C++.gitignore0
-rwxr-xr-x[-rw-r--r--]vendor/gitignore/Java.gitignore0
89 files changed, 1388 insertions, 577 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 022d79ecf49..14381f63e4b 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -44,7 +44,6 @@ const Api = {
mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines',
adminStatisticsPath: '/api/:version/application/statistics',
pipelineSinglePath: '/api/:version/projects/:id/pipelines/:pipeline_id',
- lsifPath: '/api/:version/projects/:id/commits/:commit_id/lsif/info',
environmentsPath: '/api/:version/projects/:id/environments',
group(groupId, callback) {
@@ -474,14 +473,6 @@ const Api = {
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) {
const url = Api.buildUrl(this.environmentsPath).replace(':id', encodeURIComponent(id));
return axios.get(url);
diff --git a/app/assets/javascripts/code_navigation/components/app.vue b/app/assets/javascripts/code_navigation/components/app.vue
index 0e5f1f0485d..0e0160b9832 100644
--- a/app/assets/javascripts/code_navigation/components/app.vue
+++ b/app/assets/javascripts/code_navigation/components/app.vue
@@ -7,7 +7,7 @@ export default {
Popover,
},
computed: {
- ...mapState(['currentDefinition', 'currentDefinitionPosition']),
+ ...mapState(['currentDefinition', 'currentDefinitionPosition', 'definitionPathPrefix']),
},
mounted() {
this.blobViewer = document.querySelector('.blob-viewer');
@@ -39,5 +39,6 @@ export default {
v-if="currentDefinition"
:position="currentDefinitionPosition"
:data="currentDefinition"
+ :definition-path-prefix="definitionPathPrefix"
/>
</template>
diff --git a/app/assets/javascripts/code_navigation/components/popover.vue b/app/assets/javascripts/code_navigation/components/popover.vue
index d5bbe430fcd..f216a4c6e6f 100644
--- a/app/assets/javascripts/code_navigation/components/popover.vue
+++ b/app/assets/javascripts/code_navigation/components/popover.vue
@@ -14,6 +14,10 @@ export default {
type: Object,
required: true,
},
+ definitionPathPrefix: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -27,6 +31,11 @@ export default {
top: `${this.position.y + this.position.height}px`,
};
},
+ definitionPath() {
+ return (
+ this.data.definition_path && `${this.definitionPathPrefix}/${this.data.definition_path}`
+ );
+ },
},
watch: {
position: {
@@ -67,8 +76,8 @@ export default {
{{ hover.value }}
</p>
</div>
- <div v-if="data.definition_url" class="popover-body">
- <gl-button :href="data.definition_url" target="_blank" class="w-100" variant="default">
+ <div v-if="definitionPath" class="popover-body">
+ <gl-button :href="definitionPath" target="_blank" class="w-100" variant="default">
{{ __('Go to definition') }}
</gl-button>
</div>
diff --git a/app/assets/javascripts/code_navigation/store/actions.js b/app/assets/javascripts/code_navigation/store/actions.js
index 5220b1215b8..9b607023f39 100644
--- a/app/assets/javascripts/code_navigation/store/actions.js
+++ b/app/assets/javascripts/code_navigation/store/actions.js
@@ -1,4 +1,4 @@
-import api from '~/api';
+import axios from '~/lib/utils/axios_utils';
import * as types from './mutation_types';
import { getCurrentHoverElement, setCurrentHoverElement, addInteractionClass } from '../utils';
@@ -12,11 +12,10 @@ export default {
fetchData({ commit, dispatch, state }) {
commit(types.REQUEST_DATA);
- api
- .lsifData(state.projectPath, state.commitId, [state.blobPath])
+ axios
+ .get(state.codeNavUrl)
.then(({ data }) => {
- const dataForPath = data[state.blobPath];
- const normalizedData = dataForPath.reduce((acc, d) => {
+ const normalizedData = data.reduce((acc, d) => {
if (d.hover) {
acc[`${d.start_line}:${d.start_char}`] = d;
addInteractionClass(d);
diff --git a/app/assets/javascripts/code_navigation/store/mutations.js b/app/assets/javascripts/code_navigation/store/mutations.js
index bb833a5adbc..febb7afe2f8 100644
--- a/app/assets/javascripts/code_navigation/store/mutations.js
+++ b/app/assets/javascripts/code_navigation/store/mutations.js
@@ -1,10 +1,9 @@
import * as types from './mutation_types';
export default {
- [types.SET_INITIAL_DATA](state, { projectPath, commitId, blobPath }) {
- state.projectPath = projectPath;
- state.commitId = commitId;
- state.blobPath = blobPath;
+ [types.SET_INITIAL_DATA](state, { codeNavUrl, definitionPathPrefix }) {
+ state.codeNavUrl = codeNavUrl;
+ state.definitionPathPrefix = definitionPathPrefix;
},
[types.REQUEST_DATA](state) {
state.loading = true;
diff --git a/app/assets/javascripts/ide/components/branches/search_list.vue b/app/assets/javascripts/ide/components/branches/search_list.vue
index 31f1dec43ad..76821bcd986 100644
--- a/app/assets/javascripts/ide/components/branches/search_list.vue
+++ b/app/assets/javascripts/ide/components/branches/search_list.vue
@@ -1,6 +1,6 @@
<script>
import { mapActions, mapState } from 'vuex';
-import _ from 'underscore';
+import { debounce } from 'lodash';
import { GlLoadingIcon } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import Item from './item.vue';
@@ -39,7 +39,7 @@ export default {
loadBranches() {
this.fetchBranches({ search: this.search });
},
- searchBranches: _.debounce(function debounceSearch() {
+ searchBranches: debounce(function debounceSearch() {
this.loadBranches();
}, 250),
focusSearch() {
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index 2581c3e9928..beff95eb47b 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -1,5 +1,5 @@
<script>
-import _ from 'underscore';
+import { escape as esc } from 'lodash';
import { mapState, mapGetters, createNamespacedHelpers } from 'vuex';
import { sprintf, s__ } from '~/locale';
import consts from '../../stores/modules/commit/constants';
@@ -22,7 +22,7 @@ export default {
commitToCurrentBranchText() {
return sprintf(
s__('IDE|Commit to %{branchName} branch'),
- { branchName: `<strong class="monospace">${_.escape(this.currentBranchId)}</strong>` },
+ { branchName: `<strong class="monospace">${esc(this.currentBranchId)}</strong>` },
false,
);
},
diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue
index 7710bfb49ec..504391ffdc7 100644
--- a/app/assets/javascripts/ide/components/jobs/detail.vue
+++ b/app/assets/javascripts/ide/components/jobs/detail.vue
@@ -1,6 +1,6 @@
<script>
import { mapActions, mapState } from 'vuex';
-import _ from 'underscore';
+import { throttle } from 'lodash';
import { __ } from '../../../locale';
import tooltip from '../../../vue_shared/directives/tooltip';
import Icon from '../../../vue_shared/components/icon.vue';
@@ -53,7 +53,7 @@ export default {
this.$refs.buildTrace.scrollTo(0, 0);
}
},
- scrollBuildLog: _.throttle(function buildLogScrollDebounce() {
+ scrollBuildLog: throttle(function buildLogScrollDebounce() {
const { scrollTop } = this.$refs.buildTrace;
const { offsetHeight, scrollHeight } = this.$refs.buildTrace;
diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue
index 5a8face062b..15c08988977 100644
--- a/app/assets/javascripts/ide/components/merge_requests/list.vue
+++ b/app/assets/javascripts/ide/components/merge_requests/list.vue
@@ -1,6 +1,6 @@
<script>
import { mapActions, mapState } from 'vuex';
-import _ from 'underscore';
+import { debounce } from 'lodash';
import { GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
@@ -59,7 +59,7 @@ export default {
loadMergeRequests() {
this.fetchMergeRequests({ type: this.type, search: this.search });
},
- searchMergeRequests: _.debounce(function debounceSearch() {
+ searchMergeRequests: debounce(function debounceSearch() {
this.loadMergeRequests();
}, 250),
onSearchFocus() {
diff --git a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue
index 7f65d089148..8adf0122fb4 100644
--- a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue
+++ b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue
@@ -1,6 +1,5 @@
<script>
import { mapActions, mapState } from 'vuex';
-import _ from 'underscore';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import ResizablePanel from '../resizable_panel.vue';
@@ -55,7 +54,7 @@ export default {
return this.extensionTabs.filter(tab => tab.show);
},
tabViews() {
- return _.flatten(this.tabs.map(tab => tab.views));
+ return this.tabs.map(tab => tab.views).flat();
},
aliveTabViews() {
return this.tabViews.filter(view => this.isAliveView(view.name));
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
index 3a63fc32639..343b0b6e90c 100644
--- a/app/assets/javascripts/ide/components/pipelines/list.vue
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -1,6 +1,6 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
-import _ from 'underscore';
+import { escape as esc } from 'lodash';
import { GlLoadingIcon } from '@gitlab/ui';
import { sprintf, __ } from '../../../locale';
import Icon from '../../../vue_shared/components/icon.vue';
@@ -35,7 +35,7 @@ export default {
return sprintf(
__('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>',
},
false,
diff --git a/app/assets/javascripts/ide/components/preview/clientside.vue b/app/assets/javascripts/ide/components/preview/clientside.vue
index aa8d932da6e..86a773499bc 100644
--- a/app/assets/javascripts/ide/components/preview/clientside.vue
+++ b/app/assets/javascripts/ide/components/preview/clientside.vue
@@ -1,6 +1,6 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
-import _ from 'underscore';
+import { isEmpty } from 'lodash';
import { Manager } from 'smooshpack';
import { listen } from 'codesandbox-api';
import { GlLoadingIcon } from '@gitlab/ui';
@@ -78,7 +78,7 @@ export default {
.then(() => this.initPreview());
},
beforeDestroy() {
- if (!_.isEmpty(this.manager)) {
+ if (!isEmpty(this.manager)) {
this.manager.listener();
}
this.manager = {};
@@ -125,7 +125,7 @@ export default {
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
- if (_.isEmpty(this.manager)) {
+ if (isEmpty(this.manager)) {
this.initPreview();
return;
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index 9e9d9df8f82..55a0dd848c8 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import { mapActions } from 'vuex';
-import _ from 'underscore';
import Translate from '~/vue_shared/translate';
+import { identity } from 'lodash';
import ide from './components/ide.vue';
import store from './stores';
import router from './ide_router';
@@ -31,7 +31,7 @@ Vue.use(Translate);
export function initIde(el, options = {}) {
if (!el) return null;
- const { rootComponent = ide, extendStore = _.identity } = options;
+ const { rootComponent = ide, extendStore = identity } = options;
return new Vue({
el,
diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js
index 046e562ba2b..234a7f903a1 100644
--- a/app/assets/javascripts/ide/lib/diff/controller.js
+++ b/app/assets/javascripts/ide/lib/diff/controller.js
@@ -1,5 +1,5 @@
import { Range } from 'monaco-editor';
-import { throttle } from 'underscore';
+import { throttle } from 'lodash';
import DirtyDiffWorker from './diff_worker';
import Disposable from '../common/disposable';
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 3d729463cb4..3aff4d30d81 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -1,4 +1,4 @@
-import _ from 'underscore';
+import { debounce } from 'lodash';
import { editor as monacoEditor, KeyCode, KeyMod } from 'monaco-editor';
import store from '../stores';
import DecorationsController from './decorations/controller';
@@ -38,7 +38,7 @@ export default class Editor {
setupThemes();
- this.debouncedUpdate = _.debounce(() => {
+ this.debouncedUpdate = debounce(() => {
this.updateDimensions();
}, 200);
}
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index ddc0925efb9..04cf0ad53d5 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import Vue from 'vue';
-import _ from 'underscore';
+import { escape as esc } from 'lodash';
import { __, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import flash from '~/flash';
@@ -296,7 +296,7 @@ export const getBranchData = ({ commit, state }, { projectId, branchId, force =
sprintf(
__('Branch not loaded - %{branchId}'),
{
- branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`,
+ branchId: `<strong>${esc(projectId)}/${esc(branchId)}</strong>`,
},
false,
),
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index 62084892d13..0b168009847 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -1,4 +1,4 @@
-import _ from 'underscore';
+import { escape as esc } from 'lodash';
import flash from '~/flash';
import { __, sprintf } from '~/locale';
import service from '../../services';
@@ -73,7 +73,7 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => {
text: sprintf(
__("Branch %{branchName} was not found in this project's repository."),
{
- branchName: `<strong>${_.escape(branchId)}</strong>`,
+ branchName: `<strong>${esc(branchId)}</strong>`,
},
false,
),
@@ -154,7 +154,7 @@ export const openBranch = ({ dispatch, state, getters }, { projectId, branchId,
sprintf(
__('An error occurred while getting files for - %{branchId}'),
{
- branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`,
+ branchId: `<strong>${esc(projectId)}/${esc(branchId)}</strong>`,
},
false,
),
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index 828e4ed5eb9..7d48f0adc4c 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -1,4 +1,4 @@
-import _ from 'underscore';
+import { defer } from 'lodash';
import { __ } from '../../../locale';
import service from '../../services';
import * as types from '../mutation_types';
@@ -71,7 +71,7 @@ export const getFiles = ({ state, commit, dispatch }, payload = {}) =>
// Defer setting the directory data because this triggers some intense rendering.
// The entries is all we need to load the file editor.
- _.defer(() => dispatch('setDirectoryData', { projectId, branchId, treeList }));
+ defer(() => dispatch('setDirectoryData', { projectId, branchId, treeList }));
resolve();
})
diff --git a/app/assets/javascripts/logs/components/environment_logs.vue b/app/assets/javascripts/logs/components/environment_logs.vue
index 92d4be81c75..70b3af8dc75 100644
--- a/app/assets/javascripts/logs/components/environment_logs.vue
+++ b/app/assets/javascripts/logs/components/environment_logs.vue
@@ -7,17 +7,15 @@ import {
GlAlert,
GlDropdown,
GlDropdownHeader,
- GlDropdownDivider,
GlDropdownItem,
- GlFormGroup,
- GlSearchBoxByClick,
GlInfiniteScroll,
} 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 { timeRanges, defaultTimeRange } from '~/vue_shared/constants';
+import { defaultTimeRange } from '~/vue_shared/constants';
import { timeRangeFromUrl } from '~/monitoring/utils';
import { formatDate } from '../utils';
@@ -28,12 +26,10 @@ export default {
GlAlert,
GlDropdown,
GlDropdownHeader,
- GlDropdownDivider,
GlDropdownItem,
- GlFormGroup,
- GlSearchBoxByClick,
GlInfiniteScroll,
- DateTimePicker,
+ LogSimpleFilters,
+ LogAdvancedFilters,
LogControlButtons,
},
filters: {
@@ -63,49 +59,22 @@ export default {
traceHeight: 600,
data() {
return {
- searchQuery: '',
- timeRanges,
isElasticStackCalloutDismissed: false,
scrollDownButtonDisabled: true,
};
},
computed: {
...mapState('environmentLogs', ['environments', 'timeRange', 'logs', 'pods']),
- ...mapGetters('environmentLogs', ['trace']),
-
- timeRangeModel: {
- get() {
- return this.timeRange.selected;
- },
- set(val) {
- this.setTimeRange(val);
- },
- },
+ ...mapGetters('environmentLogs', ['trace', 'showAdvancedFilters']),
showLoader() {
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() {
- return !this.isElasticStackCalloutDismissed && this.disableAdvancedControls;
- },
-
- 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');
+ return (
+ !this.isElasticStackCalloutDismissed &&
+ (this.environments.isLoading || !this.showAdvancedFilters)
+ );
},
},
mounted() {
@@ -121,7 +90,6 @@ export default {
...mapActions('environmentLogs', [
'setInitData',
'setSearch',
- 'setTimeRange',
'showPodLogs',
'showEnvironment',
'fetchEnvironments',
@@ -131,9 +99,6 @@ export default {
isCurrentEnvironment(envName) {
return envName === this.environments.current;
},
- isCurrentPod(podName) {
- return podName === this.pods.current;
- },
topReached() {
if (!this.logs.isLoading) {
this.fetchMoreLogsPrepend();
@@ -167,123 +132,49 @@ export default {
</strong>
</a>
</gl-alert>
- <div class="top-bar js-top-bar d-flex">
- <div class="row mx-n1">
- <gl-form-group
- id="environments-dropdown-fg"
- label-size="sm"
- label-for="environments-dropdown"
- class="col-3 px-1"
+ <div class="top-bar d-md-flex border bg-secondary-50 pt-2 pr-1 pb-0 pl-2">
+ <div class="flex-grow-0">
+ <gl-dropdown
+ id="environments-dropdown"
+ :text="environments.current"
+ :disabled="environments.isLoading"
+ class="mb-2 gl-h-32 pr-2 d-flex d-md-block js-environments-dropdown"
>
- <gl-dropdown
- id="environments-dropdown"
- :text="environments.current"
- :disabled="environments.isLoading"
- class="d-flex gl-h-32 js-environments-dropdown"
- toggle-class="dropdown-menu-toggle"
+ <gl-dropdown-header class="text-center">
+ {{ s__('Environments|Select environment') }}
+ </gl-dropdown-header>
+ <gl-dropdown-item
+ v-for="env in environments.options"
+ :key="env.id"
+ @click="showEnvironment(env.name)"
>
- <gl-dropdown-header class="text-center">
- {{ s__('Environments|Select environment') }}
- </gl-dropdown-header>
- <gl-dropdown-item
- v-for="env in environments.options"
- :key="env.id"
- @click="showEnvironment(env.name)"
- >
- <div class="d-flex">
- <gl-icon
- :class="{ invisible: !isCurrentEnvironment(env.name) }"
- name="status_success_borderless"
- />
- <div class="flex-grow-1">{{ env.name }}</div>
- </div>
- </gl-dropdown-item>
- </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>
- </gl-dropdown-item>
- <gl-dropdown-divider />
- </template>
-
- <gl-dropdown-item v-if="!pods.options.length" :disabled="true">
- <span 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>
- </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>
-
- <gl-form-group
- id="dates-fg"
- label-size="sm"
- 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 class="d-flex">
+ <gl-icon
+ :class="{ invisible: !isCurrentEnvironment(env.name) }"
+ name="status_success_borderless"
+ />
+ <div class="flex-grow-1">{{ env.name }}</div>
+ </div>
+ </gl-dropdown-item>
+ </gl-dropdown>
</div>
+ <log-advanced-filters
+ v-if="showAdvancedFilters"
+ ref="log-advanced-filters"
+ class="d-md-flex flex-grow-1"
+ :disabled="environments.isLoading"
+ />
+ <log-simple-filters
+ v-else
+ ref="log-simple-filters"
+ class="d-md-flex flex-grow-1"
+ :disabled="environments.isLoading"
+ />
+
<log-control-buttons
ref="scrollButtons"
- class="controllers"
+ class="flex-grow-0 pr-2 mb-2 controllers"
:scroll-down-button-disabled="scrollDownButtonDisabled"
@refresh="showPodLogs(pods.current)"
@scrollDown="scrollDown"
diff --git a/app/assets/javascripts/logs/components/log_advanced_filters.vue b/app/assets/javascripts/logs/components/log_advanced_filters.vue
new file mode 100644
index 00000000000..dfbd858bf18
--- /dev/null
+++ b/app/assets/javascripts/logs/components/log_advanced_filters.vue
@@ -0,0 +1,128 @@
+<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>
diff --git a/app/assets/javascripts/logs/components/log_simple_filters.vue b/app/assets/javascripts/logs/components/log_simple_filters.vue
new file mode 100644
index 00000000000..21fe1695624
--- /dev/null
+++ b/app/assets/javascripts/logs/components/log_simple_filters.vue
@@ -0,0 +1,73 @@
+<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>
diff --git a/app/assets/javascripts/logs/stores/getters.js b/app/assets/javascripts/logs/stores/getters.js
index 8770306fdd6..d92969c5389 100644
--- a/app/assets/javascripts/logs/stores/getters.js
+++ b/app/assets/javascripts/logs/stores/getters.js
@@ -5,5 +5,9 @@ const mapTrace = ({ timestamp = null, pod = '', message = '' }) =>
export const trace = state => state.logs.lines.map(mapTrace).join('\n');
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
+export const showAdvancedFilters = state => {
+ const environment = state.environments.options.find(
+ ({ name }) => name === state.environments.current,
+ );
+ return Boolean(environment?.enable_advanced_logs_querying);
+};
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 11687378e20..0ecb38a1ea7 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -379,25 +379,19 @@
}
.top-bar {
- @include build-trace-top-bar($gl-line-height * 3);
- position: relative;
- top: 0;
-
- .dropdown-menu-toggle {
- width: 200px;
+ .date-time-picker-wrapper,
+ .dropdown-toggle {
+ @include media-breakpoint-up(md) {
+ width: 140px;
+ }
- @include media-breakpoint-up(sm) {
- width: 300px;
+ @include media-breakpoint-up(lg) {
+ width: 160px;
}
}
.controllers {
- @include build-controllers(16px, flex-end, true, 2);
- }
-
- .refresh-control {
- @include build-controllers(16px, flex-end, true, 0);
- margin-left: 2px;
+ @include build-controllers(16px, flex-end, false, 2);
}
}
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index 00d738a50be..f161d76c623 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -43,6 +43,7 @@
.border-color-blue-300 { border-color: $blue-300; }
.border-color-default { border-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; }
.gl-children-ml-sm-3 > * {
diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb
index 2f554519632..55817550b4b 100644
--- a/app/controllers/admin/services_controller.rb
+++ b/app/controllers/admin/services_controller.rb
@@ -3,11 +3,10 @@
class Admin::ServicesController < Admin::ApplicationController
include ServiceParams
- before_action :whitelist_query_limiting, only: [:index]
before_action :service, only: [:edit, :update]
def index
- @services = services_templates
+ @services = Service.find_or_create_templates
end
def edit
@@ -31,21 +30,8 @@ class Admin::ServicesController < Admin::ApplicationController
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
def service
@service ||= Service.find_by(id: params[:id], template: true)
end
# rubocop: enable CodeReuse/ActiveRecord
-
- def whitelist_query_limiting
- Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42430')
- end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 5788fc17a9b..8c8824ae47f 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -208,11 +208,24 @@ class Projects::BlobController < Projects::ApplicationController
.last_for_path(@repository, @ref, @path).sha
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
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
environment_params[:find_latest] = true
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
@last_commit = @repository.last_commit_for_path(@commit.id, @blob.path)
+ set_code_navigation_build
render 'show'
end
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index da6a0e38c44..2f5aac892ab 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -26,7 +26,7 @@ module MilestonesHelper
end
end
- def milestones_label_path(opts = {})
+ def milestones_issues_path(opts = {})
if @project
project_issues_path(@project, opts)
elsif @group
@@ -283,6 +283,27 @@ module MilestonesHelper
can?(current_user, :admin_milestone, @project.group)
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
MilestonesHelper.prepend_if_ee('EE::MilestonesHelper')
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index b555b78cda6..d0ea7439556 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -33,6 +33,8 @@ module Ci
scheduler_failure: 2
}.freeze
+ CODE_NAVIGATION_JOB_NAME = 'code_navigation'
+
has_one :deployment, as: :deployable, class_name: 'Deployment'
has_one :resource, class_name: 'Ci::Resource', inverse_of: :build
has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index ae57da9c546..ef0701b3874 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -31,7 +31,8 @@ module Ci
metrics: 'metrics.txt',
lsif: 'lsif.json',
dotenv: '.env',
- cobertura: 'cobertura-coverage.xml'
+ cobertura: 'cobertura-coverage.xml',
+ terraform: 'tfplan.json'
}.freeze
INTERNAL_TYPES = {
@@ -59,7 +60,8 @@ module Ci
dast: :raw,
license_management: :raw,
license_scanning: :raw,
- performance: :raw
+ performance: :raw,
+ terraform: :raw
}.freeze
TYPE_AND_FORMAT_PAIRS = INTERNAL_TYPES.merge(REPORT_TYPES).freeze
@@ -80,6 +82,7 @@ module Ci
scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
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_job_name, ->(name) { joins(:job).where(ci_builds: { name: name }) }
scope :with_file_types, -> (file_types) do
types = self.file_types.select { |file_type| file_types.include?(file_type) }.values
@@ -129,7 +132,8 @@ module Ci
network_referee: 14, ## runner referees
lsif: 15, # LSIF data for code navigation
dotenv: 16,
- cobertura: 17
+ cobertura: 17,
+ terraform: 18 # Transformed json
}
enum file_format: {
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index 6dbb9649b9f..fac058e5a46 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
module Milestoneish
+ DISPLAY_ISSUES_LIMIT = 3000
+
def total_issues_count
@total_issues_count ||= Milestones::IssuesCountService.new(self).count
end
@@ -55,7 +57,15 @@ module Milestoneish
end
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
def sorted_merge_requests(user)
diff --git a/app/models/issue.rb b/app/models/issue.rb
index bdcebb4b942..0f00a78c728 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -70,7 +70,7 @@ class Issue < ApplicationRecord
scope :order_closed_date_desc, -> { reorder(closed_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 :public_only, -> { where(confidential: false) }
diff --git a/app/models/service.rb b/app/models/service.rb
index 5782fab3266..138da0c546e 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -46,6 +46,7 @@ class Service < ApplicationRecord
scope :active, -> { where(active: true) }
scope :without_defaults, -> { where(default: false) }
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 :tag_push_hooks, -> { where(tag_push_events: true, active: true) }
@@ -259,14 +260,32 @@ class Service < ApplicationRecord
self.category == :issue_tracker
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
service_names = %w[
alerts
asana
assembla
bamboo
- buildkite
bugzilla
+ buildkite
campfire
custom_issue_tracker
discord
@@ -278,20 +297,20 @@ class Service < ApplicationRecord
hipchat
irker
jira
- mattermost_slash_commands
mattermost
+ mattermost_slash_commands
+ microsoft_teams
packagist
pipelines_email
pivotaltracker
prometheus
pushover
redmine
- youtrack
- slack_slash_commands
slack
+ slack_slash_commands
teamcity
- microsoft_teams
unify_circuit
+ youtrack
]
if Rails.env.development?
@@ -301,6 +320,10 @@ class Service < ApplicationRecord
service_names.sort_by(&:downcase)
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)
service = template.dup
diff --git a/app/uploaders/content_type_whitelist.rb b/app/uploaders/content_type_whitelist.rb
index b3975d7e2e0..3210d57b00c 100644
--- a/app/uploaders/content_type_whitelist.rb
+++ b/app/uploaders/content_type_whitelist.rb
@@ -26,14 +26,14 @@ module ContentTypeWhitelist
# Here we override and extend CarrierWave's method that does not parse the
# magic headers.
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)
- message = I18n.translate(:"errors.messages.content_type_whitelist_error", allowed_types: Array(content_type_whitelist).join(", "))
- raise CarrierWave::IntegrityError, message
+ unless whitelisted_content_type?(content_type)
+ message = I18n.translate(:"errors.messages.content_type_whitelist_error", allowed_types: Array(content_type_whitelist).join(", "))
+ raise CarrierWave::IntegrityError, message
+ end
end
-
- super(new_file)
end
def whitelisted_content_type?(content_type)
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 91d1fc06a41..02a327c5a49 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -9,8 +9,9 @@
= render "projects/blob/auxiliary_viewer", blob: blob
#blob-content-holder.blob-content-holder
- - if native_code_navigation_enabled?(@project)
- #js-code-navigation{ data: { commit_id: blob.commit_id, blob_path: blob.path, project_path: @project.full_path } }
+ - if @code_navigation_build
+ - 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
= render 'projects/blob/header', blob: blob
= render 'projects/blob/content', blob: blob
diff --git a/app/views/shared/milestones/_issues_tab.html.haml b/app/views/shared/milestones/_issues_tab.html.haml
index a8db7f8a556..d7e4f2ed5a0 100644
--- a/app/views/shared/milestones/_issues_tab.html.haml
+++ b/app/views/shared/milestones/_issues_tab.html.haml
@@ -1,6 +1,11 @@
- args = { show_project_name: local_assigns.fetch(:show_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
.col-md-4
= render 'shared/milestones/issuables', args.merge(title: 'Unstarted Issues (open and unassigned)', issuables: issues.opened.unassigned, id: 'unassigned', show_counter: true)
diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml
index 4c930b90ce7..6d79b0d31b2 100644
--- a/app/views/shared/milestones/_labels_tab.html.haml
+++ b/app/views/shared/milestones/_labels_tab.html.haml
@@ -3,12 +3,12 @@
- options = { milestone_title: @milestone.title, label_name: label.title }
%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
= markdown_field(label, :description)
.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'
- = 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'
diff --git a/changelogs/unreleased/207912-implementing-filtered-search-advanced-filters.yml b/changelogs/unreleased/207912-implementing-filtered-search-advanced-filters.yml
new file mode 100644
index 00000000000..2954ca7aece
--- /dev/null
+++ b/changelogs/unreleased/207912-implementing-filtered-search-advanced-filters.yml
@@ -0,0 +1,5 @@
+---
+title: Improve logs filters on mobile, simplify kubernetes API logs filters
+merge_request: 27484
+author:
+type: added
diff --git a/changelogs/unreleased/20820-service-templates-performance.yml b/changelogs/unreleased/20820-service-templates-performance.yml
new file mode 100644
index 00000000000..d1a43032d75
--- /dev/null
+++ b/changelogs/unreleased/20820-service-templates-performance.yml
@@ -0,0 +1,5 @@
+---
+title: Reduce number of SQL queries for service templates
+merge_request: 27396
+author:
+type: performance
diff --git a/changelogs/unreleased/208884-optimize-ci_builds-counters-in-usage-data.yml b/changelogs/unreleased/208884-optimize-ci_builds-counters-in-usage-data.yml
new file mode 100644
index 00000000000..1f622a00b03
--- /dev/null
+++ b/changelogs/unreleased/208884-optimize-ci_builds-counters-in-usage-data.yml
@@ -0,0 +1,5 @@
+---
+title: Optimize ci builds counters in usage data
+merge_request: 27770
+author:
+type: performance
diff --git a/changelogs/unreleased/212264-gcs-job-log-is-stored-with-content-type-invalid-invalid.yml b/changelogs/unreleased/212264-gcs-job-log-is-stored-with-content-type-invalid-invalid.yml
new file mode 100644
index 00000000000..e6b9528780d
--- /dev/null
+++ b/changelogs/unreleased/212264-gcs-job-log-is-stored-with-content-type-invalid-invalid.yml
@@ -0,0 +1,5 @@
+---
+title: Leave upload Content-Type unchaged
+merge_request: 27864
+author:
+type: fixed
diff --git a/changelogs/unreleased/issue_39453.yml b/changelogs/unreleased/issue_39453.yml
new file mode 100644
index 00000000000..c82444009ed
--- /dev/null
+++ b/changelogs/unreleased/issue_39453.yml
@@ -0,0 +1,5 @@
+---
+title: Limits issues displayed on milestones
+merge_request: 23102
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-log-redis-calls.yml b/changelogs/unreleased/sh-log-redis-calls.yml
new file mode 100644
index 00000000000..acf07c04d4b
--- /dev/null
+++ b/changelogs/unreleased/sh-log-redis-calls.yml
@@ -0,0 +1,5 @@
+---
+title: Log Redis call count and duration to log files
+merge_request: 27735
+author:
+type: other
diff --git a/danger/changelog/Dangerfile b/danger/changelog/Dangerfile
index 3c9030e7dbc..ae1a5bbac40 100644
--- a/danger/changelog/Dangerfile
+++ b/danger/changelog/Dangerfile
@@ -28,6 +28,8 @@ def check_changelog_yaml(path)
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}"
+ elsif yaml["merge_request"] != gitlab.mr_json["iid"]
+ fail "Merge request ID was not set to #{gitlab.mr_json["iid"]}! #{SEE_DOC}"
end
rescue Psych::SyntaxError, Psych::DisallowedClass, Psych::BadAlias
# YAML could not be parsed, fail the build.
diff --git a/db/migrate/20200323122201_add_index_on_user_and_created_at_to_ci_builds.rb b/db/migrate/20200323122201_add_index_on_user_and_created_at_to_ci_builds.rb
new file mode 100644
index 00000000000..4f41fc4f478
--- /dev/null
+++ b/db/migrate/20200323122201_add_index_on_user_and_created_at_to_ci_builds.rb
@@ -0,0 +1,19 @@
+# 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
diff --git a/db/structure.sql b/db/structure.sql
index ba03603a7a8..ee6a249093d 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -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_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 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
('20200318165448'),
('20200318175008'),
('20200319203901'),
-('20200323075043');
+('20200323075043'),
+('20200323122201');
diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md
index 5276e3a7816..2c9e1bc3cdf 100644
--- a/doc/administration/gitaly/praefect.md
+++ b/doc/administration/gitaly/praefect.md
@@ -757,10 +757,7 @@ Repositories may be moved from one storage location using the [Repository
API](../../api/projects.html#edit-project):
```shell
-curl --request PUT \
- --header "PRIVATE-TOKEN: <your_access_token>" \
- --data "repository_storage=praefect" \
- https://example.gitlab.com/api/v4/projects/123
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --data "repository_storage=praefect" https://example.gitlab.com/api/v4/projects/123
```
## Debugging Praefect
diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md
index b45f1ae529d..6fd6c5dd8c1 100644
--- a/doc/administration/raketasks/maintenance.md
+++ b/doc/administration/raketasks/maintenance.md
@@ -255,7 +255,10 @@ sudo gitlab-rake gitlab:exclusive_lease:clear[project_housekeeping:4]
## 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
sudo gitlab-rake db:migrate:status
diff --git a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
index 657f29cc789..ec7b4c20462 100644
--- a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
+++ b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
@@ -867,43 +867,7 @@ end
## Sidekiq
-### Kill a worker's Sidekiq jobs
-
-```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>.
+This content has been moved to the [Troubleshooting Sidekiq docs](./sidekiq.md).
## Redis
diff --git a/doc/administration/troubleshooting/sidekiq.md b/doc/administration/troubleshooting/sidekiq.md
index 172e80bee7d..31e41725834 100644
--- a/doc/administration/troubleshooting/sidekiq.md
+++ b/doc/administration/troubleshooting/sidekiq.md
@@ -180,6 +180,13 @@ detach
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
Sometimes the speed at which Sidekiq processes jobs can be so fast that it can
@@ -260,9 +267,34 @@ end
### 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
-# for jobs like this:
-# RepositoryImportWorker.new.perform_async(100)
+# Cancelling jobs like: `RepositoryImportWorker.new.perform_async(100)`
id_list = [100]
queue = Sidekiq::Queue.new('repository_import')
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index f2b59b60fe6..88f8b9b57d2 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -150,8 +150,6 @@ For example:
## 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).
### Folder structure overview
@@ -619,6 +617,22 @@ do not use this option until further notice.
## 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)`.
It's easier to read, review, and maintain. **Do not** use `[Text][identifier]`.
@@ -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>`.
```
+### 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
To indicate the steps of navigation through the UI:
@@ -1361,7 +1388,7 @@ on this document. Further explanation is given below.
- Every method must have the REST API request. For example:
- ```
+ ```plaintext
GET /projects/:id/repository/branches
```
diff --git a/doc/update/README.md b/doc/update/README.md
index 93879efb19e..b183df22589 100644
--- a/doc/update/README.md
+++ b/doc/update/README.md
@@ -122,13 +122,15 @@ If using GitLab 12.9 and newer, run:
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
puts Sidekiq::Queue.new("background_migration").size
Sidekiq::ScheduledSet.new.select { |r| r.klass == 'BackgroundMigrationWorker' }.size
```
+---
+
**For installations from source**
If using GitLab 12.9 and newer, run:
@@ -138,13 +140,16 @@ cd /home/git/gitlab
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
puts Sidekiq::Queue.new("background_migration").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
Major versions are reserved for backwards incompatible changes. We recommend that
diff --git a/doc/user/admin_area/settings/usage_statistics.md b/doc/user/admin_area/settings/usage_statistics.md
index d3ca0520674..9e7fae05be7 100644
--- a/doc/user/admin_area/settings/usage_statistics.md
+++ b/doc/user/admin_area/settings/usage_statistics.md
@@ -10,10 +10,13 @@ to perform various actions.
All statistics are opt-out. You can enable/disable them in the
**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
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)**
If enabled, version check will inform you if a new version is available and the
diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md
index fe0c857347d..a00ec6bedeb 100644
--- a/doc/user/application_security/sast/index.md
+++ b/doc/user/application_security/sast/index.md
@@ -506,9 +506,11 @@ To use SAST in an offline environment, you need:
NOTE: **Note:**
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
-Runner's [`pull_policy` 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.
+meaning the runner will try to pull Docker images from the GitLab container registry even if a local
+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. 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
diff --git a/lib/gitlab/ci/config/entry/reports.rb b/lib/gitlab/ci/config/entry/reports.rb
index 40d37f3601a..8ccee3b5b2b 100644
--- a/lib/gitlab/ci/config/entry/reports.rb
+++ b/lib/gitlab/ci/config/entry/reports.rb
@@ -14,7 +14,7 @@ module Gitlab
ALLOWED_KEYS =
%i[junit codequality sast dependency_scanning container_scanning
dast performance license_management license_scanning metrics lsif
- dotenv cobertura].freeze
+ dotenv cobertura terraform].freeze
attributes ALLOWED_KEYS
@@ -36,6 +36,7 @@ module Gitlab
validates :lsif, array_of_strings_or_string: true
validates :dotenv, array_of_strings_or_string: true
validates :cobertura, array_of_strings_or_string: true
+ validates :terraform, array_of_strings_or_string: true
end
end
diff --git a/lib/gitlab/grape_logging/loggers/perf_logger.rb b/lib/gitlab/grape_logging/loggers/perf_logger.rb
index 7e86b35a215..ca4a702cb94 100644
--- a/lib/gitlab/grape_logging/loggers/perf_logger.rb
+++ b/lib/gitlab/grape_logging/loggers/perf_logger.rb
@@ -5,30 +5,11 @@ module Gitlab
module GrapeLogging
module Loggers
class PerfLogger < ::GrapeLogging::Loggers::Base
- def parameters(_, _)
- gitaly_data.merge(rugged_data)
- end
-
- def gitaly_data
- gitaly_calls = Gitlab::GitalyClient.get_request_count
+ include ::Gitlab::InstrumentationHelper
- return {} if gitaly_calls.zero?
-
- {
- 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
- }
+ def parameters(_, _)
+ payload = {}
+ payload.tap { add_instrumentation_data(payload) }
end
end
end
diff --git a/lib/gitlab/instrumentation/redis.rb b/lib/gitlab/instrumentation/redis.rb
new file mode 100644
index 00000000000..f9a6fdc05aa
--- /dev/null
+++ b/lib/gitlab/instrumentation/redis.rb
@@ -0,0 +1,70 @@
+# 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
diff --git a/lib/gitlab/instrumentation_helper.rb b/lib/gitlab/instrumentation_helper.rb
index edaa9c645b4..5d4e6a7bdef 100644
--- a/lib/gitlab/instrumentation_helper.rb
+++ b/lib/gitlab/instrumentation_helper.rb
@@ -4,7 +4,7 @@ module Gitlab
module InstrumentationHelper
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)
gitaly_calls = Gitlab::GitalyClient.get_request_count
@@ -20,6 +20,13 @@ module Gitlab
payload[:rugged_calls] = rugged_calls
payload[:rugged_duration_ms] = Gitlab::RuggedInstrumentation.query_time_ms
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
# Returns the queuing duration for a Sidekiq job in seconds, as a float, if the
diff --git a/lib/peek/views/detailed_view.rb b/lib/peek/views/detailed_view.rb
index 4f3eddaf11b..389f5301079 100644
--- a/lib/peek/views/detailed_view.rb
+++ b/lib/peek/views/detailed_view.rb
@@ -17,7 +17,7 @@ module Peek
end
def detail_store
- ::Gitlab::SafeRequestStore["#{key}_call_details"] ||= []
+ ::Gitlab::SafeRequestStore["#{key}_call_details".to_sym] ||= []
end
private
diff --git a/lib/peek/views/redis_detailed.rb b/lib/peek/views/redis_detailed.rb
index 14cabd62025..79845044d75 100644
--- a/lib/peek/views/redis_detailed.rb
+++ b/lib/peek/views/redis_detailed.rb
@@ -1,39 +1,5 @@
# 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 Views
class RedisDetailed < DetailedView
@@ -63,7 +29,3 @@ module Peek
end
end
end
-
-class Redis::Client
- prepend Gitlab::Peek::RedisInstrumented
-end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e1cff49bee2..48874a6c489 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -220,9 +220,6 @@ msgstr ""
msgid "%{authorsName}'s thread"
msgstr ""
-msgid "%{buy_now_link_start}Buy now!%{link_end}"
-msgstr ""
-
msgid "%{commit_author_link} authored %{commit_timeago}"
msgstr ""
@@ -446,6 +443,9 @@ msgstr ""
msgid "%{state} epics"
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_plural "%{strong_start}%{branch_count}%{strong_end} Branches"
msgstr[0] ""
@@ -584,10 +584,12 @@ msgstr ""
msgid "+ %{numberOfHiddenAssignees} more"
msgstr ""
-msgid "+%{approvers} more approvers"
-msgstr ""
+msgid "+%d more"
+msgid_plural "+%d more"
+msgstr[0] ""
+msgstr[1] ""
-msgid "+%{extraOptionCount} more"
+msgid "+%{approvers} more approvers"
msgstr ""
msgid "+%{tags} more"
@@ -2427,12 +2429,6 @@ msgstr ""
msgid "Ascending"
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."
msgstr ""
@@ -7825,6 +7821,9 @@ msgstr ""
msgid "Environments|Select environment"
msgstr ""
+msgid "Environments|Select pod"
+msgstr ""
+
msgid "Environments|Show all"
msgstr ""
@@ -8983,9 +8982,6 @@ msgstr ""
msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
msgstr ""
-msgid "For renewal instructions %{link_start}view our Licensing FAQ.%{link_end}"
-msgstr ""
-
msgid "Forgot your password?"
msgstr ""
@@ -13412,6 +13408,9 @@ msgstr ""
msgid "No webhooks found, add one in the form above."
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."
msgstr ""
@@ -16210,12 +16209,6 @@ msgstr ""
msgid "Pushes"
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}\"."
msgstr ""
@@ -18227,6 +18220,9 @@ msgid_plural "Showing %d events"
msgstr[0] ""
msgstr[1] ""
+msgid "Showing %{limit} of %{total_count} issues. "
+msgstr ""
+
msgid "Showing %{pageSize} of %{total} issues"
msgstr ""
@@ -20177,6 +20173,9 @@ msgstr ""
msgid "There was an error updating the dashboard, branch named: %{branch} already exists."
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."
msgstr ""
@@ -21654,12 +21653,6 @@ msgstr ""
msgid "Upload a certificate for your domain with all intermediates"
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"
msgstr ""
@@ -22236,6 +22229,9 @@ msgstr ""
msgid "View Documentation"
msgstr ""
+msgid "View all issues"
+msgstr ""
+
msgid "View blame prior to this change"
msgstr ""
@@ -23053,6 +23049,9 @@ msgstr ""
msgid "You could not create a new trigger."
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"
msgstr ""
@@ -23272,6 +23271,9 @@ msgstr ""
msgid "YouTube"
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."
msgstr ""
@@ -23392,15 +23394,9 @@ msgstr ""
msgid "Your issues will be imported in the background. Once finished, you'll get a confirmation email."
msgstr ""
-msgid "Your license expired on %{expires_at}."
-msgstr ""
-
msgid "Your license is valid from"
msgstr ""
-msgid "Your license will expire in %{remaining_days}."
-msgstr ""
-
msgid "Your message here"
msgstr ""
@@ -23431,10 +23427,13 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
-msgid "Your trial license expired on %{expires_at}."
+msgid "Your subscription expired!"
+msgstr ""
+
+msgid "Your subscription has been downgraded"
msgstr ""
-msgid "Your trial license will expire in %{remaining_days}."
+msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
msgid "Zoom meeting added"
diff --git a/qa/qa.rb b/qa/qa.rb
index 2cf6e02c2aa..b26d926436b 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -39,6 +39,7 @@ module QA
autoload :MailHog, 'qa/runtime/mail_hog'
autoload :IPAddress, 'qa/runtime/ip_address'
autoload :Search, 'qa/runtime/search'
+ autoload :Project, 'qa/runtime/project'
autoload :ApplicationSettings, 'qa/runtime/application_settings'
module API
diff --git a/qa/qa/runtime/project.rb b/qa/qa/runtime/project.rb
new file mode 100644
index 00000000000..89edfee1fbe
--- /dev/null
+++ b/qa/qa/runtime/project.rb
@@ -0,0 +1,35 @@
+# 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
diff --git a/qa/qa/runtime/search.rb b/qa/qa/runtime/search.rb
index 29a71b2815c..f7f87d96e68 100644
--- a/qa/qa/runtime/search.rb
+++ b/qa/qa/runtime/search.rb
@@ -42,6 +42,22 @@ module QA
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)
find_target_in_scope('blobs', search_term) do |record|
record[:filename] == file_name && record[:data].include?(search_term)
diff --git a/spec/controllers/admin/services_controller_spec.rb b/spec/controllers/admin/services_controller_spec.rb
index 35801643181..5dde0d57293 100644
--- a/spec/controllers/admin/services_controller_spec.rb
+++ b/spec/controllers/admin/services_controller_spec.rb
@@ -10,21 +10,14 @@ describe Admin::ServicesController do
end
describe 'GET #edit' do
- let!(:project) { create(:project) }
-
- Service.available_services_names.each do |service_name|
- context "#{service_name}" do
- let!(:service) do
- service_template = "#{service_name}_service".camelize.constantize
- service_template.where(template: true).first_or_create
- end
+ let!(:service) do
+ create(:jira_service, :template)
+ end
- it 'successfully displays the template' do
- get :edit, params: { id: service.id }
+ it 'successfully displays the template' do
+ get :edit, params: { id: service.id }
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
+ expect(response).to have_gitlab_http_status(:ok)
end
end
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 225538dcc45..9fdaa728fd7 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -8,18 +8,17 @@ describe Projects::BlobController do
let(:project) { create(:project, :public, :repository) }
describe "GET show" do
+ def request
+ get(:show, params: { namespace_id: project.namespace, project_id: project, id: id })
+ end
+
render_views
context 'with file path' do
before do
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
- get(:show,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- id: id
- })
+ request
end
context "valid branch, valid file" do
@@ -119,6 +118,32 @@ describe Projects::BlobController do
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
describe 'GET diff' do
diff --git a/spec/features/milestones/user_views_milestone_spec.rb b/spec/features/milestones/user_views_milestone_spec.rb
index cbc21dd02e5..d8bb4902087 100644
--- a/spec/features/milestones/user_views_milestone_spec.rb
+++ b/spec/features/milestones/user_views_milestone_spec.rb
@@ -25,6 +25,37 @@ describe "User views milestone" do
expect { visit_milestone }.not_to exceed_query_limit(control)
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
def visit_milestone
diff --git a/spec/features/projects/environments_pod_logs_spec.rb b/spec/features/projects/environments_pod_logs_spec.rb
index 121a8e1705b..2b2327940a5 100644
--- a/spec/features/projects/environments_pod_logs_spec.rb
+++ b/spec/features/projects/environments_pod_logs_spec.rb
@@ -29,7 +29,7 @@ describe 'Environment > Pod Logs', :js do
wait_for_requests
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)
@@ -47,8 +47,8 @@ describe 'Environment > Pod Logs', :js do
wait_for_requests
- page.within('.js-pods-dropdown') do
- find(".dropdown-menu-toggle:not([disabled])").click
+ page.within('.qa-pods-dropdown') do
+ find(".dropdown-toggle:not([disabled])").click
dropdown_items = find(".dropdown-menu").all(".dropdown-item:not([disabled])")
expect(dropdown_items.size).to eq(1)
diff --git a/spec/frontend/code_navigation/components/app_spec.js b/spec/frontend/code_navigation/components/app_spec.js
index cfdc0dcc6cc..d5693cc4173 100644
--- a/spec/frontend/code_navigation/components/app_spec.js
+++ b/spec/frontend/code_navigation/components/app_spec.js
@@ -16,6 +16,7 @@ function factory(initialState = {}) {
state: {
...createState(),
...initialState,
+ definitionPathPrefix: 'https://test.com/blob/master',
},
actions: {
fetchData,
diff --git a/spec/frontend/code_navigation/components/popover_spec.js b/spec/frontend/code_navigation/components/popover_spec.js
index ad05504a224..df3bbc7c1c6 100644
--- a/spec/frontend/code_navigation/components/popover_spec.js
+++ b/spec/frontend/code_navigation/components/popover_spec.js
@@ -1,6 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import Popover from '~/code_navigation/components/popover.vue';
+const DEFINITION_PATH_PREFIX = 'http:/';
+
const MOCK_CODE_DATA = Object.freeze({
hover: [
{
@@ -8,7 +10,7 @@ const MOCK_CODE_DATA = Object.freeze({
value: 'console.log',
},
],
- definition_url: 'http://test.com',
+ definition_path: 'test.com',
});
const MOCK_DOCS_DATA = Object.freeze({
@@ -18,13 +20,13 @@ const MOCK_DOCS_DATA = Object.freeze({
value: 'console.log',
},
],
- definition_url: 'http://test.com',
+ definition_path: 'test.com',
});
let wrapper;
-function factory(position, data) {
- wrapper = shallowMount(Popover, { propsData: { position, data } });
+function factory(position, data, definitionPathPrefix) {
+ wrapper = shallowMount(Popover, { propsData: { position, data, definitionPathPrefix } });
}
describe('Code navigation popover component', () => {
@@ -33,14 +35,14 @@ describe('Code navigation popover component', () => {
});
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();
});
describe('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: 'doc-output' }).exists()).toBe(false);
@@ -49,7 +51,7 @@ describe('Code navigation popover component', () => {
describe('documentation 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: 'doc-output' }).exists()).toBe(true);
diff --git a/spec/frontend/code_navigation/store/actions_spec.js b/spec/frontend/code_navigation/store/actions_spec.js
index 9c44480ca67..f58dc283ada 100644
--- a/spec/frontend/code_navigation/store/actions_spec.js
+++ b/spec/frontend/code_navigation/store/actions_spec.js
@@ -27,12 +27,10 @@ describe('Code navigation actions', () => {
describe('fetchData', () => {
let mock;
- const state = {
- projectPath: 'gitlab-org/gitlab',
- commitId: '123',
- blobPath: 'index',
- };
- const apiUrl = '/api/1/projects/gitlab-org%2Fgitlab/commits/123/lsif/info';
+
+ const codeNavUrl =
+ 'gitlab-org/gitlab-shell/-/jobs/1114/artifacts/raw/lsif/cmd/check/main.go.json';
+ const state = { codeNavUrl };
beforeEach(() => {
window.gon = { api_version: '1' };
@@ -45,20 +43,18 @@ describe('Code navigation actions', () => {
describe('success', () => {
beforeEach(() => {
- mock.onGet(apiUrl).replyOnce(200, {
- index: [
- {
- start_line: 0,
- start_char: 0,
- hover: { value: '123' },
- },
- {
- start_line: 1,
- start_char: 0,
- hover: null,
- },
- ],
- });
+ mock.onGet(codeNavUrl).replyOnce(200, [
+ {
+ start_line: 0,
+ start_char: 0,
+ hover: { value: '123' },
+ },
+ {
+ start_line: 1,
+ start_char: 0,
+ hover: null,
+ },
+ ]);
});
it('commits REQUEST_DATA_SUCCESS with normalized data', done => {
@@ -106,7 +102,7 @@ describe('Code navigation actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(apiUrl).replyOnce(500);
+ mock.onGet(codeNavUrl).replyOnce(500);
});
it('dispatches requestDataError', done => {
diff --git a/spec/frontend/code_navigation/store/mutations_spec.js b/spec/frontend/code_navigation/store/mutations_spec.js
index 117a2ed2f14..305386f4d0b 100644
--- a/spec/frontend/code_navigation/store/mutations_spec.js
+++ b/spec/frontend/code_navigation/store/mutations_spec.js
@@ -11,14 +11,12 @@ describe('Code navigation mutations', () => {
describe('SET_INITIAL_DATA', () => {
it('sets initial data', () => {
mutations.SET_INITIAL_DATA(state, {
- projectPath: 'test',
- commitId: '123',
- blobPath: 'index.js',
+ codeNavUrl: 'https://test.com/builds/1005',
+ definitionPathPrefix: 'https://test.com/blob/master',
});
- expect(state.projectPath).toBe('test');
- expect(state.commitId).toBe('123');
- expect(state.blobPath).toBe('index.js');
+ expect(state.codeNavUrl).toBe('https://test.com/builds/1005');
+ expect(state.definitionPathPrefix).toBe('https://test.com/blob/master');
});
});
diff --git a/spec/frontend/ide/components/branches/search_list_spec.js b/spec/frontend/ide/components/branches/search_list_spec.js
index fe142d70698..826d51b24f1 100644
--- a/spec/frontend/ide/components/branches/search_list_spec.js
+++ b/spec/frontend/ide/components/branches/search_list_spec.js
@@ -9,6 +9,8 @@ import { branches } from '../../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
+jest.mock('lodash/debounce', () => jest.fn);
+
describe('IDE branches search list', () => {
let wrapper;
const fetchBranchesMock = jest.fn();
diff --git a/spec/frontend/logs/components/environment_logs_spec.js b/spec/frontend/logs/components/environment_logs_spec.js
index 49642153c69..4da987725a1 100644
--- a/spec/frontend/logs/components/environment_logs_spec.js
+++ b/spec/frontend/logs/components/environment_logs_spec.js
@@ -1,7 +1,5 @@
-import Vue from 'vue';
-import { GlSprintf, GlIcon, GlDropdown, GlDropdownItem, GlSearchBoxByClick } from '@gitlab/ui';
+import { GlSprintf, GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui';
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 { createStore } from '~/logs/stores';
@@ -13,7 +11,6 @@ import {
mockLogsResult,
mockTrace,
mockPodName,
- mockSearch,
mockEnvironmentsEndpoint,
mockDocumentationPath,
} from '../mock_data';
@@ -29,7 +26,6 @@ jest.mock('lodash/throttle', () =>
);
describe('EnvironmentLogs', () => {
- let EnvironmentLogsComponent;
let store;
let dispatch;
let wrapper;
@@ -44,13 +40,9 @@ describe('EnvironmentLogs', () => {
const updateControlBtnsMock = jest.fn();
const findEnvironmentsDropdown = () => wrapper.find('.js-environments-dropdown');
- const findPodsDropdown = () => wrapper.find('.js-pods-dropdown');
- const findPodsDropdownItems = () =>
- findPodsDropdown()
- .findAll(GlDropdownItem)
- .filter(itm => !itm.attributes('disabled'));
- const findSearchBar = () => wrapper.find('.js-logs-search');
- const findTimeRangePicker = () => wrapper.find({ ref: 'dateTimePicker' });
+
+ const findSimpleFilters = () => wrapper.find({ ref: 'log-simple-filters' });
+ const findAdvancedFilters = () => wrapper.find({ ref: 'log-advanced-filters' });
const findInfoAlert = () => wrapper.find('.js-elasticsearch-alert');
const findLogControlButtons = () => wrapper.find({ name: 'log-control-buttons-stub' });
@@ -79,7 +71,7 @@ describe('EnvironmentLogs', () => {
};
const initWrapper = () => {
- wrapper = shallowMount(EnvironmentLogsComponent, {
+ wrapper = shallowMount(EnvironmentLogs, {
propsData,
store,
stubs: {
@@ -111,7 +103,6 @@ describe('EnvironmentLogs', () => {
beforeEach(() => {
store = createStore();
state = store.state.environmentLogs;
- EnvironmentLogsComponent = Vue.extend(EnvironmentLogs);
jest.spyOn(store, 'dispatch').mockResolvedValue();
@@ -132,17 +123,10 @@ describe('EnvironmentLogs', () => {
expect(wrapper.isVueInstance()).toBe(true);
expect(wrapper.isEmpty()).toBe(false);
- // top bar
expect(findEnvironmentsDropdown().is(GlDropdown)).toBe(true);
- expect(findPodsDropdown().is(GlDropdown)).toBe(true);
+ expect(findSimpleFilters().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(findLogTrace().exists()).toBe(true);
});
@@ -181,20 +165,6 @@ describe('EnvironmentLogs', () => {
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', () => {
expect(updateControlBtnsMock).not.toHaveBeenCalled();
});
@@ -237,17 +207,14 @@ describe('EnvironmentLogs', () => {
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', () => {
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', () => {
@@ -271,21 +238,6 @@ describe('EnvironmentLogs', () => {
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', () => {
expect(findInfoAlert().exists()).toBe(false);
});
@@ -306,24 +258,16 @@ describe('EnvironmentLogs', () => {
const item = items.at(i);
if (item.text() !== mockEnvName) {
- expect(item.find(GlIcon).classes()).toContain('invisible');
+ expect(item.find(GlIcon).classes('invisible')).toBe(true);
} else {
- // selected
- expect(item.find(GlIcon).classes()).not.toContain('invisible');
+ expect(item.find(GlIcon).classes('invisible')).toBe(false);
}
});
});
- it('populates pods dropdown', () => {
- const items = findPodsDropdownItems();
-
- 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('displays advanced filters for elasticsearch logs API', () => {
+ expect(findSimpleFilters().exists()).toBe(false);
+ expect(findAdvancedFilters().exists()).toBe(true);
});
it('shows infinite scroll with height and no content', () => {
@@ -331,19 +275,6 @@ describe('EnvironmentLogs', () => {
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', () => {
const trace = findLogTrace();
expect(trace.text().split('\n').length).toBe(mockTrace.length);
@@ -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', () => {
expect(dispatch).not.toHaveBeenCalledWith(`${module}/showPodLogs`, expect.anything());
diff --git a/spec/frontend/logs/components/log_advanced_filters_spec.js b/spec/frontend/logs/components/log_advanced_filters_spec.js
new file mode 100644
index 00000000000..a6fbc40c2c6
--- /dev/null
+++ b/spec/frontend/logs/components/log_advanced_filters_spec.js
@@ -0,0 +1,185 @@
+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);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/logs/components/log_simple_filters_spec.js b/spec/frontend/logs/components/log_simple_filters_spec.js
new file mode 100644
index 00000000000..13504a2b1fc
--- /dev/null
+++ b/spec/frontend/logs/components/log_simple_filters_spec.js
@@ -0,0 +1,138 @@
+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]);
+ });
+ });
+});
diff --git a/spec/frontend/logs/stores/getters_spec.js b/spec/frontend/logs/stores/getters_spec.js
index fdce575fa97..9d213d8c01f 100644
--- a/spec/frontend/logs/stores/getters_spec.js
+++ b/spec/frontend/logs/stores/getters_spec.js
@@ -1,7 +1,7 @@
-import * as getters from '~/logs/stores/getters';
+import { trace, showAdvancedFilters } from '~/logs/stores/getters';
import logsPageState from '~/logs/stores/state';
-import { mockLogsResult, mockTrace } from '../mock_data';
+import { mockLogsResult, mockTrace, mockEnvName, mockEnvironments } from '../mock_data';
describe('Logs Store getters', () => {
let state;
@@ -13,7 +13,7 @@ describe('Logs Store getters', () => {
describe('trace', () => {
describe('when state is initialized', () => {
it('returns an empty string', () => {
- expect(getters.trace(state)).toEqual('');
+ expect(trace(state)).toEqual('');
});
});
@@ -23,7 +23,7 @@ describe('Logs Store getters', () => {
});
it('returns an empty string', () => {
- expect(getters.trace(state)).toEqual('');
+ expect(trace(state)).toEqual('');
});
});
@@ -33,7 +33,42 @@ describe('Logs Store getters', () => {
});
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);
});
});
});
diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb
index 2c8f76c8f34..9bba3eb2b77 100644
--- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb
@@ -46,6 +46,7 @@ describe Gitlab::Ci::Config::Entry::Reports do
:lsif | 'lsif.json'
:dotenv | 'build.dotenv'
:cobertura | 'cobertura-coverage.xml'
+ :terraform | 'tfplan.json'
end
with_them do
diff --git a/spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb b/spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb
new file mode 100644
index 00000000000..6f20b8877e0
--- /dev/null
+++ b/spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb
@@ -0,0 +1,28 @@
+# 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
diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb
index c2674638743..9788c9f4a3c 100644
--- a/spec/lib/gitlab/instrumentation_helper_spec.rb
+++ b/spec/lib/gitlab/instrumentation_helper_spec.rb
@@ -1,11 +1,51 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
require 'rspec-parameterized'
describe Gitlab::InstrumentationHelper do
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
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
diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
index bd04d30f85f..aab63ba88ad 100644
--- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
+++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
@@ -175,26 +175,30 @@ describe Gitlab::SidekiqLogging::StructuredLogger do
end
end
- context 'with Gitaly and Rugged calls' do
+ context 'with Gitaly, Rugged, and Redis calls' do
let(:timing_data) do
{
gitaly_calls: 10,
gitaly_duration: 10000,
rugged_calls: 1,
- rugged_duration_ms: 5000
+ rugged_duration_ms: 5000,
+ redis_calls: 3,
+ redis_duration_ms: 1234
}
end
- before do
- job.merge!(timing_data)
+ let(:expected_end_payload) do
+ end_payload.except('args').merge(timing_data)
end
it 'logs with Gitaly and Rugged timing data' do
Timecop.freeze(timestamp) do
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
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index de93c3c1675..6f6ff3704b4 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -140,6 +140,18 @@ describe Ci::JobArtifact do
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
subject { create(:ci_job_artifact, :archive) }
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index cff607a4731..5808d6e37e5 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -33,17 +33,34 @@ describe Milestone, 'Milestoneish' do
end
describe '#sorted_issues' do
- it 'sorts issues by label priority' do
+ before do
issue.labels << label_1
security_issue_1.labels << label_2
closed_issue_1.labels << label_3
+ end
+ it 'sorts issues by label priority' do
issues = milestone.sorted_issues(member)
expect(issues.first).to eq(issue)
expect(issues.second).to eq(security_issue_1)
expect(issues.third).not_to eq(closed_issue_1)
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
context 'attributes visibility' do
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 7c80b5231d1..cecd4f76fc5 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -149,9 +149,58 @@ describe Service do
end
end
- describe "Template" do
+ describe 'template' do
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
context 'when template is invalid' do
it 'sets service template to inactive when template is invalid' do
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 0ed4dcec93e..86b68dc3ade 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -34,6 +34,7 @@ describe Ci::RetryBuildService do
job_artifacts_container_scanning job_artifacts_dast
job_artifacts_license_management job_artifacts_license_scanning
job_artifacts_performance job_artifacts_lsif
+ job_artifacts_terraform
job_artifacts_codequality job_artifacts_metrics scheduled_at
job_variables waiting_for_resource_at job_artifacts_metrics_referee
job_artifacts_network_referee job_artifacts_dotenv
diff --git a/spec/support/shared_examples/uploaders/upload_type_shared_examples.rb b/spec/support/shared_examples/uploaders/upload_type_shared_examples.rb
index 81ac6bd94db..e58723324d3 100644
--- a/spec/support/shared_examples/uploaders/upload_type_shared_examples.rb
+++ b/spec/support/shared_examples/uploaders/upload_type_shared_examples.rb
@@ -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))
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
diff --git a/spec/uploaders/content_type_whitelist_spec.rb b/spec/uploaders/content_type_whitelist_spec.rb
index be519ead1c8..4689f83759d 100644
--- a/spec/uploaders/content_type_whitelist_spec.rb
+++ b/spec/uploaders/content_type_whitelist_spec.rb
@@ -18,6 +18,7 @@ describe ContentTypeWhitelist do
let(:path) { File.join('spec', 'fixtures', 'rails_sample.jpg') }
it_behaves_like 'accepted carrierwave upload'
+ it_behaves_like 'upload with content type', 'image/jpeg'
end
context 'upload non-whitelisted file content type' do
diff --git a/spec/uploaders/job_artifact_uploader_spec.rb b/spec/uploaders/job_artifact_uploader_spec.rb
index 60b5a6697b1..a03cf3b9dea 100644
--- a/spec/uploaders/job_artifact_uploader_spec.rb
+++ b/spec/uploaders/job_artifact_uploader_spec.rb
@@ -97,5 +97,12 @@ describe JobArtifactUploader do
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
+
+ # 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
diff --git a/vendor/gitignore/C++.gitignore b/vendor/gitignore/C++.gitignore
index 259148fa18f..259148fa18f 100644..100755
--- a/vendor/gitignore/C++.gitignore
+++ b/vendor/gitignore/C++.gitignore
diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore
index a1c2a238a96..a1c2a238a96 100644..100755
--- a/vendor/gitignore/Java.gitignore
+++ b/vendor/gitignore/Java.gitignore