Commit 37ff5631 authored by Simon Knox's avatar Simon Knox

Merge branch 'master' of gitlab.com:gitlab-org/gitlab into psi-filtered-vars

parents 0bfbb49c 6f114149
...@@ -43,16 +43,16 @@ code_quality: ...@@ -43,16 +43,16 @@ code_quality:
# We need to duplicate this job's definition because it seems it's impossible to # We need to duplicate this job's definition because it seems it's impossible to
# override an included `only.refs`. # override an included `only.refs`.
# See https://gitlab.com/gitlab-org/gitlab/issues/31371. # See https://gitlab.com/gitlab-org/gitlab/issues/31371.
# Once https://gitlab.com/gitlab-org/gitlab/merge_requests/16487 will be deployed .sast:
# to GitLab.com, we should be able to use the template and set SAST_DISABLE_DIND: "true".
sast:
extends: extends:
- .default-retry - .default-retry
- .reports:rules:sast - .reports:rules:sast
- .use-docker-in-docker - .use-docker-in-docker
stage: test stage: test
allow_failure: true # `needs: []` starts the job immediately in the pipeline
# https://docs.gitlab.com/ee/ci/yaml/README.html#needs
needs: [] needs: []
allow_failure: true
artifacts: artifacts:
paths: paths:
- gl-sast-report.json # GitLab-specific - gl-sast-report.json # GitLab-specific
...@@ -63,22 +63,39 @@ sast: ...@@ -63,22 +63,39 @@ sast:
# emptying DOCKER_HOST so it can be detected properly on kubernetes executor # emptying DOCKER_HOST so it can be detected properly on kubernetes executor
# with the script below # with the script below
DOCKER_HOST: "" DOCKER_HOST: ""
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
SAST_ANALYZER_IMAGE_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
SAST_ANALYZER_IMAGE_TAG: 2
SAST_BRAKEMAN_LEVEL: 2 # GitLab-specific SAST_BRAKEMAN_LEVEL: 2 # GitLab-specific
SAST_EXCLUDED_PATHS: qa,spec,doc,ee/spec # GitLab-specific SAST_EXCLUDED_PATHS: qa,spec,doc,ee/spec # GitLab-specific
script: script:
- export SAST_VERSION=${SP_VERSION:-$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')} - /analyzer run
- |
if ! docker info &>/dev/null; then brakeman-sast:
if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then extends: .sast
export DOCKER_HOST='tcp://localhost:2375' image:
fi name: "$SAST_ANALYZER_IMAGE_PREFIX/brakeman:$SAST_ANALYZER_IMAGE_TAG"
fi
- | eslint-sast:
ENVS=`printenv | grep -vE '^(DOCKER_|CI|GITLAB_|FF_|HOME|PWD|OLDPWD|PATH|SHLVL|HOSTNAME)' | sed -n '/^[^\t]/s/=.*//p' | sed '/^$/d' | sed 's/^/-e /g' | tr '\n' ' '` extends: .sast
docker run "$ENVS" \ image:
--volume "$PWD:/code" \ name: "$SAST_ANALYZER_IMAGE_PREFIX/eslint:$SAST_ANALYZER_IMAGE_TAG"
--volume /var/run/docker.sock:/var/run/docker.sock \
"registry.gitlab.com/gitlab-org/security-products/sast:$SAST_VERSION" /app/bin/run /code kubesec-sast:
extends: .sast
image:
name: "$SAST_ANALYZER_IMAGE_PREFIX/kubesec:$SAST_ANALYZER_IMAGE_TAG"
nodejs-scan-sast:
extends: .sast
image:
name: "$SAST_ANALYZER_IMAGE_PREFIX/nodejs-scan:$SAST_ANALYZER_IMAGE_TAG"
secrets-sast:
extends: .sast
image:
name: "$SAST_ANALYZER_IMAGE_PREFIX/secrets:$SAST_ANALYZER_IMAGE_TAG"
# We need to duplicate this job's definition because it seems it's impossible to # We need to duplicate this job's definition because it seems it's impossible to
# override an included `only.refs`. # override an included `only.refs`.
......
...@@ -20,12 +20,11 @@ build-qa-image: ...@@ -20,12 +20,11 @@ build-qa-image:
- time docker build --cache-from "${QA_MASTER_IMAGE}" --tag ${QA_IMAGE} --file ./qa/Dockerfile ./ - time docker build --cache-from "${QA_MASTER_IMAGE}" --tag ${QA_IMAGE} --file ./qa/Dockerfile ./
- time docker push ${QA_IMAGE} - time docker push ${QA_IMAGE}
review-cleanup: .review-cleanup-base:
extends: extends:
- .default-retry - .default-retry
- .review:rules:review-cleanup - .review:rules:review-cleanup
stage: prepare stage: prepare
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
allow_failure: true allow_failure: true
environment: environment:
name: review/auto-cleanup name: review/auto-cleanup
...@@ -36,6 +35,18 @@ review-cleanup: ...@@ -36,6 +35,18 @@ review-cleanup:
script: script:
- ruby -rrubygems scripts/review_apps/automated_cleanup.rb - ruby -rrubygems scripts/review_apps/automated_cleanup.rb
review-cleanup:
extends:
- .review-cleanup-base
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
review-cleanup-helm3:
extends:
- .review-cleanup-base
variables:
HELM_3: 1
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-helm3-kubectl1.14
review-gcp-cleanup: review-gcp-cleanup:
extends: extends:
- .review:rules:review-gcp-cleanup - .review:rules:review-gcp-cleanup
......
...@@ -409,7 +409,6 @@ linters: ...@@ -409,7 +409,6 @@ linters:
- 'ee/app/views/projects/push_rules/_index.html.haml' - 'ee/app/views/projects/push_rules/_index.html.haml'
- 'ee/app/views/projects/services/gitlab_slack_application/_help.html.haml' - 'ee/app/views/projects/services/gitlab_slack_application/_help.html.haml'
- 'ee/app/views/projects/services/gitlab_slack_application/_slack_integration_form.html.haml' - 'ee/app/views/projects/services/gitlab_slack_application/_slack_integration_form.html.haml'
- 'ee/app/views/projects/services/prometheus/_metrics.html.haml'
- 'ee/app/views/projects/settings/slacks/edit.html.haml' - 'ee/app/views/projects/settings/slacks/edit.html.haml'
- 'ee/app/views/shared/_additional_email_text.html.haml' - 'ee/app/views/shared/_additional_email_text.html.haml'
- 'ee/app/views/shared/_mirror_update_button.html.haml' - 'ee/app/views/shared/_mirror_update_button.html.haml'
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
/* eslint-disable vue/require-default-prop */ /* eslint-disable vue/require-default-prop */
import IssueCardInner from './issue_card_inner.vue'; import IssueCardInner from './issue_card_inner.vue';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import sidebarEventHub from '~/sidebar/event_hub';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
export default { export default {
...@@ -73,6 +74,11 @@ export default { ...@@ -73,6 +74,11 @@ export default {
showIssue(e) { showIssue(e) {
if (e.target.classList.contains('js-no-trigger')) return; if (e.target.classList.contains('js-no-trigger')) return;
// If no issues are opened, close all sidebars first
if (!boardsStore.detail?.issue?.id) {
sidebarEventHub.$emit('sidebar.closeAll');
}
// If CMD or CTRL is clicked // If CMD or CTRL is clicked
const isMultiSelect = this.canMultiSelect && (e.ctrlKey || e.metaKey); const isMultiSelect = this.canMultiSelect && (e.ctrlKey || e.metaKey);
......
...@@ -103,12 +103,14 @@ export default Vue.extend({ ...@@ -103,12 +103,14 @@ export default Vue.extend({
eventHub.$on('sidebar.addAssignee', this.addAssignee); eventHub.$on('sidebar.addAssignee', this.addAssignee);
eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees); eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees);
eventHub.$on('sidebar.saveAssignees', this.saveAssignees); eventHub.$on('sidebar.saveAssignees', this.saveAssignees);
eventHub.$on('sidebar.closeAll', this.closeSidebar);
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('sidebar.removeAssignee', this.removeAssignee); eventHub.$off('sidebar.removeAssignee', this.removeAssignee);
eventHub.$off('sidebar.addAssignee', this.addAssignee); eventHub.$off('sidebar.addAssignee', this.addAssignee);
eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees); eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
eventHub.$off('sidebar.saveAssignees', this.saveAssignees); eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
eventHub.$off('sidebar.closeAll', this.closeSidebar);
}, },
mounted() { mounted() {
new IssuableContext(this.currentUser); new IssuableContext(this.currentUser);
......
...@@ -112,7 +112,6 @@ export default { ...@@ -112,7 +112,6 @@ export default {
mergeRequestDiffs: state => state.diffs.mergeRequestDiffs, mergeRequestDiffs: state => state.diffs.mergeRequestDiffs,
mergeRequestDiff: state => state.diffs.mergeRequestDiff, mergeRequestDiff: state => state.diffs.mergeRequestDiff,
commit: state => state.diffs.commit, commit: state => state.diffs.commit,
targetBranchName: state => state.diffs.targetBranchName,
renderOverflowWarning: state => state.diffs.renderOverflowWarning, renderOverflowWarning: state => state.diffs.renderOverflowWarning,
numTotalFiles: state => state.diffs.realSize, numTotalFiles: state => state.diffs.realSize,
numVisibleFiles: state => state.diffs.size, numVisibleFiles: state => state.diffs.size,
...@@ -123,19 +122,9 @@ export default { ...@@ -123,19 +122,9 @@ export default {
...mapState('diffs', ['showTreeList', 'isLoading', 'startVersion']), ...mapState('diffs', ['showTreeList', 'isLoading', 'startVersion']),
...mapGetters('diffs', ['isParallelView', 'currentDiffIndex']), ...mapGetters('diffs', ['isParallelView', 'currentDiffIndex']),
...mapGetters(['isNotesFetched', 'getNoteableData']), ...mapGetters(['isNotesFetched', 'getNoteableData']),
targetBranch() {
return {
branchName: this.targetBranchName,
versionIndex: -1,
path: '',
};
},
canCurrentUserFork() { canCurrentUserFork() {
return this.currentUser.can_fork === true && this.currentUser.can_create_merge_request; return this.currentUser.can_fork === true && this.currentUser.can_create_merge_request;
}, },
showCompareVersions() {
return this.mergeRequestDiffs && this.mergeRequestDiff;
},
renderDiffFiles() { renderDiffFiles() {
return ( return (
this.diffFiles.length > 0 || this.diffFiles.length > 0 ||
...@@ -369,8 +358,6 @@ export default { ...@@ -369,8 +358,6 @@ export default {
<div v-else id="diffs" :class="{ active: shouldShow }" class="diffs tab-pane"> <div v-else id="diffs" :class="{ active: shouldShow }" class="diffs tab-pane">
<compare-versions <compare-versions
:merge-request-diffs="mergeRequestDiffs" :merge-request-diffs="mergeRequestDiffs"
:merge-request-diff="mergeRequestDiff"
:target-branch="targetBranch"
:is-limited-container="isLimitedContainer" :is-limited-container="isLimitedContainer"
:diff-files-length="diffFilesLength" :diff-files-length="diffFilesLength"
/> />
......
<script> <script>
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { n__, __, sprintf } from '~/locale';
import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
export default { export default {
...@@ -10,98 +8,14 @@ export default { ...@@ -10,98 +8,14 @@ export default {
TimeAgo, TimeAgo,
}, },
props: { props: {
otherVersions: { versions: {
type: Array, type: Array,
required: false, required: true,
default: () => [],
},
mergeRequestVersion: {
type: Object,
required: false,
default: null,
},
startVersion: {
type: Object,
required: false,
default: null,
},
targetBranch: {
type: Object,
required: false,
default: null,
},
showCommitCount: {
type: Boolean,
required: false,
default: false,
},
baseVersionPath: {
type: String,
required: false,
default: null,
}, },
}, },
computed: { computed: {
targetVersions() {
if (this.mergeRequestVersion) {
return this.otherVersions;
}
return [...this.otherVersions, this.targetBranch];
},
selectedVersionName() { selectedVersionName() {
const selectedVersion = this.startVersion || this.targetBranch || this.mergeRequestVersion; return this.versions.find(x => x.selected)?.versionName || '';
return this.versionName(selectedVersion);
},
},
methods: {
commitsText(version) {
return n__(`%d commit,`, `%d commits,`, version.commits_count);
},
href(version) {
if (this.isBase(version)) {
return this.baseVersionPath;
}
if (this.showCommitCount) {
return version.version_path;
}
return version.compare_path;
},
versionName(version) {
if (this.isLatest(version)) {
return __('latest version');
}
if (this.targetBranch && (this.isBase(version) || !version)) {
return this.targetBranch.branchName;
}
return sprintf(__(`version %{versionIndex}`), { versionIndex: version.version_index });
},
isActive(version) {
if (!version) {
return false;
}
if (this.targetBranch) {
return (
(this.isBase(version) && !this.startVersion) ||
(this.startVersion && this.startVersion.version_index === version.version_index)
);
}
return version.version_index === this.mergeRequestVersion.version_index;
},
isBase(version) {
if (!version || !this.targetBranch) {
return false;
}
return version.versionIndex === -1;
},
isHead() {
return parseBoolean(getParameterByName('diff_head'));
},
isLatest(version) {
return (
this.mergeRequestVersion && version.version_index === this.targetVersions[0].version_index
);
}, },
}, },
}; };
...@@ -120,13 +34,15 @@ export default { ...@@ -120,13 +34,15 @@ export default {
<div class="dropdown-menu dropdown-select dropdown-menu-selectable"> <div class="dropdown-menu dropdown-select dropdown-menu-selectable">
<div class="dropdown-content"> <div class="dropdown-content">
<ul> <ul>
<li v-for="version in targetVersions" :key="version.id"> <li v-for="version in versions" :key="version.id">
<a :class="{ 'is-active': isActive(version) }" :href="href(version)"> <a :class="{ 'is-active': version.selected }" :href="version.href">
<div> <div>
<strong> <strong>
{{ versionName(version) }} {{ version.versionName }}
<template v-if="isHead()">{{ s__('DiffsCompareBaseBranch|(HEAD)') }}</template> <template v-if="version.isHead">{{
<template v-else-if="isBase(version)">{{ s__('DiffsCompareBaseBranch|(HEAD)')
}}</template>
<template v-else-if="version.isBase">{{
s__('DiffsCompareBaseBranch|(base)') s__('DiffsCompareBaseBranch|(base)')
}}</template> }}</template>
</strong> </strong>
...@@ -136,8 +52,8 @@ export default { ...@@ -136,8 +52,8 @@ export default {
</div> </div>
<div> <div>
<small> <small>
<template v-if="showCommitCount"> <template v-if="version.commitsText">
{{ commitsText(version) }} {{ version.commitsText }}
</template> </template>
<time-ago <time-ago
v-if="version.created_at" v-if="version.created_at"
......
...@@ -4,14 +4,14 @@ import { GlTooltipDirective, GlLink, GlDeprecatedButton, GlSprintf } from '@gitl ...@@ -4,14 +4,14 @@ import { GlTooltipDirective, GlLink, GlDeprecatedButton, GlSprintf } from '@gitl
import { __ } from '~/locale'; import { __ } from '~/locale';
import { polyfillSticky } from '~/lib/utils/sticky'; import { polyfillSticky } from '~/lib/utils/sticky';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import CompareVersionsDropdown from './compare_versions_dropdown.vue'; import CompareDropdownLayout from './compare_dropdown_layout.vue';
import SettingsDropdown from './settings_dropdown.vue'; import SettingsDropdown from './settings_dropdown.vue';
import DiffStats from './diff_stats.vue'; import DiffStats from './diff_stats.vue';
import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants'; import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants';
export default { export default {
components: { components: {
CompareVersionsDropdown, CompareDropdownLayout,
Icon, Icon,
GlLink, GlLink,
GlDeprecatedButton, GlDeprecatedButton,
...@@ -27,16 +27,6 @@ export default { ...@@ -27,16 +27,6 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
mergeRequestDiff: {
type: Object,
required: false,
default: () => ({}),
},
targetBranch: {
type: Object,
required: false,
default: null,
},
isLimitedContainer: { isLimitedContainer: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -48,7 +38,11 @@ export default { ...@@ -48,7 +38,11 @@ export default {
}, },
}, },
computed: { computed: {
...mapGetters('diffs', ['hasCollapsedFile']), ...mapGetters('diffs', [
'hasCollapsedFile',
'diffCompareDropdownTargetVersions',
'diffCompareDropdownSourceVersions',
]),
...mapState('diffs', [ ...mapState('diffs', [
'commit', 'commit',
'showTreeList', 'showTreeList',
...@@ -57,18 +51,12 @@ export default { ...@@ -57,18 +51,12 @@ export default {
'addedLines', 'addedLines',
'removedLines', 'removedLines',
]), ]),
comparableDiffs() {
return this.mergeRequestDiffs.slice(1);
},
showDropdowns() { showDropdowns() {
return !this.commit && this.mergeRequestDiffs.length; return !this.commit && this.mergeRequestDiffs.length;
}, },
toggleFileBrowserTitle() { toggleFileBrowserTitle() {
return this.showTreeList ? __('Hide file browser') : __('Show file browser'); return this.showTreeList ? __('Hide file browser') : __('Show file browser');
}, },
baseVersionPath() {
return this.mergeRequestDiff.base_version_path;
},
}, },
created() { created() {
this.CENTERED_LIMITED_CONTAINER_CLASSES = CENTERED_LIMITED_CONTAINER_CLASSES; this.CENTERED_LIMITED_CONTAINER_CLASSES = CENTERED_LIMITED_CONTAINER_CLASSES;
...@@ -113,19 +101,14 @@ export default { ...@@ -113,19 +101,14 @@ export default {
:message="s__('MergeRequest|Compare %{source} and %{target}')" :message="s__('MergeRequest|Compare %{source} and %{target}')"
> >
<template #source> <template #source>
<compare-versions-dropdown <compare-dropdown-layout
:other-versions="mergeRequestDiffs" :versions="diffCompareDropdownSourceVersions"
:merge-request-version="mergeRequestDiff"
:show-commit-count="true"
class="mr-version-dropdown" class="mr-version-dropdown"
/> />
</template> </template>
<template #target> <template #target>
<compare-versions-dropdown <compare-dropdown-layout
:other-versions="comparableDiffs" :versions="diffCompareDropdownTargetVersions"
:base-version-path="baseVersionPath"
:start-version="startVersion"
:target-branch="targetBranch"
class="mr-version-compare-dropdown" class="mr-version-compare-dropdown"
/> />
</template> </template>
......
...@@ -58,3 +58,5 @@ export const START_RENDERING_INDEX = 200; ...@@ -58,3 +58,5 @@ export const START_RENDERING_INDEX = 200;
export const INLINE_DIFF_LINES_KEY = 'highlighted_diff_lines'; export const INLINE_DIFF_LINES_KEY = 'highlighted_diff_lines';
export const PARALLEL_DIFF_LINES_KEY = 'parallel_diff_lines'; export const PARALLEL_DIFF_LINES_KEY = 'parallel_diff_lines';
export const DIFFS_PER_PAGE = 20; export const DIFFS_PER_PAGE = 20;
export const DIFF_COMPARE_BASE_VERSION_INDEX = -1;
import { __, n__ } from '~/locale'; import { __, n__ } from '~/locale';
import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants'; import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants';
export * from './getters_versions_dropdowns';
export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW_TYPE; export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW_TYPE;
export const isInlineView = state => state.diffViewType === INLINE_DIFF_VIEW_TYPE; export const isInlineView = state => state.diffViewType === INLINE_DIFF_VIEW_TYPE;
......
import { __, n__, sprintf } from '~/locale';
import { DIFF_COMPARE_BASE_VERSION_INDEX } from '../constants';
export const selectedTargetIndex = state =>
state.startVersion?.version_index || DIFF_COMPARE_BASE_VERSION_INDEX;
export const selectedSourceIndex = state => state.mergeRequestDiff.version_index;
export const diffCompareDropdownTargetVersions = (state, getters) => {
// startVersion only exists if the user has selected a version other
// than "base" so if startVersion is null then base must be selected
const baseVersion = {
versionName: state.targetBranchName,
version_index: DIFF_COMPARE_BASE_VERSION_INDEX,
href: state.mergeRequestDiff.base_version_path,
isBase: true,
selected: !state.startVersion,
};
// Appended properties here are to make the compare_dropdown_layout easier to reason about
const formatVersion = v => {
return {
href: v.compare_path,
versionName: sprintf(__(`version %{versionIndex}`), { versionIndex: v.version_index }),
selected: v.version_index === getters.selectedTargetIndex,
...v,
};
};
return [...state.mergeRequestDiffs.slice(1).map(formatVersion), baseVersion];
};
export const diffCompareDropdownSourceVersions = (state, getters) => {
// Appended properties here are to make the compare_dropdown_layout easier to reason about
return state.mergeRequestDiffs.map((v, i) => ({
...v,
href: v.version_path,
commitsText: n__(`%d commit,`, `%d commits,`, v.commits_count),
versionName:
i === 0
? __('latest version')
: sprintf(__(`version %{versionIndex}`), { versionIndex: v.version_index }),
selected: v.version_index === getters.selectedSourceIndex,
}));
};
...@@ -15,7 +15,7 @@ export default () => ({ ...@@ -15,7 +15,7 @@ export default () => ({
endpoint: '', endpoint: '',
basePath: '', basePath: '',
commit: null, commit: null,
startVersion: null, startVersion: null, // Null unless a target diff is selected for comparison that is not the "base" diff
diffFiles: [], diffFiles: [],
coverageFiles: {}, coverageFiles: {},
mergeRequestDiffs: [], mergeRequestDiffs: [],
......
...@@ -326,6 +326,7 @@ export default { ...@@ -326,6 +326,7 @@ export default {
}, },
[types.SET_SHOW_WHITESPACE](state, showWhitespace) { [types.SET_SHOW_WHITESPACE](state, showWhitespace) {
state.showWhitespace = showWhitespace; state.showWhitespace = showWhitespace;
state.diffFiles = [];
}, },
[types.TOGGLE_FILE_FINDER_VISIBLE](state, visible) { [types.TOGGLE_FILE_FINDER_VISIBLE](state, visible) {
state.fileFinderVisible = visible; state.fileFinderVisible = visible;
......
...@@ -5,6 +5,7 @@ import { highCountTrim } from '~/lib/utils/text_utility'; ...@@ -5,6 +5,7 @@ import { highCountTrim } from '~/lib/utils/text_utility';
import SetStatusModalTrigger from './set_status_modal/set_status_modal_trigger.vue'; import SetStatusModalTrigger from './set_status_modal/set_status_modal_trigger.vue';
import SetStatusModalWrapper from './set_status_modal/set_status_modal_wrapper.vue'; import SetStatusModalWrapper from './set_status_modal/set_status_modal_wrapper.vue';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import Tracking from '~/tracking';
/** /**
* Updates todo counter when todos are toggled. * Updates todo counter when todos are toggled.
...@@ -73,6 +74,24 @@ function initStatusTriggers() { ...@@ -73,6 +74,24 @@ function initStatusTriggers() {
} }
} }
export function initNavUserDropdownTracking() {
const el = document.querySelector('.js-nav-user-dropdown');
const buyEl = document.querySelector('.js-buy-ci-minutes-link');
if (el && buyEl) {
const { trackLabel, trackProperty } = buyEl.dataset;
const trackEvent = 'show_buy_ci_minutes';
$(el).on('shown.bs.dropdown', () => {
Tracking.event(undefined, trackEvent, {
label: trackLabel,
property: trackProperty,
});
});
}
}
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
requestIdleCallback(initStatusTriggers); requestIdleCallback(initStatusTriggers);
initNavUserDropdownTracking();
}); });
/**
* @param {String} queryLabel - Default query label for chart
* @param {Object} metricAttributes - Default metric attribute values (e.g. method, instance)
* @returns {String} The formatted query label
* @example
* singleAttributeLabel('app', {__name__: "up", app: "prometheus"}) -> "app: prometheus"
*/
const singleAttributeLabel = (queryLabel, metricAttributes) => {
if (!queryLabel) return '';
const relevantAttribute = queryLabel.toLowerCase().replace(' ', '_');
const value = metricAttributes[relevantAttribute];
if (!value) return '';
return `${queryLabel}: ${value}`;
};
/**
* @param {String} queryLabel - Default query label for chart
* @param {Object} metricAttributes - Default metric attribute values (e.g. method, instance)
* @returns {String} The formatted query label
* @example
* templatedLabel('__name__', {__name__: "up", app: "prometheus"}) -> "__name__"
*/
const templatedLabel = (queryLabel, metricAttributes) => {
if (!queryLabel) return '';
// eslint-disable-next-line array-callback-return
Object.entries(metricAttributes).map(([templateVar, label]) => {
const regex = new RegExp(`{{\\s*${templateVar}\\s*}}`, 'g');
// eslint-disable-next-line no-param-reassign
queryLabel = queryLabel.replace(regex, label);
});
return queryLabel;
};
/**
* @param {Object} metricAttributes - Default metric attribute values (e.g. method, instance)
* @returns {String} The formatted query label
* @example
* multiMetricLabel('', {__name__: "up", app: "prometheus"}) -> "__name__: up, app: prometheus"
*/
const multiMetricLabel = metricAttributes => {
return Object.entries(metricAttributes)
.map(([templateVar, label]) => `${templateVar}: ${label}`)
.join(', ');
};
/**
* @param {String} queryLabel - Default query label for chart
* @param {Object} metricAttributes - Default metric attribute values (e.g. method, instance)
* @returns {String} The formatted query label
*/
const getSeriesLabel = (queryLabel, metricAttributes) => {
return (
singleAttributeLabel(queryLabel, metricAttributes) ||
templatedLabel(queryLabel, metricAttributes) ||
multiMetricLabel(metricAttributes) ||
queryLabel
);
};
/** /**
* @param {Array} queryResults - Array of Result objects * @param {Array} queryResults - Array of Result objects
* @param {Object} defaultConfig - Default chart config values (e.g. lineStyle, name) * @param {Object} defaultConfig - Default chart config values (e.g. lineStyle, name)
...@@ -12,21 +72,11 @@ export const makeDataSeries = (queryResults, defaultConfig) => ...@@ -12,21 +72,11 @@ export const makeDataSeries = (queryResults, defaultConfig) =>
if (!data.length) { if (!data.length) {
return null; return null;
} }
const relevantMetric = defaultConfig.name.toLowerCase().replace(' ', '_');
const name = result.metric[relevantMetric];
const series = { data }; const series = { data };
if (name) { return {
series.name = `${defaultConfig.name}: ${name}`; ...defaultConfig,
} else { ...series,
series.name = defaultConfig.name; name: getSeriesLabel(defaultConfig.name, result.metric),
Object.keys(result.metric).forEach(templateVar => { };
const value = result.metric[templateVar];
const regex = new RegExp(`{{\\s*${templateVar}\\s*}}`, 'g');
series.name = series.name.replace(regex, value);
});
}
return { ...defaultConfig, ...series };
}) })
.filter(series => series !== null); .filter(series => series !== null);
...@@ -25,7 +25,7 @@ export default { ...@@ -25,7 +25,7 @@ export default {
return Promise.resolve(file.content); return Promise.resolve(file.content);
} }
if (file.raw) { if (file.raw || !file.rawPath) {
return Promise.resolve(file.raw); return Promise.resolve(file.raw);
} }
......
<script> <script>
import getJiraProjects from '../queries/getJiraProjects.query.graphql'; import getJiraProjects from '../queries/getJiraProjects.query.graphql';
import JiraImportForm from './jira_import_form.vue';
import JiraImportSetup from './jira_import_setup.vue';
export default { export default {
name: 'JiraImportApp', name: 'JiraImportApp',
components: {
JiraImportForm,
JiraImportSetup,
},
props: { props: {
isJiraConfigured: {
type: Boolean,
required: true,
},
projectPath: { projectPath: {
type: String, type: String,
required: true, required: true,
}, },
setupIllustration: {
type: String,
required: true,
},
}, },
apollo: { apollo: {
getJiraImports: { getJiraImports: {
...@@ -18,11 +32,17 @@ export default { ...@@ -18,11 +32,17 @@ export default {
}; };
}, },
update: data => data.project.jiraImports, update: data => data.project.jiraImports,
skip() {
return !this.isJiraConfigured;
},
}, },
}, },
}; };
</script> </script>
<template> <template>
<div></div> <div>
<jira-import-setup v-if="!isJiraConfigured" :illustration="setupIllustration" />
<jira-import-form v-else />
</div>
</template> </template>
<script>
import { GlAvatar, GlNewButton, GlFormGroup, GlFormSelect, GlLabel } from '@gitlab/ui';
export default {
name: 'JiraImportForm',
components: {
GlAvatar,
GlNewButton,
GlFormGroup,
GlFormSelect,
GlLabel,
},
currentUserAvatarUrl: gon.current_user_avatar_url,
currentUsername: gon.current_username,
};
</script>
<template>
<div>
<h3 class="page-title">{{ __('New Jira import') }}</h3>
<hr />
<form>
<gl-form-group
class="row align-items-center"
:label="__('Import from')"
label-cols-sm="2"
label-for="jira-project-select"
>
<gl-form-select id="jira-project-select" class="mb-2" />
</gl-form-group>
<gl-form-group
class="row align-items-center"
:label="__('Issue label')"
label-cols-sm="2"
label-for="jira-project-label"
>
<gl-label
id="jira-project-label"
class="mb-2"
background-color="#428BCA"
title="jira-import::KEY-1"
scoped
/>
</gl-form-group>
<hr />
<p class="offset-md-1">
{{
__(
"For each Jira issue successfully imported, we'll create a new GitLab issue with the following data:",
)
}}
</p>
<gl-form-group
class="row align-items-center mb-1"
:label="__('Title')"
label-cols-sm="2"
label-for="jira-project-title"
>
<p id="jira-project-title" class="mb-2">{{ __('jira.issue.summary') }}</p>
</gl-form-group>
<gl-form-group
class="row align-items-center mb-1"
:label="__('Reporter')"
label-cols-sm="2"
label-for="jira-project-reporter"
>
<gl-avatar
id="jira-project-reporter"
class="mb-2"
:src="$options.currentUserAvatarUrl"
:size="24"
:aria-label="$options.currentUsername"
/>
</gl-form-group>
<gl-form-group
class="row align-items-center mb-1"
:label="__('Description')"
label-cols-sm="2"
label-for="jira-project-description"
>
<p id="jira-project-description" class="mb-2">{{ __('jira.issue.description.content') }}</p>
</gl-form-group>
<div class="footer-block row-content-block d-flex justify-content-between">
<gl-new-button category="primary" variant="success">{{ __('Next') }}</gl-new-button>
<gl-new-button>{{ __('Cancel') }}</gl-new-button>
</div>
</form>
</div>
</template>
<script>
export default {
name: 'JiraImportSetup',
props: {
illustration: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="empty-state">
<div class="svg-content">
<img :src="illustration" :alt="__('Set up Jira Integration illustration')" />
</div>
<div class="text-content d-flex flex-column align-items-center">
<p>{{ __('You will first need to set up Jira Integration to use this feature.') }}</p>
<a class="btn btn-success" href="../services/jira/edit">
{{ __('Set up Jira Integration') }}
</a>
</div>
</div>
</template>
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import App from './components/jira_import_app.vue'; import App from './components/jira_import_app.vue';
Vue.use(VueApollo); Vue.use(VueApollo);
...@@ -23,7 +24,9 @@ export default function mountJiraImportApp() { ...@@ -23,7 +24,9 @@ export default function mountJiraImportApp() {
render(createComponent) { render(createComponent) {
return createComponent(App, { return createComponent(App, {
props: { props: {
isJiraConfigured: parseBoolean(el.dataset.isJiraConfigured),
projectPath: el.dataset.projectPath, projectPath: el.dataset.projectPath,
setupIllustration: el.dataset.setupIllustration,
}, },
}); });
}, },
......
...@@ -87,11 +87,17 @@ export const generateAnnotationsSeries = ({ deployments = [], annotations = [] } ...@@ -87,11 +87,17 @@ export const generateAnnotationsSeries = ({ deployments = [], annotations = [] }
return { return {
name: 'deployments', name: 'deployments',
value: [deployment.createdAt, annotationsYAxisCoords.pos], value: [deployment.createdAt, annotationsYAxisCoords.pos],
// style options
symbol: deployment.icon, symbol: deployment.icon,
symbolSize: symbolSizes.default, symbolSize: symbolSizes.default,
itemStyle: { itemStyle: {
color: deployment.color, color: deployment.color,
}, },
// metadata that are accessible in `formatTooltipText` method
tooltipData: {
sha: deployment.sha.substring(0, 8),
commitUrl: deployment.commitUrl,
},
}; };
}); });
...@@ -100,8 +106,12 @@ export const generateAnnotationsSeries = ({ deployments = [], annotations = [] } ...@@ -100,8 +106,12 @@ export const generateAnnotationsSeries = ({ deployments = [], annotations = [] }
return { return {
name: 'annotations', name: 'annotations',
value: [annotation.from, annotationsYAxisCoords.pos], value: [annotation.from, annotationsYAxisCoords.pos],
// style options
symbol: 'none', symbol: 'none',
description: annotation.description, // metadata that are accessible in `formatTooltipText` method
tooltipData: {
description: annotation.description,
},
}; };
}); });
......
...@@ -58,7 +58,7 @@ export default { ...@@ -58,7 +58,7 @@ export default {
}, },
methods: { methods: {
formatLegendLabel(query) { formatLegendLabel(query) {
return `${query.label}`; return query.label;
}, },
onResize() { onResize() {
if (!this.$refs.barChart) return; if (!this.$refs.barChart) return;
......
...@@ -76,7 +76,7 @@ export default { ...@@ -76,7 +76,7 @@ export default {
}, },
methods: { methods: {
formatLegendLabel(query) { formatLegendLabel(query) {
return `${query.label}`; return query.label;
}, },
onResize() { onResize() {
if (!this.$refs.columnChart) return; if (!this.$refs.columnChart) return;
......
...@@ -251,7 +251,7 @@ export default { ...@@ -251,7 +251,7 @@ export default {
}, },
methods: { methods: {
formatLegendLabel(query) { formatLegendLabel(query) {
return `${query.label}`; return query.label;
}, },
isTooltipOfType(tooltipType, defaultType) { isTooltipOfType(tooltipType, defaultType) {
return tooltipType === defaultType; return tooltipType === defaultType;
...@@ -262,19 +262,17 @@ export default { ...@@ -262,19 +262,17 @@ export default {
params.seriesData.forEach(dataPoint => { params.seriesData.forEach(dataPoint => {
if (dataPoint.value) { if (dataPoint.value) {
const [xVal, yVal] = dataPoint.value; const [, yVal] = dataPoint.value;
this.tooltip.type = dataPoint.name; this.tooltip.type = dataPoint.name;
if (this.isTooltipOfType(this.tooltip.type, this.$options.tooltipTypes.deployments)) { if (this.isTooltipOfType(this.tooltip.type, this.$options.tooltipTypes.deployments)) {
const [deploy] = this.recentDeployments.filter( const { data = {} } = dataPoint;
deployment => deployment.createdAt === xVal, this.tooltip.sha = data?.tooltipData?.sha;
); this.tooltip.commitUrl = data?.tooltipData?.commitUrl;
this.tooltip.sha = deploy.sha.substring(0, 8);
this.tooltip.commitUrl = deploy.commitUrl;
} else if ( } else if (
this.isTooltipOfType(this.tooltip.type, this.$options.tooltipTypes.annotations) this.isTooltipOfType(this.tooltip.type, this.$options.tooltipTypes.annotations)
) { ) {
const { data } = dataPoint; const { data } = dataPoint;
this.tooltip.content.push(data?.description); this.tooltip.content.push(data?.tooltipData?.description);
} else { } else {
const { seriesName, color, dataIndex } = dataPoint; const { seriesName, color, dataIndex } = dataPoint;
......
...@@ -68,12 +68,11 @@ export const parseEnvironmentsResponse = (response = [], projectPath) => ...@@ -68,12 +68,11 @@ export const parseEnvironmentsResponse = (response = [], projectPath) =>
* https://gitlab.com/gitlab-org/gitlab/issues/207198 * https://gitlab.com/gitlab-org/gitlab/issues/207198
* *
* @param {Array} metrics - Array of prometheus metrics * @param {Array} metrics - Array of prometheus metrics
* @param {String} defaultLabel - Default label for metrics
* @returns {Object} * @returns {Object}
*/ */
const mapToMetricsViewModel = (metrics, defaultLabel) => const mapToMetricsViewModel = metrics =>
metrics.map(({ label, id, metric_id, query_range, prometheus_endpoint_path, ...metric }) => ({ metrics.map(({ label, id, metric_id, query_range, prometheus_endpoint_path, ...metric }) => ({
label: label || defaultLabel, label,
queryRange: query_range, queryRange: query_range,
prometheusEndpointPath: prometheus_endpoint_path, prometheusEndpointPath: prometheus_endpoint_path,
metricId: uniqMetricsId({ metric_id, id }), metricId: uniqMetricsId({ metric_id, id }),
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import GitlabTeamMemberBadge from '~/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue';
export default { export default {
components: { components: {
timeAgoTooltip, timeAgoTooltip,
GitlabTeamMemberBadge,
}, },
props: { props: {
author: { author: {
...@@ -48,6 +50,9 @@ export default { ...@@ -48,6 +50,9 @@ export default {
hasAuthor() { hasAuthor() {
return this.author && Object.keys(this.author).length; return this.author && Object.keys(this.author).length;
}, },
showGitlabTeamMemberBadge() {
return this.author?.is_gitlab_employee;
},
}, },
methods: { methods: {
...mapActions(['setTargetNoteHash']), ...mapActions(['setTargetNoteHash']),
...@@ -73,19 +78,21 @@ export default { ...@@ -73,19 +78,21 @@ export default {
{{ __('Toggle thread') }} {{ __('Toggle thread') }}
</button> </button>
</div> </div>
<a <template v-if="hasAuthor">
v-if="hasAuthor" <a
v-once v-once
:href="author.path" :href="author.path"
class="js-user-link" class="js-user-link"
:data-user-id="author.id" :data-user-id="author.id"
:data-username="author.username" :data-username="author.username"
> >
<slot name="note-header-info"></slot> <slot name="note-header-info"></slot>
<span class="note-header-author-name bold">{{ author.name }}</span> <span class="note-header-author-name bold">{{ author.name }}</span>
<span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span> <span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span>
<span class="note-headline-light">@{{ author.username }}</span> <span class="note-headline-light">@{{ author.username }}</span>
</a> </a>
<gitlab-team-member-badge v-if="showGitlabTeamMemberBadge" />
</template>
<span v-else>{{ __('A deleted user') }}</span> <span v-else>{{ __('A deleted user') }}</span>
<span class="note-headline-light note-headline-meta"> <span class="note-headline-light note-headline-meta">
<span class="system-note-message"> <slot></slot> </span> <span class="system-note-message"> <slot></slot> </span>
......
import IntegrationSettingsForm from '~/integrations/integration_settings_form'; import IntegrationSettingsForm from '~/integrations/integration_settings_form';
import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics'; import PrometheusMetrics from '~/prometheus_metrics/custom_metrics';
import PrometheusAlerts from '~/prometheus_alerts'; import PrometheusAlerts from '~/prometheus_alerts';
import initAlertsSettings from '~/alerts_service_settings'; import initAlertsSettings from '~/alerts_service_settings';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const prometheusSettingsWrapper = document.querySelector('.js-prometheus-metrics-monitoring');
const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
integrationSettingsForm.init(); integrationSettingsForm.init();
const prometheusSettingsSelector = '.js-prometheus-metrics-monitoring';
const prometheusSettingsWrapper = document.querySelector(prometheusSettingsSelector);
if (prometheusSettingsWrapper) { if (prometheusSettingsWrapper) {
const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring'); const prometheusMetrics = new PrometheusMetrics(prometheusSettingsSelector);
prometheusMetrics.loadActiveMetrics(); prometheusMetrics.init();
} }
PrometheusAlerts(); PrometheusAlerts();
......
...@@ -2,4 +2,5 @@ export default { ...@@ -2,4 +2,5 @@ export default {
EMPTY: 'empty', EMPTY: 'empty',
LOADING: 'loading', LOADING: 'loading',
LIST: 'list', LIST: 'list',
NO_INTEGRATION: 'no-integration',
}; };
import $ from 'jquery'; import $ from 'jquery';
import { escape, sortBy } from 'lodash'; import { escape, sortBy } from 'lodash';
import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics'; import PrometheusMetrics from './prometheus_metrics';
import PANEL_STATE from './constants'; import PANEL_STATE from './constants';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
export default class EEPrometheusMetrics extends PrometheusMetrics { export default class CustomMetrics extends PrometheusMetrics {
constructor(wrapperSelector) { constructor(wrapperSelector) {
super(wrapperSelector); super(wrapperSelector);
this.customMetrics = []; this.customMetrics = [];
...@@ -55,6 +55,14 @@ export default class EEPrometheusMetrics extends PrometheusMetrics { ...@@ -55,6 +55,14 @@ export default class EEPrometheusMetrics extends PrometheusMetrics {
this.isServiceActive = this.$monitoredCustomMetricsPanel.data('service-active'); this.isServiceActive = this.$monitoredCustomMetricsPanel.data('service-active');
} }
init() {
if (this.isServiceActive) {
this.loadActiveCustomMetrics();
} else {
this.setNoIntegrationActiveState();
}
}
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
setHidden(els) { setHidden(els) {
els.forEach(el => el.addClass('hidden')); els.forEach(el => el.addClass('hidden'));
...@@ -98,7 +106,7 @@ export default class EEPrometheusMetrics extends PrometheusMetrics { ...@@ -98,7 +106,7 @@ export default class EEPrometheusMetrics extends PrometheusMetrics {
const sortedMetrics = sortBy(this.customMetrics.map(capitalizeGroup), ['group', 'title']); const sortedMetrics = sortBy(this.customMetrics.map(capitalizeGroup), ['group', 'title']);
sortedMetrics.forEach(metric => { sortedMetrics.forEach(metric => {
this.$monitoredCustomMetricsList.append(EEPrometheusMetrics.customMetricTemplate(metric)); this.$monitoredCustomMetricsList.append(CustomMetrics.customMetricTemplate(metric));
}); });
this.$monitoredCustomMetricsCount.text(this.customMetrics.length); this.$monitoredCustomMetricsCount.text(this.customMetrics.length);
......
...@@ -28,6 +28,10 @@ export default class PrometheusMetrics { ...@@ -28,6 +28,10 @@ export default class PrometheusMetrics {
this.$panelToggle.on('click', e => this.handlePanelToggle(e)); this.$panelToggle.on('click', e => this.handlePanelToggle(e));
} }
init() {
this.loadActiveMetrics();
}
/* eslint-disable class-methods-use-this */ /* eslint-disable class-methods-use-this */
handlePanelToggle(e) { handlePanelToggle(e) {
const $toggleBtn = $(e.currentTarget); const $toggleBtn = $(e.currentTarget);
......
...@@ -53,11 +53,19 @@ export default { ...@@ -53,11 +53,19 @@ export default {
/> />
<ci-icon v-else :status="iconStatus" :size="24" /> <ci-icon v-else :status="iconStatus" :size="24" />
</div> </div>
<div class="report-block-list-issue-description"> <div class="report-block-list-issue-description">
<div class="report-block-list-issue-description-text">{{ summary }}</div> <div class="report-block-list-issue-description-text">
{{ summary
<popover v-if="popoverOptions" :options="popoverOptions" /> }}<span v-if="popoverOptions" class="text-nowrap"
>&nbsp;<popover v-if="popoverOptions" :options="popoverOptions" class="align-top" />
</span>
</div>
</div>
<div
v-if="$slots.default"
class="text-right flex-fill d-flex justify-content-end flex-column flex-sm-row"
>
<slot></slot>
</div> </div>
</div> </div>
</template> </template>
...@@ -12,7 +12,7 @@ export default function createRouter(base, baseRef) { ...@@ -12,7 +12,7 @@ export default function createRouter(base, baseRef) {
base: joinPaths(gon.relative_url_root || '', base), base: joinPaths(gon.relative_url_root || '', base),
routes: [ routes: [
{ {
path: `(/-)?/tree/(${encodeURIComponent(baseRef)}|${baseRef})/:path*`, path: `(/-)?/tree/(${encodeURIComponent(baseRef).replace(/%2F/g, '/')}|${baseRef})/:path*`,
name: 'treePath', name: 'treePath',
component: TreePage, component: TreePage,
props: route => ({ props: route => ({
......
<script>
import { GlNewButton, GlLoadingIcon } from '@gitlab/ui';
export default {
components: {
GlNewButton,
GlLoadingIcon,
},
props: {
saveable: {
type: Boolean,
required: false,
default: false,
},
savingChanges: {
type: Boolean,
required: false,
default: false,
},
},
};
</script>
<template>
<div class="d-flex bg-light border-top justify-content-between align-items-center py-3 px-4">
<gl-loading-icon :class="{ invisible: !savingChanges }" size="md" />
<gl-new-button
variant="success"
:disabled="!saveable || savingChanges"
@click="$emit('submit')"
>
{{ __('Submit Changes') }}
</gl-new-button>
</div>
</template>
...@@ -3,27 +3,29 @@ import { mapState, mapGetters, mapActions } from 'vuex'; ...@@ -3,27 +3,29 @@ import { mapState, mapGetters, mapActions } from 'vuex';
import { GlSkeletonLoader } from '@gitlab/ui'; import { GlSkeletonLoader } from '@gitlab/ui';
import EditArea from './edit_area.vue'; import EditArea from './edit_area.vue';
import Toolbar from './publish_toolbar.vue';
export default { export default {
components: { components: {
EditArea, EditArea,
GlSkeletonLoader, GlSkeletonLoader,
Toolbar,
}, },
computed: { computed: {
...mapState(['content', 'isLoadingContent']), ...mapState(['content', 'isLoadingContent', 'isSavingChanges']),
...mapGetters(['isContentLoaded']), ...mapGetters(['isContentLoaded', 'contentChanged']),
}, },
mounted() { mounted() {
this.loadContent(); this.loadContent();
}, },
methods: { methods: {
...mapActions(['loadContent']), ...mapActions(['loadContent', 'setContent', 'submitChanges']),
}, },
}; };
</script> </script>
<template> <template>
<div class="d-flex justify-content-center h-100"> <div class="d-flex justify-content-center h-100 pt-2">
<div v-if="isLoadingContent" class="w-50 h-50 mt-2"> <div v-if="isLoadingContent" class="w-50 h-50">
<gl-skeleton-loader :width="500" :height="102"> <gl-skeleton-loader :width="500" :height="102">
<rect width="500" height="16" rx="4" /> <rect width="500" height="16" rx="4" />
<rect y="20" width="375" height="16" rx="4" /> <rect y="20" width="375" height="16" rx="4" />
...@@ -33,6 +35,17 @@ export default { ...@@ -33,6 +35,17 @@ export default {
<rect x="410" y="40" width="90" height="16" rx="4" /> <rect x="410" y="40" width="90" height="16" rx="4" />
</gl-skeleton-loader> </gl-skeleton-loader>
</div> </div>
<edit-area v-if="isContentLoaded" class="w-75 h-100 shadow-none" :value="content" /> <div v-if="isContentLoaded" class="d-flex flex-grow-1 flex-column">
<edit-area
class="w-75 h-100 shadow-none align-self-center"
:value="content"
@input="setContent"
/>
<toolbar
:saveable="contentChanged"
:saving-changes="isSavingChanges"
@submit="submitChanges"
/>
</div>
</div> </div>
</template> </template>
...@@ -6,7 +6,7 @@ const initStaticSiteEditor = el => { ...@@ -6,7 +6,7 @@ const initStaticSiteEditor = el => {
const { projectId, path: sourcePath } = el.dataset; const { projectId, path: sourcePath } = el.dataset;
const store = createStore({ const store = createStore({
initialState: { projectId, sourcePath }, initialState: { projectId, sourcePath, username: window.gon.current_username },
}); });
return new Vue({ return new Vue({
......
// TODO implement
const submitContentChanges = () => new Promise(resolve => setTimeout(resolve, 1000));
export default submitContentChanges;
...@@ -3,6 +3,7 @@ import { __ } from '~/locale'; ...@@ -3,6 +3,7 @@ import { __ } from '~/locale';
import * as mutationTypes from './mutation_types'; import * as mutationTypes from './mutation_types';
import loadSourceContent from '~/static_site_editor/services/load_source_content'; import loadSourceContent from '~/static_site_editor/services/load_source_content';
import submitContentChanges from '~/static_site_editor/services/submit_content_changes';
export const loadContent = ({ commit, state: { sourcePath, projectId } }) => { export const loadContent = ({ commit, state: { sourcePath, projectId } }) => {
commit(mutationTypes.LOAD_CONTENT); commit(mutationTypes.LOAD_CONTENT);
...@@ -15,4 +16,19 @@ export const loadContent = ({ commit, state: { sourcePath, projectId } }) => { ...@@ -15,4 +16,19 @@ export const loadContent = ({ commit, state: { sourcePath, projectId } }) => {
}); });
}; };
export const setContent = ({ commit }, content) => {
commit(mutationTypes.SET_CONTENT, content);
};
export const submitChanges = ({ state: { projectId, content, sourcePath, username }, commit }) => {
commit(mutationTypes.SUBMIT_CHANGES);
return submitContentChanges({ content, projectId, sourcePath, username })
.then(data => commit(mutationTypes.SUBMIT_CHANGES_SUCCESS, data))
.catch(error => {
commit(mutationTypes.SUBMIT_CHANGES_ERROR);
createFlash(error.message);
});
};
export default () => {}; export default () => {};
// eslint-disable-next-line import/prefer-default-export export const isContentLoaded = ({ originalContent }) => Boolean(originalContent);
export const isContentLoaded = ({ content }) => Boolean(content); export const contentChanged = ({ originalContent, content }) => originalContent !== content;
export const LOAD_CONTENT = 'loadContent'; export const LOAD_CONTENT = 'loadContent';
export const RECEIVE_CONTENT_SUCCESS = 'receiveContentSuccess'; export const RECEIVE_CONTENT_SUCCESS = 'receiveContentSuccess';
export const RECEIVE_CONTENT_ERROR = 'receiveContentError'; export const RECEIVE_CONTENT_ERROR = 'receiveContentError';
export const SET_CONTENT = 'setContent';
export const SUBMIT_CHANGES = 'submitChanges';
export const SUBMIT_CHANGES_SUCCESS = 'submitChangesSuccess';
export const SUBMIT_CHANGES_ERROR = 'submitChangesError';
...@@ -8,8 +8,23 @@ export default { ...@@ -8,8 +8,23 @@ export default {
state.isLoadingContent = false; state.isLoadingContent = false;
state.title = title; state.title = title;
state.content = content; state.content = content;
state.originalContent = content;
}, },
[types.RECEIVE_CONTENT_ERROR](state) { [types.RECEIVE_CONTENT_ERROR](state) {
state.isLoadingContent = false; state.isLoadingContent = false;
}, },
[types.SET_CONTENT](state, content) {
state.content = content;
},
[types.SUBMIT_CHANGES](state) {
state.isSavingChanges = true;
},
[types.SUBMIT_CHANGES_SUCCESS](state, meta) {
state.savedContentMeta = meta;
state.isSavingChanges = false;
state.originalContent = state.content;
},
[types.SUBMIT_CHANGES_ERROR](state) {
state.isSavingChanges = false;
},
}; };
const createState = (initialState = {}) => ({ const createState = (initialState = {}) => ({
username: null,
projectId: null, projectId: null,
sourcePath: null, sourcePath: null,
isLoadingContent: false, isLoadingContent: false,
isSavingChanges: false,
originalContent: '',
content: '', content: '',
title: '', title: '',
......
<script>
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
const GITLAB_TEAM_MEMBER_LABEL = __('GitLab Team Member');
export default {
name: 'GitlabTeamMemberBadge',
directives: {
GlTooltip: GlTooltipDirective,
},
components: { GlIcon },
gitlabTeamMemberLabel: GITLAB_TEAM_MEMBER_LABEL,
};
</script>
<template>
<span
v-gl-tooltip.hover
:title="$options.gitlabTeamMemberLabel"
role="img"
:aria-label="$options.gitlabTeamMemberLabel"
class="d-inline-block align-middle"
>
<gl-icon name="tanuki-verified" class="gl-text-purple d-block" />
</span>
</template>
...@@ -210,3 +210,15 @@ ...@@ -210,3 +210,15 @@
} }
} }
} }
.health-status {
.dropdown-body {
.health-divider {
border-top-color: $gray-200;
}
.dropdown-item:not(.health-dropdown-item) {
padding: 0;
}
}
}
...@@ -63,15 +63,6 @@ ...@@ -63,15 +63,6 @@
list-style: none; list-style: none;
padding: 0 1px; padding: 0 1px;
margin: 0; margin: 0;
.license-item {
line-height: $gl-padding-32;
.license-packages {
font-size: $label-font-size;
}
}
} }
.report-block-list-icon { .report-block-list-icon {
......
...@@ -6,18 +6,20 @@ class GroupsController < Groups::ApplicationController ...@@ -6,18 +6,20 @@ class GroupsController < Groups::ApplicationController
include ParamsBackwardCompatibility include ParamsBackwardCompatibility
include PreviewMarkdown include PreviewMarkdown
include RecordUserLastActivity include RecordUserLastActivity
include SendFileUpload
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
respond_to :html respond_to :html
prepend_before_action(only: [:show, :issues]) { authenticate_sessionless_user!(:rss) } prepend_before_action(only: [:show, :issues]) { authenticate_sessionless_user!(:rss) }
prepend_before_action(only: [:issues_calendar]) { authenticate_sessionless_user!(:ics) } prepend_before_action(only: [:issues_calendar]) { authenticate_sessionless_user!(:ics) }
prepend_before_action :ensure_export_enabled, only: [:export, :download_export]
before_action :authenticate_user!, only: [:new, :create] before_action :authenticate_user!, only: [:new, :create]
before_action :group, except: [:index, :new, :create] before_action :group, except: [:index, :new, :create]
# Authorize # Authorize
before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects, :transfer] before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects, :transfer, :export, :download_export]
before_action :authorize_create_group!, only: [:new] before_action :authorize_create_group!, only: [:new]
before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests] before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests]
...@@ -29,6 +31,8 @@ class GroupsController < Groups::ApplicationController ...@@ -29,6 +31,8 @@ class GroupsController < Groups::ApplicationController
push_frontend_feature_flag(:vue_issuables_list, @group) push_frontend_feature_flag(:vue_issuables_list, @group)
end end
before_action :export_rate_limit, only: [:export, :download_export]
skip_cross_project_access_check :index, :new, :create, :edit, :update, skip_cross_project_access_check :index, :new, :create, :edit, :update,
:destroy, :projects :destroy, :projects
# When loading show as an atom feed, we render events that could leak cross # When loading show as an atom feed, we render events that could leak cross
...@@ -134,6 +138,25 @@ class GroupsController < Groups::ApplicationController ...@@ -134,6 +138,25 @@ class GroupsController < Groups::ApplicationController
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def export
export_service = Groups::ImportExport::ExportService.new(group: @group, user: current_user)
if export_service.async_execute
redirect_to edit_group_path(@group), notice: _('Group export started.')
else
redirect_to edit_group_path(@group), alert: _('Group export could not be started.')
end
end
def download_export
if @group.export_file_exists?
send_upload(@group.export_file, attachment: @group.export_file.filename)
else
redirect_to edit_group_path(@group),
alert: _('Group export link has expired. Please generate a new export from your group settings.')
end
end
protected protected
def render_show_html def render_show_html
...@@ -234,6 +257,21 @@ class GroupsController < Groups::ApplicationController ...@@ -234,6 +257,21 @@ class GroupsController < Groups::ApplicationController
url_for(safe_params) url_for(safe_params)
end end
def export_rate_limit
prefixed_action = "group_#{params[:action]}".to_sym
if Gitlab::ApplicationRateLimiter.throttled?(prefixed_action, scope: [current_user, prefixed_action, @group])
Gitlab::ApplicationRateLimiter.log_request(request, "#{prefixed_action}_request_limit".to_sym, current_user)
flash[:alert] = _('This endpoint has been requested too many times. Try again later.')
redirect_to edit_group_path(@group)
end
end
def ensure_export_enabled
render_404 unless Feature.enabled?(:group_import_export, @group, default_enabled: true)
end
private private
def groups def groups
......
...@@ -7,9 +7,10 @@ module Projects ...@@ -7,9 +7,10 @@ module Projects
before_action :jira_integration_configured? before_action :jira_integration_configured?
def show def show
@is_jira_configured = @project.jira_service.present?
return if Feature.enabled?(:jira_issue_import_vue, @project) return if Feature.enabled?(:jira_issue_import_vue, @project)
unless @project.import_state&.in_progress? unless @project.latest_jira_import&.in_progress?
jira_client = @project.jira_service.client jira_client = @project.jira_service.client
jira_projects = jira_client.Project.all jira_projects = jira_client.Project.all
...@@ -20,7 +21,7 @@ module Projects ...@@ -20,7 +21,7 @@ module Projects
end end
end end
flash[:notice] = _("Import %{status}") % { status: @project.import_state.status } if @project.import_state.present? && !@project.import_state.none? flash[:notice] = _("Import %{status}") % { status: @project.jira_import_status } unless @project.latest_jira_import&.initial?
end end
def import def import
...@@ -39,12 +40,13 @@ module Projects ...@@ -39,12 +40,13 @@ module Projects
private private
def jira_import_enabled? def jira_import_enabled?
return if Feature.enabled?(:jira_issue_import, @project) return if @project.jira_issues_import_feature_flag_enabled?
redirect_to project_issues_path(@project) redirect_to project_issues_path(@project)
end end
def jira_integration_configured? def jira_integration_configured?
return if Feature.enabled?(:jira_issue_import_vue, @project)
return if @project.jira_service return if @project.jira_service
flash[:notice] = _("Configure the Jira integration first on your project's %{strong_start} Settings > Integrations > Jira%{strong_end} page." % flash[:notice] = _("Configure the Jira integration first on your project's %{strong_start} Settings > Integrations > Jira%{strong_end} page." %
......
...@@ -180,7 +180,7 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -180,7 +180,7 @@ class Projects::PipelinesController < Projects::ApplicationController
render json: TestReportSerializer render json: TestReportSerializer
.new(current_user: @current_user) .new(current_user: @current_user)
.represent(test_reports) .represent(test_reports, project: project)
end end
end end
end end
......
...@@ -13,8 +13,6 @@ ...@@ -13,8 +13,6 @@
class LicenseTemplateFinder class LicenseTemplateFinder
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
prepend_if_ee('::EE::LicenseTemplateFinder') # rubocop: disable Cop/InjectEnterpriseEditionModule
attr_reader :project, :params attr_reader :project, :params
def initialize(project, params = {}) def initialize(project, params = {})
...@@ -52,3 +50,5 @@ class LicenseTemplateFinder ...@@ -52,3 +50,5 @@ class LicenseTemplateFinder
params.fetch(:popular, nil) params.fetch(:popular, nil)
end end
end end
LicenseTemplateFinder.prepend_if_ee('::EE::LicenseTemplateFinder')
...@@ -27,8 +27,6 @@ ...@@ -27,8 +27,6 @@
class ProjectsFinder < UnionFinder class ProjectsFinder < UnionFinder
include CustomAttributesFilter include CustomAttributesFilter
prepend_if_ee('::EE::ProjectsFinder') # rubocop: disable Cop/InjectEnterpriseEditionModule
attr_accessor :params attr_accessor :params
attr_reader :current_user, :project_ids_relation attr_reader :current_user, :project_ids_relation
...@@ -225,3 +223,5 @@ class ProjectsFinder < UnionFinder ...@@ -225,3 +223,5 @@ class ProjectsFinder < UnionFinder
{ min_access_level: params[:min_access_level] } { min_access_level: params[:min_access_level] }
end end
end end
ProjectsFinder.prepend_if_ee('::EE::ProjectsFinder')
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
class TemplateFinder class TemplateFinder
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
prepend_if_ee('::EE::TemplateFinder') # rubocop: disable Cop/InjectEnterpriseEditionModule
VENDORED_TEMPLATES = HashWithIndifferentAccess.new( VENDORED_TEMPLATES = HashWithIndifferentAccess.new(
dockerfiles: ::Gitlab::Template::DockerfileTemplate, dockerfiles: ::Gitlab::Template::DockerfileTemplate,
gitignores: ::Gitlab::Template::GitignoreTemplate, gitignores: ::Gitlab::Template::GitignoreTemplate,
...@@ -42,3 +40,5 @@ class TemplateFinder ...@@ -42,3 +40,5 @@ class TemplateFinder
end end
end end
end end
TemplateFinder.prepend_if_ee('::EE::TemplateFinder')
...@@ -30,11 +30,11 @@ module Mutations ...@@ -30,11 +30,11 @@ module Mutations
service_response = ::JiraImport::StartImportService service_response = ::JiraImport::StartImportService
.new(context[:current_user], project, jira_project_key) .new(context[:current_user], project, jira_project_key)
.execute .execute
import_data = service_response.payload[:import_data] jira_import = service_response.success? ? service_response.payload[:import_data] : nil
errors = service_response.error? ? [service_response.message] : []
{ {
jira_import: import_data.errors.blank? ? import_data.projects.last : nil, jira_import: jira_import,
errors: errors_on_object(import_data) errors: errors
} }
end end
......
# frozen_string_literal: true
module Resolvers
module Metrics
class DashboardResolver < Resolvers::BaseResolver
argument :path, GraphQL::STRING_TYPE,
required: true,
description: "Path to a file which defines metrics dashboard eg: 'config/prometheus/common_metrics.yml'"
type Types::Metrics::DashboardType, null: true
alias_method :environment, :object
def resolve(**args)
return unless environment
::PerformanceMonitoring::PrometheusDashboard.find_for(project: environment.project, user: context[:current_user], path: args[:path], options: { environment: environment })
end
end
end
end
...@@ -8,15 +8,13 @@ module Resolvers ...@@ -8,15 +8,13 @@ module Resolvers
alias_method :project, :object alias_method :project, :object
def resolve(**args) def resolve(**args)
return JiraImportData.none unless project&.import_data.present?
authorize!(project) authorize!(project)
project.import_data.becomes(JiraImportData).projects project.jira_imports
end end
def authorized_resource?(project) def authorized_resource?(project)
return false unless Feature.enabled?(:jira_issue_import, project) return false unless project.jira_issues_import_feature_flag_enabled?
Ability.allowed?(context[:current_user], :admin_project, project) Ability.allowed?(context[:current_user], :admin_project, project)
end end
......
...@@ -15,5 +15,9 @@ module Types ...@@ -15,5 +15,9 @@ module Types
field :state, GraphQL::STRING_TYPE, null: false, field :state, GraphQL::STRING_TYPE, null: false,
description: 'State of the environment, for example: available/stopped' description: 'State of the environment, for example: available/stopped'
field :metrics_dashboard, Types::Metrics::DashboardType, null: true,
description: 'Metrics dashboard schema for the environment',
resolver: Resolvers::Metrics::DashboardResolver
end end
end end
...@@ -8,20 +8,12 @@ module Types ...@@ -8,20 +8,12 @@ module Types
graphql_name 'JiraImport' graphql_name 'JiraImport'
field :scheduled_at, Types::TimeType, null: true, field :scheduled_at, Types::TimeType, null: true,
description: 'Timestamp of when the Jira import was created/started' method: :created_at,
description: 'Timestamp of when the Jira import was created'
field :scheduled_by, Types::UserType, null: true, field :scheduled_by, Types::UserType, null: true,
description: 'User that started the Jira import' description: 'User that started the Jira import'
field :jira_project_key, GraphQL::STRING_TYPE, null: false, field :jira_project_key, GraphQL::STRING_TYPE, null: false,
description: 'Project key for the imported Jira project', description: 'Project key for the imported Jira project'
method: :key
def scheduled_at
DateTime.parse(object.scheduled_at)
end
def scheduled_by
::Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.scheduled_by['user_id']).find
end
end end
# rubocop: enable Graphql/AuthorizeTypes # rubocop: enable Graphql/AuthorizeTypes
end end
# frozen_string_literal: true
module Types
module Metrics
# rubocop: disable Graphql/AuthorizeTypes
# Authorization is performed at environment level
class DashboardType < ::Types::BaseObject
graphql_name 'MetricsDashboard'
field :path, GraphQL::STRING_TYPE, null: true,
description: 'Path to a file with the dashboard definition'
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
...@@ -41,6 +41,23 @@ module ClustersHelper ...@@ -41,6 +41,23 @@ module ClustersHelper
end end
end end
def cluster_type_label(cluster_type)
case cluster_type
when 'project_type'
s_('ClusterIntegration|Project cluster')
when 'group_type'
s_('ClusterIntegration|Group cluster')
when 'instance_type'
s_('ClusterIntegration|Instance cluster')
else
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(
ArgumentError.new('Cluster Type Missing'),
cluster_error: { error: 'Cluster Type Missing', cluster_type: cluster_type }
)
_('Cluster')
end
end
def has_rbac_enabled?(cluster) def has_rbac_enabled?(cluster)
return cluster.platform_kubernetes_rbac? if cluster.platform_kubernetes return cluster.platform_kubernetes_rbac? if cluster.platform_kubernetes
......
...@@ -254,5 +254,3 @@ class Blob < SimpleDelegator ...@@ -254,5 +254,3 @@ class Blob < SimpleDelegator
classes.find { |viewer_class| viewer_class.can_render?(self, verify_binary: verify_binary) } classes.find { |viewer_class| viewer_class.can_render?(self, verify_binary: verify_binary) }
end end
end end
Blob.prepend_if_ee('EE::Blob')
...@@ -67,7 +67,6 @@ module Ci ...@@ -67,7 +67,6 @@ module Ci
end end
def from_needs(scope) def from_needs(scope)
return scope unless Feature.enabled?(:ci_dag_support, project, default_enabled: true)
return scope unless processable.scheduling_type_dag? return scope unless processable.scheduling_type_dag?
needs_names = processable.needs.artifacts.select(:name) needs_names = processable.needs.artifacts.select(:name)
......
...@@ -25,8 +25,6 @@ module Ci ...@@ -25,8 +25,6 @@ module Ci
end end
def self.select_with_aggregated_needs(project) def self.select_with_aggregated_needs(project)
return all unless Feature.enabled?(:ci_dag_support, project, default_enabled: true)
aggregated_needs_names = Ci::BuildNeed aggregated_needs_names = Ci::BuildNeed
.scoped_build .scoped_build
.select("ARRAY_AGG(name)") .select("ARRAY_AGG(name)")
......
...@@ -123,6 +123,7 @@ module Clusters ...@@ -123,6 +123,7 @@ module Clusters
scope :managed, -> { where(managed: true) } scope :managed, -> { where(managed: true) }
scope :with_persisted_applications, -> { eager_load(*APPLICATIONS_ASSOCIATIONS) } scope :with_persisted_applications, -> { eager_load(*APPLICATIONS_ASSOCIATIONS) }
scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) } scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) }
scope :with_management_project, -> { where.not(management_project: nil) }
scope :for_project_namespace, -> (namespace_id) { joins(:projects).where(projects: { namespace_id: namespace_id }) } scope :for_project_namespace, -> (namespace_id) { joins(:projects).where(projects: { namespace_id: namespace_id }) }
......
...@@ -16,7 +16,7 @@ module ImportState ...@@ -16,7 +16,7 @@ module ImportState
end end
def self.jid_by(project_id:, status:) def self.jid_by(project_id:, status:)
select(:jid).with_status(status).find_by(project_id: project_id) select(:jid).where(status: status).find_by(project_id: project_id)
end end
end end
end end
......
# frozen_string_literal: true
module WhereComposite
extend ActiveSupport::Concern
class TooManyIds < ArgumentError
LIMIT = 100
def initialize(no_of_ids)
super(<<~MSG)
At most #{LIMIT} identifier sets at a time please! Got #{no_of_ids}.
Have you considered splitting your request into batches?
MSG
end
def self.guard(collection)
n = collection.size
return collection if n <= LIMIT
raise self, n
end
end
class_methods do
# Apply a set of constraints that function as composite IDs.
#
# This is the plural form of the standard ActiveRecord idiom:
# `where(foo: x, bar: y)`, except it allows multiple pairs of `x` and
# `y` to be specified, with the semantics that translate to:
#
# ```sql
# WHERE
# (foo = x_0 AND bar = y_0)
# OR (foo = x_1 AND bar = y_1)
# OR ...
# ```
#
# or the equivalent:
#
# ```sql
# WHERE
# (foo, bar) IN ((x_0, y_0), (x_1, y_1), ...)
# ```
#
# @param permitted_keys [Array<Symbol>] The keys each hash must have. There
# must be at least one key (but really,
# it ought to be at least two)
# @param hashes [Array<#to_h>|#to_h] The constraints. Each parameter must have a
# value for the keys named in `permitted_keys`
#
# e.g.:
# ```
# where_composite(%i[foo bar], [{foo: 1, bar: 2}, {foo: 1, bar: 3}])
# ```
#
def where_composite(permitted_keys, hashes)
raise ArgumentError, 'no permitted_keys' unless permitted_keys.present?
# accept any hash-like thing, such as Structs
hashes = TooManyIds.guard(Array.wrap(hashes)).map(&:to_h)
return none if hashes.empty?
case permitted_keys.size
when 1
key = permitted_keys.first
where(key => hashes.map { |hash| hash.fetch(key) })
else
clauses = hashes.map do |hash|
permitted_keys.map do |key|
arel_table[key].eq(hash.fetch(key))
end.reduce(:and)
end
where(clauses.reduce(:or))
end
rescue KeyError
raise ArgumentError, "all arguments must contain #{permitted_keys}"
end
end
end
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
class ContainerExpirationPolicy < ApplicationRecord class ContainerExpirationPolicy < ApplicationRecord
include Schedulable include Schedulable
include UsageStatistics
belongs_to :project, inverse_of: :container_expiration_policy belongs_to :project, inverse_of: :container_expiration_policy
......
...@@ -13,6 +13,8 @@ class GroupGroupLink < ApplicationRecord ...@@ -13,6 +13,8 @@ class GroupGroupLink < ApplicationRecord
validates :group_access, inclusion: { in: Gitlab::Access.all_values }, validates :group_access, inclusion: { in: Gitlab::Access.all_values },
presence: true presence: true
scope :non_guests, -> { where('group_access > ?', Gitlab::Access::GUEST) }
def self.access_options def self.access_options
Gitlab::Access.options_with_owner Gitlab::Access.options_with_owner
end end
......
...@@ -16,6 +16,7 @@ class Issue < ApplicationRecord ...@@ -16,6 +16,7 @@ class Issue < ApplicationRecord
include LabelEventable include LabelEventable
include IgnorableColumns include IgnorableColumns
include MilestoneEventable include MilestoneEventable
include WhereComposite
DueDateStruct = Struct.new(:title, :name).freeze DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
...@@ -78,6 +79,26 @@ class Issue < ApplicationRecord ...@@ -78,6 +79,26 @@ class Issue < ApplicationRecord
scope :counts_by_state, -> { reorder(nil).group(:state_id).count } scope :counts_by_state, -> { reorder(nil).group(:state_id).count }
# An issue can be uniquely identified by project_id and iid
# Takes one or more sets of composite IDs, expressed as hash-like records of
# `{project_id: x, iid: y}`.
#
# @see WhereComposite::where_composite
#
# e.g:
#
# .by_project_id_and_iid({project_id: 1, iid: 2})
# .by_project_id_and_iid([]) # returns ActiveRecord::NullRelation
# .by_project_id_and_iid([
# {project_id: 1, iid: 1},
# {project_id: 2, iid: 1},
# {project_id: 1, iid: 2}
# ])
#
scope :by_project_id_and_iid, ->(composites) do
where_composite(%i[project_id iid], composites)
end
after_commit :expire_etag_cache, unless: :importing? after_commit :expire_etag_cache, unless: :importing?
after_save :ensure_metrics, unless: :importing? after_save :ensure_metrics, unless: :importing?
......
# frozen_string_literal: true
class JiraImportData < ProjectImportData
JiraProjectDetails = Struct.new(:key, :scheduled_at, :scheduled_by)
FORCE_IMPORT_KEY = 'force-import'
def projects
return [] unless data
projects = data.dig('jira', 'projects')&.map do |p|
JiraProjectDetails.new(p['key'], p['scheduled_at'], p['scheduled_by'])
end
projects&.sort_by { |jp| jp.scheduled_at } || []
end
def <<(project)
self.data ||= { 'jira' => { 'projects' => [] } }
self.data['jira'] ||= { 'projects' => [] }
self.data['jira']['projects'] = [] if data['jira']['projects'].blank? || !data['jira']['projects'].is_a?(Array)
self.data['jira']['projects'] << project.to_h
self.data.deep_stringify_keys!
end
def force_import!
self.data ||= {}
self.data.deep_merge!({ 'jira' => { FORCE_IMPORT_KEY => true } })
self.data.deep_stringify_keys!
end
def force_import?
!!data&.dig('jira', FORCE_IMPORT_KEY) && !projects.blank?
end
def finish_import!
return if data&.dig('jira', FORCE_IMPORT_KEY).nil?
data['jira'].delete(FORCE_IMPORT_KEY)
end
def current_project
projects.last
end
end
...@@ -22,6 +22,8 @@ class JiraImportState < ApplicationRecord ...@@ -22,6 +22,8 @@ class JiraImportState < ApplicationRecord
message: _('Cannot have multiple Jira imports running at the same time') message: _('Cannot have multiple Jira imports running at the same time')
} }
alias_method :scheduled_by, :user
state_machine :status, initial: :initial do state_machine :status, initial: :initial do
event :schedule do event :schedule do
transition initial: :scheduled transition initial: :scheduled
...@@ -46,6 +48,11 @@ class JiraImportState < ApplicationRecord ...@@ -46,6 +48,11 @@ class JiraImportState < ApplicationRecord
end end
end end
before_transition any => :finished do |state, _|
InternalId.flush_records!(project: state.project)
state.project.update_project_counter_caches
end
after_transition any => :finished do |state, _| after_transition any => :finished do |state, _|
if state.jid.present? if state.jid.present?
Gitlab::SidekiqStatus.unset(state.jid) Gitlab::SidekiqStatus.unset(state.jid)
...@@ -67,4 +74,8 @@ class JiraImportState < ApplicationRecord ...@@ -67,4 +74,8 @@ class JiraImportState < ApplicationRecord
def in_progress? def in_progress?
scheduled? || started? scheduled? || started?
end end
def non_initial?
!initial?
end
end end
...@@ -555,22 +555,28 @@ class MergeRequest < ApplicationRecord ...@@ -555,22 +555,28 @@ class MergeRequest < ApplicationRecord
end end
end end
def diff_stats
return unless diff_refs
strong_memoize(:diff_stats) do
project.repository.diff_stats(diff_refs.base_sha, diff_refs.head_sha)
end
end
def diff_size def diff_size
# Calling `merge_request_diff.diffs.real_size` will also perform # Calling `merge_request_diff.diffs.real_size` will also perform
# highlighting, which we don't need here. # highlighting, which we don't need here.
merge_request_diff&.real_size || diffs.real_size merge_request_diff&.real_size || diff_stats&.real_size || diffs.real_size
end end
def modified_paths(past_merge_request_diff: nil) def modified_paths(past_merge_request_diff: nil)
diffs = if past_merge_request_diff if past_merge_request_diff
past_merge_request_diff past_merge_request_diff.modified_paths
elsif compare elsif compare
compare diff_stats&.paths || compare.modified_paths
else else
self.merge_request_diff merge_request_diff.modified_paths
end end
diffs.modified_paths
end end
def new_paths def new_paths
......
...@@ -4,27 +4,41 @@ module PerformanceMonitoring ...@@ -4,27 +4,41 @@ module PerformanceMonitoring
class PrometheusDashboard class PrometheusDashboard
include ActiveModel::Model include ActiveModel::Model
attr_accessor :dashboard, :panel_groups attr_accessor :dashboard, :panel_groups, :path, :environment, :priority
validates :dashboard, presence: true validates :dashboard, presence: true
validates :panel_groups, presence: true validates :panel_groups, presence: true
def self.from_json(json_content) class << self
dashboard = new( def from_json(json_content)
dashboard: json_content['dashboard'], dashboard = new(
panel_groups: json_content['panel_groups'].map { |group| PrometheusPanelGroup.from_json(group) } dashboard: json_content['dashboard'],
) panel_groups: json_content['panel_groups'].map { |group| PrometheusPanelGroup.from_json(group) }
)
dashboard.tap(&:validate!)
dashboard.tap(&:validate!)
end
def find_for(project:, user:, path:, options: {})
dashboard_response = Gitlab::Metrics::Dashboard::Finder.find(project, user, options.merge(dashboard_path: path))
return unless dashboard_response[:status] == :success
new(
{
path: path,
environment: options[:environment]
}.merge(dashboard_response[:dashboard])
)
end
end end
def to_yaml def to_yaml
self.as_json(only: valid_attributes).to_yaml self.as_json(only: yaml_valid_attributes).to_yaml
end end
private private
def valid_attributes def yaml_valid_attributes
%w(panel_groups panels metrics group priority type title y_label weight id unit label query query_range dashboard) %w(panel_groups panels metrics group priority type title y_label weight id unit label query query_range dashboard)
end end
end end
......
...@@ -4,7 +4,7 @@ module PerformanceMonitoring ...@@ -4,7 +4,7 @@ module PerformanceMonitoring
class PrometheusPanel class PrometheusPanel
include ActiveModel::Model include ActiveModel::Model
attr_accessor :type, :title, :y_label, :weight, :metrics attr_accessor :type, :title, :y_label, :weight, :metrics, :y_axis
validates :title, presence: true validates :title, presence: true
validates :metrics, presence: true validates :metrics, presence: true
...@@ -20,5 +20,9 @@ module PerformanceMonitoring ...@@ -20,5 +20,9 @@ module PerformanceMonitoring
panel.tap(&:validate!) panel.tap(&:validate!)
end end
def id(group_title)
Digest::SHA2.hexdigest([group_title, type, title].join)
end
end end
end end
...@@ -786,6 +786,10 @@ class Project < ApplicationRecord ...@@ -786,6 +786,10 @@ class Project < ApplicationRecord
Feature.enabled?(:context_commits, default_enabled: true) Feature.enabled?(:context_commits, default_enabled: true)
end end
def jira_issues_import_feature_flag_enabled?
Feature.enabled?(:jira_issue_import, self, default_enabled: true)
end
def team def team
@team ||= ProjectTeam.new(self) @team ||= ProjectTeam.new(self)
end end
...@@ -859,9 +863,7 @@ class Project < ApplicationRecord ...@@ -859,9 +863,7 @@ class Project < ApplicationRecord
end end
def jira_import_status def jira_import_status
return import_status if jira_force_import? latest_jira_import&.status || 'initial'
import_data&.becomes(JiraImportData)&.projects.blank? ? 'none' : 'finished'
end end
def human_import_status_name def human_import_status_name
...@@ -875,8 +877,6 @@ class Project < ApplicationRecord ...@@ -875,8 +877,6 @@ class Project < ApplicationRecord
elsif gitlab_project_import? elsif gitlab_project_import?
# Do not retry on Import/Export until https://gitlab.com/gitlab-org/gitlab-foss/issues/26189 is solved. # Do not retry on Import/Export until https://gitlab.com/gitlab-org/gitlab-foss/issues/26189 is solved.
RepositoryImportWorker.set(retry: false).perform_async(self.id) RepositoryImportWorker.set(retry: false).perform_async(self.id)
elsif jira_import?
Gitlab::JiraImport::Stage::StartImportWorker.perform_async(self.id)
else else
RepositoryImportWorker.perform_async(self.id) RepositoryImportWorker.perform_async(self.id)
end end
...@@ -909,7 +909,7 @@ class Project < ApplicationRecord ...@@ -909,7 +909,7 @@ class Project < ApplicationRecord
# This method is overridden in EE::Project model # This method is overridden in EE::Project model
def remove_import_data def remove_import_data
import_data&.destroy unless jira_import? import_data&.destroy
end end
def ci_config_path=(value) def ci_config_path=(value)
...@@ -972,11 +972,7 @@ class Project < ApplicationRecord ...@@ -972,11 +972,7 @@ class Project < ApplicationRecord
end end
def jira_import? def jira_import?
import_type == 'jira' && Feature.enabled?(:jira_issue_import, self) import_type == 'jira' && latest_jira_import.present? && jira_issues_import_feature_flag_enabled?
end
def jira_force_import?
jira_import? && import_data&.becomes(JiraImportData)&.force_import?
end end
def gitlab_project_import? def gitlab_project_import?
......
...@@ -3,11 +3,6 @@ ...@@ -3,11 +3,6 @@
class ProjectGroupLink < ApplicationRecord class ProjectGroupLink < ApplicationRecord
include Expirable include Expirable
GUEST = 10
REPORTER = 20
DEVELOPER = 30
MAINTAINER = 40
belongs_to :project belongs_to :project
belongs_to :group belongs_to :group
...@@ -18,6 +13,8 @@ class ProjectGroupLink < ApplicationRecord ...@@ -18,6 +13,8 @@ class ProjectGroupLink < ApplicationRecord
validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true
validate :different_group validate :different_group
scope :non_guests, -> { where('group_access > ?', Gitlab::Access::GUEST) }
after_commit :refresh_group_members_authorized_projects after_commit :refresh_group_members_authorized_projects
alias_method :shared_with_group, :group alias_method :shared_with_group, :group
...@@ -27,7 +24,7 @@ class ProjectGroupLink < ApplicationRecord ...@@ -27,7 +24,7 @@ class ProjectGroupLink < ApplicationRecord
end end
def self.default_access def self.default_access
DEVELOPER Gitlab::Access::DEVELOPER
end end
def self.search(query) def self.search(query)
......
...@@ -56,7 +56,8 @@ class PrometheusAlert < ApplicationRecord ...@@ -56,7 +56,8 @@ class PrometheusAlert < ApplicationRecord
"for" => "5m", "for" => "5m",
"labels" => { "labels" => {
"gitlab" => "hook", "gitlab" => "hook",
"gitlab_alert_id" => prometheus_metric_id "gitlab_alert_id" => prometheus_metric_id,
"gitlab_prometheus_alert_id" => id
} }
} }
end end
......
...@@ -267,7 +267,7 @@ class Snippet < ApplicationRecord ...@@ -267,7 +267,7 @@ class Snippet < ApplicationRecord
def repository_size_checker def repository_size_checker
strong_memoize(:repository_size_checker) do strong_memoize(:repository_size_checker) do
::Gitlab::RepositorySizeChecker.new( ::Gitlab::RepositorySizeChecker.new(
current_size_proc: -> { repository._uncached_size.megabytes }, current_size_proc: -> { repository.size.megabytes },
limit: Gitlab::CurrentSettings.snippet_size_limit limit: Gitlab::CurrentSettings.snippet_size_limit
) )
end end
......
...@@ -89,3 +89,5 @@ module Clusters ...@@ -89,3 +89,5 @@ module Clusters
end end
end end
end end
Clusters::ClusterPresenter.prepend_if_ee('EE::Clusters::ClusterPresenter')
...@@ -26,8 +26,6 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated ...@@ -26,8 +26,6 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
presents :build presents :build
prepend_if_ee('::EE::CommitStatusPresenter') # rubocop: disable Cop/InjectEnterpriseEditionModule
def self.callout_failure_messages def self.callout_failure_messages
CALLOUT_FAILURE_MESSAGES CALLOUT_FAILURE_MESSAGES
end end
...@@ -44,3 +42,5 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated ...@@ -44,3 +42,5 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
script_failure? || missing_dependency_failure? || archived_failure? || scheduler_failure? || data_integrity_failure? script_failure? || missing_dependency_failure? || archived_failure? || scheduler_failure? || data_integrity_failure?
end end
end end
CommitStatusPresenter.prepend_if_ee('::EE::CommitStatusPresenter')
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module Projects module Projects
module Prometheus module Prometheus
class AlertPresenter < Gitlab::View::Presenter::Delegated class AlertPresenter < Gitlab::View::Presenter::Delegated
RESERVED_ANNOTATIONS = %w(gitlab_incident_markdown title).freeze RESERVED_ANNOTATIONS = %w(gitlab_incident_markdown gitlab_y_label title).freeze
GENERIC_ALERT_SUMMARY_ANNOTATIONS = %w(monitoring_tool service hosts).freeze GENERIC_ALERT_SUMMARY_ANNOTATIONS = %w(monitoring_tool service hosts).freeze
MARKDOWN_LINE_BREAK = " \n".freeze MARKDOWN_LINE_BREAK = " \n".freeze
INCIDENT_LABEL_NAME = IncidentManagement::CreateIssueService::INCIDENT_LABEL[:title].freeze INCIDENT_LABEL_NAME = IncidentManagement::CreateIssueService::INCIDENT_LABEL[:title].freeze
......
# frozen_string_literal: true # frozen_string_literal: true
class BuildDetailsEntity < JobEntity class BuildDetailsEntity < JobEntity
prepend_if_ee('::EE::BuildDetailEntity') # rubocop: disable Cop/InjectEnterpriseEditionModule
expose :coverage, :erased_at, :duration expose :coverage, :erased_at, :duration
expose :tag_list, as: :tags expose :tag_list, as: :tags
expose :has_trace?, as: :has_trace expose :has_trace?, as: :has_trace
...@@ -152,3 +150,5 @@ class BuildDetailsEntity < JobEntity ...@@ -152,3 +150,5 @@ class BuildDetailsEntity < JobEntity
_("Please refer to <a href=\"%{docs_url}\">%{docs_url}</a>") % { docs_url: docs_url } _("Please refer to <a href=\"%{docs_url}\">%{docs_url}</a>") % { docs_url: docs_url }
end end
end end
BuildDetailsEntity.prepend_if_ee('::EE::BuildDetailEntity')
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
class IssueEntity < IssuableEntity class IssueEntity < IssuableEntity
include TimeTrackableEntity include TimeTrackableEntity
prepend_if_ee('::EE::IssueEntity') # rubocop: disable Cop/InjectEnterpriseEditionModule
expose :state expose :state
expose :milestone_id expose :milestone_id
...@@ -73,3 +72,5 @@ class IssueEntity < IssuableEntity ...@@ -73,3 +72,5 @@ class IssueEntity < IssuableEntity
help_page_path('user/project/settings/index.md', anchor: 'archiving-a-project') help_page_path('user/project/settings/index.md', anchor: 'archiving-a-project')
end end
end end
IssueEntity.prepend_if_ee('::EE::IssueEntity')
# frozen_string_literal: true # frozen_string_literal: true
class ProjectMirrorEntity < Grape::Entity class ProjectMirrorEntity < Grape::Entity
prepend_if_ee('::EE::ProjectMirrorEntity') # rubocop: disable Cop/InjectEnterpriseEditionModule
expose :id expose :id
expose :remote_mirrors_attributes, using: RemoteMirrorEntity do |project| expose :remote_mirrors_attributes, using: RemoteMirrorEntity do |project|
project.remote_mirrors project.remote_mirrors
end end
end end
ProjectMirrorEntity.prepend_if_ee('::EE::ProjectMirrorEntity')
...@@ -8,7 +8,8 @@ module Ci ...@@ -8,7 +8,8 @@ module Ci
# issue: https://gitlab.com/gitlab-org/gitlab/issues/34224 # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
class CompareReportsBaseService < ::BaseService class CompareReportsBaseService < ::BaseService
def execute(base_pipeline, head_pipeline) def execute(base_pipeline, head_pipeline)
comparer = comparer_class.new(get_report(base_pipeline), get_report(head_pipeline)) comparer = build_comparer(base_pipeline, head_pipeline)
{ {
status: :parsed, status: :parsed,
key: key(base_pipeline, head_pipeline), key: key(base_pipeline, head_pipeline),
...@@ -28,6 +29,12 @@ module Ci ...@@ -28,6 +29,12 @@ module Ci
data&.fetch(:key, nil) == key(base_pipeline, head_pipeline) data&.fetch(:key, nil) == key(base_pipeline, head_pipeline)
end end
protected
def build_comparer(base_pipeline, head_pipeline)
comparer_class.new(get_report(base_pipeline), get_report(head_pipeline))
end
private private
def key(base_pipeline, head_pipeline) def key(base_pipeline, head_pipeline)
......
...@@ -93,7 +93,7 @@ module Ci ...@@ -93,7 +93,7 @@ module Ci
end end
def processable_status(processable) def processable_status(processable)
if Feature.enabled?(:ci_dag_support, project, default_enabled: true) && processable.scheduling_type_dag? if processable.scheduling_type_dag?
# Processable uses DAG, get status of all dependent needs # Processable uses DAG, get status of all dependent needs
@collection.status_for_names(processable.aggregated_needs_names.to_a) @collection.status_for_names(processable.aggregated_needs_names.to_a)
else else
......
...@@ -43,8 +43,6 @@ module Ci ...@@ -43,8 +43,6 @@ module Ci
end end
def process_dag_builds_without_needs def process_dag_builds_without_needs
return false unless Feature.enabled?(:ci_dag_support, project, default_enabled: true)
created_processables.scheduling_type_dag.without_needs.each do |build| created_processables.scheduling_type_dag.without_needs.each do |build|
process_build(build, 'success') process_build(build, 'success')
end end
...@@ -52,7 +50,6 @@ module Ci ...@@ -52,7 +50,6 @@ module Ci
def process_dag_builds_with_needs(trigger_build_ids) def process_dag_builds_with_needs(trigger_build_ids)
return false unless trigger_build_ids.present? return false unless trigger_build_ids.present?
return false unless Feature.enabled?(:ci_dag_support, project, default_enabled: true)
# we find processables that are dependent: # we find processables that are dependent:
# 1. because of current dependency, # 1. because of current dependency,
...@@ -110,11 +107,7 @@ module Ci ...@@ -110,11 +107,7 @@ module Ci
end end
def created_stage_scheduled_processables def created_stage_scheduled_processables
if Feature.enabled?(:ci_dag_support, project, default_enabled: true) created_processables.scheduling_type_stage
created_processables.scheduling_type_stage
else
created_processables
end
end end
def created_processables def created_processables
......
...@@ -10,9 +10,15 @@ module Groups ...@@ -10,9 +10,15 @@ module Groups
@shared = @params[:shared] || Gitlab::ImportExport::Shared.new(@group) @shared = @params[:shared] || Gitlab::ImportExport::Shared.new(@group)
end end
def async_execute
GroupExportWorker.perform_async(@current_user.id, @group.id, @params)
end
def execute def execute
validate_user_permissions validate_user_permissions
remove_existing_export! if @group.export_file_exists?
save! save!
ensure ensure
cleanup cleanup
...@@ -30,6 +36,13 @@ module Groups ...@@ -30,6 +36,13 @@ module Groups
end end
end end
def remove_existing_export!
import_export_upload = @group.import_export_upload
import_export_upload.remove_export_file!
import_export_upload.save
end
def save! def save!
if savers.all?(&:save) if savers.all?(&:save)
notify_success notify_success
......
...@@ -20,27 +20,31 @@ module JiraImport ...@@ -20,27 +20,31 @@ module JiraImport
private private
def create_and_schedule_import def create_and_schedule_import
import_data = project.create_or_update_import_data(data: {}).becomes(JiraImportData) jira_import = build_jira_import
jira_project_details = JiraImportData::JiraProjectDetails.new(
jira_project_key,
Time.now.strftime('%Y-%m-%d %H:%M:%S'),
{ user_id: user.id, name: user.name }
)
import_data << jira_project_details
import_data.force_import!
project.import_type = 'jira' project.import_type = 'jira'
project.import_state.schedule if project.save! project.save! && jira_import.schedule!
ServiceResponse.success(payload: { import_data: import_data } ) ServiceResponse.success(payload: { import_data: jira_import } )
rescue => ex rescue => ex
# in case project.save! raises an erorr # in case project.save! raises an erorr
Gitlab::ErrorTracking.track_exception(ex, project_id: project.id) Gitlab::ErrorTracking.track_exception(ex, project_id: project.id)
build_error_response(ex.message) build_error_response(ex.message)
jira_import.do_fail!
end
def build_jira_import
project.jira_imports.build(
user: user,
jira_project_key: jira_project_key,
# we do not have the jira_project_name or jira_project_xid yet so just set a mock value,
# we will once https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28190
jira_project_name: jira_project_key,
jira_project_xid: 0
)
end end
def validate def validate
return build_error_response(_('Jira import feature is disabled.')) unless Feature.enabled?(:jira_issue_import, project) return build_error_response(_('Jira import feature is disabled.')) unless project.jira_issues_import_feature_flag_enabled?
return build_error_response(_('You do not have permissions to run the import.')) unless user.can?(:admin_project, project) return build_error_response(_('You do not have permissions to run the import.')) unless user.can?(:admin_project, project)
return build_error_response(_('Jira integration not configured.')) unless project.jira_service&.active? return build_error_response(_('Jira integration not configured.')) unless project.jira_service&.active?
return build_error_response(_('Unable to find Jira project to import data from.')) if jira_project_key.blank? return build_error_response(_('Unable to find Jira project to import data from.')) if jira_project_key.blank?
...@@ -48,18 +52,11 @@ module JiraImport ...@@ -48,18 +52,11 @@ module JiraImport
end end
def build_error_response(message) def build_error_response(message)
import_data = JiraImportData.new(project: project) ServiceResponse.error(message: message, http_status: 400)
import_data.errors.add(:base, message)
ServiceResponse.error(
message: import_data.errors.full_messages.to_sentence,
http_status: 400,
payload: { import_data: import_data }
)
end end
def import_in_progress? def import_in_progress?
import_state = project.import_state || project.create_import_state project.latest_jira_import&.in_progress?
import_state.in_progress?
end end
end end
end end
...@@ -11,6 +11,7 @@ module Metrics ...@@ -11,6 +11,7 @@ module Metrics
SEQUENCE = [ SEQUENCE = [
STAGES::CommonMetricsInserter, STAGES::CommonMetricsInserter,
STAGES::EndpointInserter, STAGES::EndpointInserter,
STAGES::PanelIdsInserter,
STAGES::Sorter STAGES::Sorter
].freeze ].freeze
......
...@@ -57,7 +57,7 @@ module Metrics ...@@ -57,7 +57,7 @@ module Metrics
# @return [Hash] # @return [Hash]
override :raw_dashboard override :raw_dashboard
def raw_dashboard def raw_dashboard
panels_not_found!(identifiers) if panels.empty? panels_not_found!(identifiers) if metrics.empty?
{ 'panel_groups' => [{ 'panels' => panels }] } { 'panel_groups' => [{ 'panels' => panels }] }
end end
...@@ -66,11 +66,20 @@ module Metrics ...@@ -66,11 +66,20 @@ module Metrics
# Generated dashboard panels for each metric which # Generated dashboard panels for each metric which
# matches the provided input. # matches the provided input.
#
# As the panel is generated
# on the fly, we're using default values for info
# not represented in the DB.
#
# @return [Array<Hash>] # @return [Array<Hash>]
def panels def panels
strong_memoize(:panels) do [{
metrics.map { |metric| panel_for_metric(metric) } type: DEFAULT_PANEL_TYPE,
end weight: DEFAULT_PANEL_WEIGHT,
title: title,
y_label: y_label,
metrics: metrics.map(&:to_metric_hash)
}]
end end
# Metrics which match the provided inputs. # Metrics which match the provided inputs.
...@@ -78,12 +87,14 @@ module Metrics ...@@ -78,12 +87,14 @@ module Metrics
# displayed in a single panel/chart. # displayed in a single panel/chart.
# @return [ActiveRecord::AssociationRelation<PromtheusMetric>] # @return [ActiveRecord::AssociationRelation<PromtheusMetric>]
def metrics def metrics
PrometheusMetricsFinder.new( strong_memoize(:metrics) do
project: project, PrometheusMetricsFinder.new(
group: group_key, project: project,
title: title, group: group_key,
y_label: y_label title: title,
).execute y_label: y_label
).execute
end
end end
# Returns a symbol representing the group that # Returns a symbol representing the group that
...@@ -101,22 +112,6 @@ module Metrics ...@@ -101,22 +112,6 @@ module Metrics
.to_s .to_s
end end
end end
# Returns a representation of a PromtheusMetric
# as a dashboard panel. As the panel is generated
# on the fly, we're using default values for info
# not represented in the DB.
#
# @return [Hash]
def panel_for_metric(metric)
{
type: DEFAULT_PANEL_TYPE,
weight: DEFAULT_PANEL_WEIGHT,
title: metric.title,
y_label: metric.y_label,
metrics: [metric.to_metric_hash]
}
end
end end
end end
end end
...@@ -10,7 +10,10 @@ module Metrics ...@@ -10,7 +10,10 @@ module Metrics
class GitlabAlertEmbedService < ::Metrics::Dashboard::BaseEmbedService class GitlabAlertEmbedService < ::Metrics::Dashboard::BaseEmbedService
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
SEQUENCE = [STAGES::EndpointInserter].freeze SEQUENCE = [
STAGES::EndpointInserter,
STAGES::PanelIdsInserter
].freeze
class << self class << self
# Determines whether the provided params are sufficient # Determines whether the provided params are sufficient
......
...@@ -10,7 +10,8 @@ module Metrics ...@@ -10,7 +10,8 @@ module Metrics
include ReactiveCaching include ReactiveCaching
SEQUENCE = [ SEQUENCE = [
::Gitlab::Metrics::Dashboard::Stages::GrafanaFormatter ::Gitlab::Metrics::Dashboard::Stages::GrafanaFormatter,
::Gitlab::Metrics::Dashboard::Stages::PanelIdsInserter
].freeze ].freeze
self.reactive_cache_key = ->(service) { service.cache_key } self.reactive_cache_key = ->(service) { service.cache_key }
......
...@@ -11,6 +11,7 @@ module Metrics ...@@ -11,6 +11,7 @@ module Metrics
SEQUENCE = [ SEQUENCE = [
STAGES::EndpointInserter, STAGES::EndpointInserter,
STAGES::PanelIdsInserter,
STAGES::Sorter STAGES::Sorter
].freeze ].freeze
......
...@@ -11,6 +11,7 @@ module Metrics ...@@ -11,6 +11,7 @@ module Metrics
SEQUENCE = [ SEQUENCE = [
STAGES::CustomMetricsInserter, STAGES::CustomMetricsInserter,
STAGES::EndpointInserter, STAGES::EndpointInserter,
STAGES::PanelIdsInserter,
STAGES::Sorter STAGES::Sorter
].freeze ].freeze
......
...@@ -13,6 +13,7 @@ module Metrics ...@@ -13,6 +13,7 @@ module Metrics
STAGES::CustomMetricsInserter, STAGES::CustomMetricsInserter,
STAGES::CustomMetricsDetailsInserter, STAGES::CustomMetricsDetailsInserter,
STAGES::EndpointInserter, STAGES::EndpointInserter,
STAGES::PanelIdsInserter,
STAGES::Sorter STAGES::Sorter
].freeze ].freeze
......
...@@ -3,11 +3,6 @@ ...@@ -3,11 +3,6 @@
class SearchService class SearchService
include Gitlab::Allowable include Gitlab::Allowable
REDACTABLE_RESULTS = [
ActiveRecord::Relation,
Gitlab::Search::FoundBlob
].freeze
SEARCH_TERM_LIMIT = 64 SEARCH_TERM_LIMIT = 64
SEARCH_CHAR_LIMIT = 4096 SEARCH_CHAR_LIMIT = 4096
...@@ -68,10 +63,6 @@ class SearchService ...@@ -68,10 +63,6 @@ class SearchService
@search_objects ||= redact_unauthorized_results(search_results.objects(scope, params[:page])) @search_objects ||= redact_unauthorized_results(search_results.objects(scope, params[:page]))
end end
def redactable_results
REDACTABLE_RESULTS
end
private private
def visible_result?(object) def visible_result?(object)
...@@ -80,12 +71,9 @@ class SearchService ...@@ -80,12 +71,9 @@ class SearchService
Ability.allowed?(current_user, :"read_#{object.to_ability_name}", object) Ability.allowed?(current_user, :"read_#{object.to_ability_name}", object)
end end
def redact_unauthorized_results(results) def redact_unauthorized_results(results_collection)
return results unless redactable_results.any? { |redactable| results.is_a?(redactable) } results = results_collection.to_a
permitted_results = results.select { |object| visible_result?(object) }
permitted_results = results.select do |object|
visible_result?(object)
end
filtered_results = (results - permitted_results).each_with_object({}) do |object, memo| filtered_results = (results - permitted_results).each_with_object({}) do |object, memo|
memo[object.id] = { ability: :"read_#{object.to_ability_name}", id: object.id, class_name: object.class.name } memo[object.id] = { ability: :"read_#{object.to_ability_name}", id: object.id, class_name: object.class.name }
...@@ -93,13 +81,13 @@ class SearchService ...@@ -93,13 +81,13 @@ class SearchService
log_redacted_search_results(filtered_results.values) if filtered_results.any? log_redacted_search_results(filtered_results.values) if filtered_results.any?
return results.id_not_in(filtered_results.keys) if results.is_a?(ActiveRecord::Relation) return results_collection.id_not_in(filtered_results.keys) if results_collection.is_a?(ActiveRecord::Relation)
Kaminari.paginate_array( Kaminari.paginate_array(
permitted_results, permitted_results,
total_count: results.total_count, total_count: results_collection.total_count,
limit: results.limit_value, limit: results_collection.limit_value,
offset: results.offset_value offset: results_collection.offset_value
) )
end end
......
...@@ -4,7 +4,7 @@ module Tags ...@@ -4,7 +4,7 @@ module Tags
class CreateService < BaseService class CreateService < BaseService
def execute(tag_name, target, message) def execute(tag_name, target, message)
valid_tag = Gitlab::GitRefValidator.validate(tag_name) valid_tag = Gitlab::GitRefValidator.validate(tag_name)
return error('Tag name invalid') unless valid_tag return error('Tag name invalid', 400) unless valid_tag
repository = project.repository repository = project.repository
message = message&.strip message = message&.strip
...@@ -14,7 +14,7 @@ module Tags ...@@ -14,7 +14,7 @@ module Tags
begin begin
new_tag = repository.add_tag(current_user, tag_name, target, message) new_tag = repository.add_tag(current_user, tag_name, target, message)
rescue Gitlab::Git::Repository::TagExistsError rescue Gitlab::Git::Repository::TagExistsError
return error("Tag #{tag_name} already exists") return error("Tag #{tag_name} already exists", 409)
rescue Gitlab::Git::PreReceiveError => ex rescue Gitlab::Git::PreReceiveError => ex
return error(ex.message) return error(ex.message)
end end
...@@ -24,7 +24,7 @@ module Tags ...@@ -24,7 +24,7 @@ module Tags
success.merge(tag: new_tag) success.merge(tag: new_tag)
else else
error("Target #{target} is invalid") error("Target #{target} is invalid", 400)
end end
end end
end end
......
...@@ -137,8 +137,6 @@ module ObjectStorage ...@@ -137,8 +137,6 @@ module ObjectStorage
included do |base| included do |base|
base.include(ObjectStorage) base.include(ObjectStorage)
include_if_ee('::EE::ObjectStorage::Concern') # rubocop: disable Cop/InjectEnterpriseEditionModule
after :migrate, :delete_migrated_file after :migrate, :delete_migrated_file
end end
...@@ -463,3 +461,5 @@ module ObjectStorage ...@@ -463,3 +461,5 @@ module ObjectStorage
end end
end end
end end
ObjectStorage::Concern.include_if_ee('::EE::ObjectStorage::Concern')
...@@ -41,7 +41,12 @@ ...@@ -41,7 +41,12 @@
.js-serverless-survey-banner{ data: { user_name: current_user.name, user_email: current_user.email } } .js-serverless-survey-banner{ data: { user_name: current_user.name, user_email: current_user.email } }
%h4= @cluster.name .d-flex.my-3
%p.badge.badge-light.p-2.mr-2
= cluster_type_label(@cluster.cluster_type)
%h4.m-0
= @cluster.name
= render 'banner' = render 'banner'
- if cluster_created?(@cluster) - if cluster_created?(@cluster)
...@@ -56,7 +61,3 @@ ...@@ -56,7 +61,3 @@
.tab-content.py-3 .tab-content.py-3
.tab-pane.active{ role: 'tabpanel' } .tab-pane.active{ role: 'tabpanel' }
= render_cluster_info_tab_content(params[:tab], expanded_by_default?) = render_cluster_info_tab_content(params[:tab], expanded_by_default?)
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment