Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
c417764f00
commit
0254867cf0
2
Gemfile
2
Gemfile
|
|
@ -164,6 +164,7 @@ gem 'diff_match_patch', '~> 0.1.0'
|
|||
|
||||
# Application server
|
||||
gem 'rack', '~> 2.0.9'
|
||||
gem 'rack-timeout', '~> 0.5.1'
|
||||
|
||||
group :unicorn do
|
||||
gem 'unicorn', '~> 5.5'
|
||||
|
|
@ -173,7 +174,6 @@ end
|
|||
group :puma do
|
||||
gem 'gitlab-puma', '~> 4.3.3.gitlab.2', require: false
|
||||
gem 'gitlab-puma_worker_killer', '~> 0.1.1.gitlab.1', require: false
|
||||
gem 'rack-timeout', require: false
|
||||
end
|
||||
|
||||
# State machine
|
||||
|
|
|
|||
|
|
@ -817,7 +817,7 @@ GEM
|
|||
rack
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-timeout (0.5.1)
|
||||
rack-timeout (0.5.2)
|
||||
rails (6.0.3.1)
|
||||
actioncable (= 6.0.3.1)
|
||||
actionmailbox (= 6.0.3.1)
|
||||
|
|
@ -1350,7 +1350,7 @@ DEPENDENCIES
|
|||
rack-cors (~> 1.0.6)
|
||||
rack-oauth2 (~> 1.9.3)
|
||||
rack-proxy (~> 0.6.0)
|
||||
rack-timeout
|
||||
rack-timeout (~> 0.5.1)
|
||||
rails (~> 6.0.3.1)
|
||||
rails-controller-testing
|
||||
rails-i18n (~> 6.0)
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ export default class Clusters {
|
|||
initRemoveClusterActions() {
|
||||
const el = document.querySelector('#js-cluster-remove-actions');
|
||||
if (el && el.dataset) {
|
||||
const { clusterName, clusterPath } = el.dataset;
|
||||
const { clusterName, clusterPath, hasManagementProject } = el.dataset;
|
||||
|
||||
this.removeClusterAction = new Vue({
|
||||
el,
|
||||
|
|
@ -231,6 +231,7 @@ export default class Clusters {
|
|||
props: {
|
||||
clusterName,
|
||||
clusterPath,
|
||||
hasManagementProject,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { escape } from 'lodash';
|
||||
import SplitButton from '~/vue_shared/components/split_button.vue';
|
||||
import { GlModal, GlDeprecatedButton, GlFormInput } from '@gitlab/ui';
|
||||
import { GlModal, GlButton, GlDeprecatedButton, GlFormInput } from '@gitlab/ui';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import csrf from '~/lib/utils/csrf';
|
||||
|
||||
|
|
@ -27,6 +27,7 @@ export default {
|
|||
components: {
|
||||
SplitButton,
|
||||
GlModal,
|
||||
GlButton,
|
||||
GlDeprecatedButton,
|
||||
GlFormInput,
|
||||
},
|
||||
|
|
@ -39,6 +40,10 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
hasManagementProject: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -90,6 +95,9 @@ export default {
|
|||
canSubmit() {
|
||||
return this.enteredClusterName === this.clusterName;
|
||||
},
|
||||
canCleanupResources() {
|
||||
return !this.hasManagementProject;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleClickRemoveCluster(cleanup = false) {
|
||||
|
|
@ -112,12 +120,21 @@ export default {
|
|||
<template>
|
||||
<div>
|
||||
<split-button
|
||||
v-if="canCleanupResources"
|
||||
:action-items="$options.splitButtonActionItems"
|
||||
menu-class="dropdown-menu-large"
|
||||
variant="danger"
|
||||
@remove-cluster="handleClickRemoveCluster(false)"
|
||||
@remove-cluster-and-cleanup="handleClickRemoveCluster(true)"
|
||||
/>
|
||||
<gl-button
|
||||
v-else
|
||||
variant="danger"
|
||||
data-testid="btnRemove"
|
||||
@click="handleClickRemoveCluster(false)"
|
||||
>
|
||||
{{ s__('ClusterIntegration|Remove integration') }}
|
||||
</gl-button>
|
||||
<gl-modal
|
||||
ref="modal"
|
||||
size="lg"
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ export default {
|
|||
issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
|
||||
subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
|
||||
health_status: this.form.find('input[name="update[health_status]"]').val(),
|
||||
epic_id: this.form.find('input[name="update[epic_id]"]').val(),
|
||||
add_label_ids: [],
|
||||
remove_label_ids: [],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -71,6 +71,14 @@ export default class IssuableBulkUpdateSidebar {
|
|||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
if (IS_EE) {
|
||||
import('ee/vue_shared/components/sidebar/epics_select/epics_select_bundle')
|
||||
.then(({ default: EpicSelect }) => {
|
||||
EpicSelect();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
setupBulkUpdateActions() {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import Icon from '~/vue_shared/components/icon.vue';
|
|||
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
|
||||
|
||||
import DashboardsDropdown from './dashboards_dropdown.vue';
|
||||
import RefreshButton from './refresh_button.vue';
|
||||
|
||||
import TrackEventDirective from '~/vue_shared/directives/track_event';
|
||||
import { getAddMetricTrackingOptions, timeRangeToUrl } from '../utils';
|
||||
|
|
@ -44,6 +45,7 @@ export default {
|
|||
|
||||
DateTimePicker,
|
||||
DashboardsDropdown,
|
||||
RefreshButton,
|
||||
},
|
||||
directives: {
|
||||
GlModal: GlModalDirective,
|
||||
|
|
@ -129,11 +131,7 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('monitoringDashboard', [
|
||||
'filterEnvironments',
|
||||
'fetchDashboardData',
|
||||
'toggleStarredValue',
|
||||
]),
|
||||
...mapActions('monitoringDashboard', ['filterEnvironments', 'toggleStarredValue']),
|
||||
selectDashboard(dashboard) {
|
||||
const params = {
|
||||
dashboard: encodeURIComponent(dashboard.path),
|
||||
|
|
@ -149,9 +147,6 @@ export default {
|
|||
onDateTimePickerInvalid() {
|
||||
this.$emit('dateTimePickerInvalid');
|
||||
},
|
||||
refreshDashboard() {
|
||||
this.fetchDashboardData();
|
||||
},
|
||||
|
||||
toggleRearrangingPanels() {
|
||||
this.$emit('setRearrangingPanels', !this.isRearrangingPanels);
|
||||
|
|
@ -252,16 +247,7 @@ export default {
|
|||
</div>
|
||||
|
||||
<div class="mb-2 pr-2 d-flex d-sm-block">
|
||||
<gl-deprecated-button
|
||||
ref="refreshDashboardBtn"
|
||||
v-gl-tooltip
|
||||
class="flex-grow-1"
|
||||
variant="default"
|
||||
:title="s__('Metrics|Refresh dashboard')"
|
||||
@click="refreshDashboard"
|
||||
>
|
||||
<icon name="retry" />
|
||||
</gl-deprecated-button>
|
||||
<refresh-button />
|
||||
</div>
|
||||
|
||||
<div class="flex-grow-1"></div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,163 @@
|
|||
<script>
|
||||
import { n__, __ } from '~/locale';
|
||||
import { mapActions } from 'vuex';
|
||||
|
||||
import {
|
||||
GlButtonGroup,
|
||||
GlButton,
|
||||
GlNewDropdown,
|
||||
GlNewDropdownItem,
|
||||
GlNewDropdownDivider,
|
||||
GlTooltipDirective,
|
||||
} from '@gitlab/ui';
|
||||
|
||||
const makeInterval = (length = 0, unit = 's') => {
|
||||
const shortLabel = `${length}${unit}`;
|
||||
switch (unit) {
|
||||
case 'd':
|
||||
return {
|
||||
interval: length * 24 * 60 * 60 * 1000,
|
||||
shortLabel,
|
||||
label: n__('%d day', '%d days', length),
|
||||
};
|
||||
case 'h':
|
||||
return {
|
||||
interval: length * 60 * 60 * 1000,
|
||||
shortLabel,
|
||||
label: n__('%d hour', '%d hours', length),
|
||||
};
|
||||
case 'm':
|
||||
return {
|
||||
interval: length * 60 * 1000,
|
||||
shortLabel,
|
||||
label: n__('%d minute', '%d minutes', length),
|
||||
};
|
||||
case 's':
|
||||
default:
|
||||
return {
|
||||
interval: length * 1000,
|
||||
shortLabel,
|
||||
label: n__('%d second', '%d seconds', length),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButtonGroup,
|
||||
GlButton,
|
||||
GlNewDropdown,
|
||||
GlNewDropdownItem,
|
||||
GlNewDropdownDivider,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
refreshInterval: null,
|
||||
timeoutId: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
dropdownText() {
|
||||
return this.refreshInterval?.shortLabel ?? __('Off');
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
refreshInterval() {
|
||||
if (this.refreshInterval !== null) {
|
||||
this.startAutoRefresh();
|
||||
} else {
|
||||
this.stopAutoRefresh();
|
||||
}
|
||||
},
|
||||
},
|
||||
destroyed() {
|
||||
this.stopAutoRefresh();
|
||||
},
|
||||
methods: {
|
||||
...mapActions('monitoringDashboard', ['fetchDashboardData']),
|
||||
|
||||
refresh() {
|
||||
this.fetchDashboardData();
|
||||
},
|
||||
startAutoRefresh() {
|
||||
const schedule = () => {
|
||||
if (this.refreshInterval) {
|
||||
this.timeoutId = setTimeout(this.startAutoRefresh, this.refreshInterval.interval);
|
||||
}
|
||||
};
|
||||
|
||||
this.stopAutoRefresh();
|
||||
if (document.hidden) {
|
||||
// Inactive tab? Skip fetch and schedule again
|
||||
schedule();
|
||||
} else {
|
||||
// Active tab! Fetch data and then schedule when settled
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
this.fetchDashboardData().finally(schedule);
|
||||
}
|
||||
},
|
||||
stopAutoRefresh() {
|
||||
clearTimeout(this.timeoutId);
|
||||
this.timeoutId = null;
|
||||
},
|
||||
|
||||
setRefreshInterval(option) {
|
||||
this.refreshInterval = option;
|
||||
},
|
||||
removeRefreshInterval() {
|
||||
this.refreshInterval = null;
|
||||
},
|
||||
isChecked(option) {
|
||||
if (this.refreshInterval) {
|
||||
return option.interval === this.refreshInterval.interval;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
|
||||
refreshIntervals: [
|
||||
makeInterval(5),
|
||||
makeInterval(10),
|
||||
makeInterval(30),
|
||||
makeInterval(5, 'm'),
|
||||
makeInterval(30, 'm'),
|
||||
makeInterval(1, 'h'),
|
||||
makeInterval(2, 'h'),
|
||||
makeInterval(12, 'h'),
|
||||
makeInterval(1, 'd'),
|
||||
],
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-button-group>
|
||||
<gl-button
|
||||
v-gl-tooltip
|
||||
class="gl-flex-grow-1"
|
||||
variant="default"
|
||||
:title="s__('Metrics|Refresh dashboard')"
|
||||
icon="retry"
|
||||
@click="refresh"
|
||||
/>
|
||||
<gl-new-dropdown v-gl-tooltip :title="s__('Metrics|Set refresh rate')" :text="dropdownText">
|
||||
<gl-new-dropdown-item
|
||||
:is-check-item="true"
|
||||
:is-checked="refreshInterval === null"
|
||||
@click="removeRefreshInterval()"
|
||||
>{{ __('Off') }}</gl-new-dropdown-item
|
||||
>
|
||||
<gl-new-dropdown-divider />
|
||||
<gl-new-dropdown-item
|
||||
v-for="(option, i) in $options.refreshIntervals"
|
||||
:key="i"
|
||||
:is-check-item="true"
|
||||
:is-checked="isChecked(option)"
|
||||
@click="setRefreshInterval(option)"
|
||||
>{{ option.label }}</gl-new-dropdown-item
|
||||
>
|
||||
</gl-new-dropdown>
|
||||
</gl-button-group>
|
||||
</template>
|
||||
|
|
@ -12,6 +12,7 @@ const successMessageSelector = '.validation-success';
|
|||
const pendingMessageSelector = '.validation-pending';
|
||||
const unavailableMessageSelector = '.validation-error';
|
||||
const suggestionsMessageSelector = '.gl-path-suggestions';
|
||||
const inputGroupSelector = '.input-group';
|
||||
|
||||
export default class GroupPathValidator extends InputValidator {
|
||||
constructor(opts = {}) {
|
||||
|
|
@ -39,7 +40,7 @@ export default class GroupPathValidator extends InputValidator {
|
|||
static validateGroupPathInput(inputDomElement) {
|
||||
const groupPath = inputDomElement.value;
|
||||
|
||||
if (inputDomElement.checkValidity() && groupPath.length > 0) {
|
||||
if (inputDomElement.checkValidity() && groupPath.length > 1) {
|
||||
GroupPathValidator.setMessageVisibility(inputDomElement, pendingMessageSelector);
|
||||
|
||||
fetchGroupPathAvailability(groupPath)
|
||||
|
|
@ -69,9 +70,10 @@ export default class GroupPathValidator extends InputValidator {
|
|||
}
|
||||
|
||||
static setMessageVisibility(inputDomElement, messageSelector, isVisible = true) {
|
||||
const messageElement = inputDomElement.parentElement.parentElement.querySelector(
|
||||
messageSelector,
|
||||
);
|
||||
const messageElement = inputDomElement
|
||||
.closest(inputGroupSelector)
|
||||
.parentElement.querySelector(messageSelector);
|
||||
|
||||
messageElement.classList.toggle('hide', !isVisible);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export default {
|
|||
)
|
||||
}}
|
||||
<gl-link ref="accessibility-issue-learn-more" :href="learnMoreUrl" target="_blank">{{
|
||||
s__('AccessibilityReport|Learn More')
|
||||
s__('AccessibilityReport|Learn more')
|
||||
}}</gl-link>
|
||||
</div>
|
||||
{{ sprintf(s__('AccessibilityReport|Message: %{message}'), { message: issue.message }) }}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
|
|||
import { __ } from '../../locale';
|
||||
import Icon from '../../vue_shared/components/icon.vue';
|
||||
import getRefMixin from '../mixins/get_ref';
|
||||
import getProjectShortPath from '../queries/getProjectShortPath.query.graphql';
|
||||
import getProjectPath from '../queries/getProjectPath.query.graphql';
|
||||
import getPermissions from '../queries/getPermissions.query.graphql';
|
||||
import projectShortPathQuery from '../queries/project_short_path.query.graphql';
|
||||
import projetPathQuery from '../queries/project_path.query.graphql';
|
||||
import permissionsQuery from '../queries/permissions.query.graphql';
|
||||
|
||||
const ROW_TYPES = {
|
||||
header: 'header',
|
||||
|
|
@ -23,13 +23,13 @@ export default {
|
|||
},
|
||||
apollo: {
|
||||
projectShortPath: {
|
||||
query: getProjectShortPath,
|
||||
query: projectShortPathQuery,
|
||||
},
|
||||
projectPath: {
|
||||
query: getProjectPath,
|
||||
query: projetPathQuery,
|
||||
},
|
||||
userPermissions: {
|
||||
query: getPermissions,
|
||||
query: permissionsQuery,
|
||||
variables() {
|
||||
return {
|
||||
projectPath: this.projectPath,
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
|
|||
import CiIcon from '../../vue_shared/components/ci_icon.vue';
|
||||
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
|
||||
import getRefMixin from '../mixins/get_ref';
|
||||
import getProjectPath from '../queries/getProjectPath.query.graphql';
|
||||
import pathLastCommit from '../queries/pathLastCommit.query.graphql';
|
||||
import projectPathQuery from '../queries/project_path.query.graphql';
|
||||
import pathLastCommitQuery from '../queries/path_last_commit.query.graphql';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
@ -28,10 +28,10 @@ export default {
|
|||
mixins: [getRefMixin],
|
||||
apollo: {
|
||||
projectPath: {
|
||||
query: getProjectPath,
|
||||
query: projectPathQuery,
|
||||
},
|
||||
commit: {
|
||||
query: pathLastCommit,
|
||||
query: pathLastCommitQuery,
|
||||
variables() {
|
||||
return {
|
||||
projectPath: this.projectPath,
|
||||
|
|
@ -102,7 +102,7 @@ export default {
|
|||
<template v-else-if="commit">
|
||||
<user-avatar-link
|
||||
v-if="commit.author"
|
||||
:link-href="commit.author.webUrl"
|
||||
:link-href="commit.author.webPath"
|
||||
:img-src="commit.author.avatarUrl"
|
||||
:img-size="40"
|
||||
class="avatar-cell"
|
||||
|
|
@ -118,7 +118,7 @@ export default {
|
|||
<div class="commit-detail flex-list">
|
||||
<div class="commit-content qa-commit-content">
|
||||
<gl-link
|
||||
:href="commit.webUrl"
|
||||
:href="commit.webPath"
|
||||
:class="{ 'font-italic': !commit.message }"
|
||||
class="commit-row-message item-title"
|
||||
v-html="commit.titleHtml"
|
||||
|
|
@ -135,7 +135,7 @@ export default {
|
|||
<div class="committer">
|
||||
<gl-link
|
||||
v-if="commit.author"
|
||||
:href="commit.author.webUrl"
|
||||
:href="commit.author.webPath"
|
||||
class="commit-author-link js-user-link"
|
||||
>
|
||||
{{ commit.author.name }}
|
||||
|
|
|
|||
|
|
@ -3,15 +3,15 @@ import $ from 'jquery';
|
|||
import '~/behaviors/markdown/render_gfm';
|
||||
import { GlLink, GlLoadingIcon } from '@gitlab/ui';
|
||||
import { handleLocationHash } from '~/lib/utils/common_utils';
|
||||
import getReadmeQuery from '../../queries/getReadme.query.graphql';
|
||||
import readmeQery from '../../queries/readme.query.graphql';
|
||||
|
||||
export default {
|
||||
apollo: {
|
||||
readme: {
|
||||
query: getReadmeQuery,
|
||||
query: readmeQery,
|
||||
variables() {
|
||||
return {
|
||||
url: this.blob.webUrl,
|
||||
url: this.blob.webPath,
|
||||
};
|
||||
},
|
||||
loadingKey: 'loading',
|
||||
|
|
@ -51,7 +51,7 @@ export default {
|
|||
<div class="js-file-title file-title-flex-parent">
|
||||
<div class="file-header-content">
|
||||
<i aria-hidden="true" class="fa fa-file-text-o fa-fw"></i>
|
||||
<gl-link :href="blob.webUrl">
|
||||
<gl-link :href="blob.webPath">
|
||||
<strong>{{ blob.name }}</strong>
|
||||
</gl-link>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { GlSkeletonLoading } from '@gitlab/ui';
|
||||
import { sprintf, __ } from '../../../locale';
|
||||
import getRefMixin from '../../mixins/get_ref';
|
||||
import getProjectPath from '../../queries/getProjectPath.query.graphql';
|
||||
import projectPathQuery from '../../queries/project_path.query.graphql';
|
||||
import TableHeader from './header.vue';
|
||||
import TableRow from './row.vue';
|
||||
import ParentRow from './parent_row.vue';
|
||||
|
|
@ -17,7 +17,7 @@ export default {
|
|||
mixins: [getRefMixin],
|
||||
apollo: {
|
||||
projectPath: {
|
||||
query: getProjectPath,
|
||||
query: projectPathQuery,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
|
|
@ -96,7 +96,7 @@ export default {
|
|||
:name="entry.name"
|
||||
:path="entry.flatPath"
|
||||
:type="entry.type"
|
||||
:url="entry.webUrl"
|
||||
:url="entry.webUrl || entry.webPath"
|
||||
:submodule-tree-url="entry.treeUrl"
|
||||
:lfs-oid="entry.lfsOid"
|
||||
:loading-path="loadingPath"
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { escapeFileUrl } from '~/lib/utils/url_utility';
|
|||
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import FileIcon from '~/vue_shared/components/file_icon.vue';
|
||||
import getRefMixin from '../../mixins/get_ref';
|
||||
import getCommit from '../../queries/getCommit.query.graphql';
|
||||
import commitQuery from '../../queries/commit.query.graphql';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
@ -29,7 +29,7 @@ export default {
|
|||
},
|
||||
apollo: {
|
||||
commit: {
|
||||
query: getCommit,
|
||||
query: commitQuery,
|
||||
variables() {
|
||||
return {
|
||||
fileName: this.name,
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import createFlash from '~/flash';
|
|||
import { __ } from '../../locale';
|
||||
import FileTable from './table/index.vue';
|
||||
import getRefMixin from '../mixins/get_ref';
|
||||
import getFiles from '../queries/getFiles.query.graphql';
|
||||
import getProjectPath from '../queries/getProjectPath.query.graphql';
|
||||
import getVueFileListLfsBadge from '../queries/getVueFileListLfsBadge.query.graphql';
|
||||
import filesQuery from '../queries/files.query.graphql';
|
||||
import projectPathQuery from '../queries/project_path.query.graphql';
|
||||
import vueFileListLfsBadgeQuery from '../queries/vue_file_list_lfs_badge.query.graphql';
|
||||
import FilePreview from './preview/index.vue';
|
||||
import { readmeFile } from '../utils/readme';
|
||||
|
||||
|
|
@ -19,10 +19,10 @@ export default {
|
|||
mixins: [getRefMixin],
|
||||
apollo: {
|
||||
projectPath: {
|
||||
query: getProjectPath,
|
||||
query: projectPathQuery,
|
||||
},
|
||||
vueFileListLfsBadge: {
|
||||
query: getVueFileListLfsBadge,
|
||||
query: vueFileListLfsBadgeQuery,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
|
|
@ -75,7 +75,7 @@ export default {
|
|||
|
||||
return this.$apollo
|
||||
.query({
|
||||
query: getFiles,
|
||||
query: filesQuery,
|
||||
variables: {
|
||||
projectPath: this.projectPath,
|
||||
ref: this.ref,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { normalizeData } from 'ee_else_ce/repository/utils/commit';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import getCommits from './queries/getCommits.query.graphql';
|
||||
import getProjectPath from './queries/getProjectPath.query.graphql';
|
||||
import getRef from './queries/getRef.query.graphql';
|
||||
import commitsQuery from './queries/commits.query.graphql';
|
||||
import projectPathQuery from './queries/project_path.query.graphql';
|
||||
import refQuery from './queries/ref.query.graphql';
|
||||
|
||||
let fetchpromise;
|
||||
let resolvers = [];
|
||||
|
|
@ -22,8 +22,8 @@ export function fetchLogsTree(client, path, offset, resolver = null) {
|
|||
|
||||
if (fetchpromise) return fetchpromise;
|
||||
|
||||
const { projectPath } = client.readQuery({ query: getProjectPath });
|
||||
const { escapedRef } = client.readQuery({ query: getRef });
|
||||
const { projectPath } = client.readQuery({ query: projectPathQuery });
|
||||
const { escapedRef } = client.readQuery({ query: refQuery });
|
||||
|
||||
fetchpromise = axios
|
||||
.get(
|
||||
|
|
@ -36,10 +36,10 @@ export function fetchLogsTree(client, path, offset, resolver = null) {
|
|||
)
|
||||
.then(({ data, headers }) => {
|
||||
const headerLogsOffset = headers['more-logs-offset'];
|
||||
const { commits } = client.readQuery({ query: getCommits });
|
||||
const { commits } = client.readQuery({ query: commitsQuery });
|
||||
const newCommitData = [...commits, ...normalizeData(data, path)];
|
||||
client.writeQuery({
|
||||
query: getCommits,
|
||||
query: commitsQuery,
|
||||
data: { commits: newCommitData },
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import getRef from '../queries/getRef.query.graphql';
|
||||
import refQuery from '../queries/ref.query.graphql';
|
||||
|
||||
export default {
|
||||
apollo: {
|
||||
ref: {
|
||||
query: getRef,
|
||||
query: refQuery,
|
||||
manual: true,
|
||||
result({ data, loading }) {
|
||||
if (!loading) {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import getFiles from '../queries/getFiles.query.graphql';
|
||||
import filesQuery from '../queries/files.query.graphql';
|
||||
import getRefMixin from './get_ref';
|
||||
import getProjectPath from '../queries/getProjectPath.query.graphql';
|
||||
import projectPathQuery from '../queries/project_path.query.graphql';
|
||||
|
||||
export default {
|
||||
mixins: [getRefMixin],
|
||||
apollo: {
|
||||
projectPath: {
|
||||
query: getProjectPath,
|
||||
query: projectPathQuery,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
|
|
@ -21,7 +21,7 @@ export default {
|
|||
|
||||
return this.$apollo
|
||||
.query({
|
||||
query: getFiles,
|
||||
query: filesQuery,
|
||||
variables: {
|
||||
projectPath: this.projectPath,
|
||||
ref: this.ref,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#import "ee_else_ce/repository/queries/commit.fragment.graphql"
|
||||
|
||||
query getCommit($fileName: String!, $type: String!, $path: String!) {
|
||||
query Commit($fileName: String!, $type: String!, $path: String!) {
|
||||
commit(path: $path, fileName: $fileName, type: $type) @client {
|
||||
...TreeEntryCommit
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
#import "ee_else_ce/repository/queries/commit.fragment.graphql"
|
||||
|
||||
query getCommits {
|
||||
query Commits {
|
||||
commits @client {
|
||||
...TreeEntryCommit
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ fragment TreeEntry on Entry {
|
|||
type
|
||||
}
|
||||
|
||||
query getFiles(
|
||||
query Files(
|
||||
$projectPath: ID!
|
||||
$path: String
|
||||
$ref: String!
|
||||
|
|
@ -23,7 +23,7 @@ query getFiles(
|
|||
edges {
|
||||
node {
|
||||
...TreeEntry
|
||||
webUrl
|
||||
webPath
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
|
|
@ -46,7 +46,7 @@ query getFiles(
|
|||
edges {
|
||||
node {
|
||||
...TreeEntry
|
||||
webUrl
|
||||
webPath
|
||||
lfsOid @include(if: $vueLfsEnabled)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
query getProjectPath {
|
||||
projectPath
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
|
||||
query PathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
|
||||
project(fullPath: $projectPath) {
|
||||
repository {
|
||||
tree(path: $path, ref: $ref) {
|
||||
|
|
@ -8,14 +8,14 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
|
|||
titleHtml
|
||||
description
|
||||
message
|
||||
webUrl
|
||||
webPath
|
||||
authoredDate
|
||||
authorName
|
||||
authorGravatar
|
||||
author {
|
||||
name
|
||||
avatarUrl
|
||||
webUrl
|
||||
webPath
|
||||
}
|
||||
signatureHtml
|
||||
pipelines(ref: $ref, first: 1) {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
query getPermissions($projectPath: ID!) {
|
||||
query Permissions($projectPath: ID!) {
|
||||
project(fullPath: $projectPath) {
|
||||
userPermissions {
|
||||
pushCode
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
query ProjectPath {
|
||||
projectPath
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
query getProjectShortPath {
|
||||
query ProjectShortPath {
|
||||
projectShortPath @client
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
query getReadme($url: String!) {
|
||||
query Readme($url: String!) {
|
||||
readme(url: $url) @client {
|
||||
html
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
query getRef {
|
||||
query Ref {
|
||||
ref @client
|
||||
escapedRef @client
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
query getVueFileListLfsBadge {
|
||||
query VueFileListLfsBadge {
|
||||
vueFileListLfsBadge @client
|
||||
}
|
||||
|
|
@ -1089,3 +1089,11 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bulk-update {
|
||||
.dropdown-toggle-text {
|
||||
&.is-default {
|
||||
color: $gl-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,12 @@
|
|||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.bulk-update {
|
||||
.filter-item {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sort-filter {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@
|
|||
color: $sidebar-text;
|
||||
}
|
||||
|
||||
svg {
|
||||
.nav-icon-container svg {
|
||||
fill: $sidebar-text;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
}
|
||||
|
||||
&.status-box-issue-closed,
|
||||
&.status-box-alert-resolved,
|
||||
&.status-box-mr-merged {
|
||||
background-color: $blue-500;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class InvitesController < ApplicationController
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
before_action :member
|
||||
skip_before_action :authenticate_user!, only: :decline
|
||||
|
||||
helper_method :member?, :current_user_matches_invite?
|
||||
|
||||
respond_to :html
|
||||
|
||||
def show
|
||||
accept if skip_invitation_prompt?
|
||||
end
|
||||
|
||||
def accept
|
||||
|
|
@ -38,6 +43,20 @@ class InvitesController < ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def skip_invitation_prompt?
|
||||
!member? && current_user_matches_invite?
|
||||
end
|
||||
|
||||
def current_user_matches_invite?
|
||||
@member.invite_email == current_user.email
|
||||
end
|
||||
|
||||
def member?
|
||||
strong_memoize(:is_member) do
|
||||
@member.source.users.include?(current_user)
|
||||
end
|
||||
end
|
||||
|
||||
def member
|
||||
return @member if defined?(@member)
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
# created_before: datetime
|
||||
# updated_after: datetime
|
||||
# updated_before: datetime
|
||||
# confidential: boolean
|
||||
#
|
||||
class IssuesFinder < IssuableFinder
|
||||
CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ module Mutations
|
|||
|
||||
ERROR_MESSAGE = 'You cannot perform write operations on a read-only instance'
|
||||
|
||||
field_class ::Types::BaseField
|
||||
|
||||
field :errors, [GraphQL::STRING_TYPE],
|
||||
null: false,
|
||||
description: 'Errors encountered during execution of the mutation.'
|
||||
|
|
|
|||
|
|
@ -10,8 +10,13 @@ module Mutations
|
|||
field :updated_ids,
|
||||
[GraphQL::ID_TYPE],
|
||||
null: false,
|
||||
deprecated: { reason: 'Use todos', milestone: '13.2' },
|
||||
description: 'Ids of the updated todos'
|
||||
|
||||
field :todos, [::Types::TodoType],
|
||||
null: false,
|
||||
description: 'Updated todos'
|
||||
|
||||
def resolve
|
||||
authorize!(current_user)
|
||||
|
||||
|
|
@ -19,6 +24,7 @@ module Mutations
|
|||
|
||||
{
|
||||
updated_ids: map_to_global_ids(updated_ids),
|
||||
todos: Todo.id_in(updated_ids),
|
||||
errors: []
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,7 +14,12 @@ module Mutations
|
|||
|
||||
field :updated_ids, [GraphQL::ID_TYPE],
|
||||
null: false,
|
||||
description: 'The ids of the updated todo items'
|
||||
description: 'The ids of the updated todo items',
|
||||
deprecated: { reason: 'Use todos', milestone: '13.2' }
|
||||
|
||||
field :todos, [::Types::TodoType],
|
||||
null: false,
|
||||
description: 'Updated todos'
|
||||
|
||||
def resolve(ids:)
|
||||
check_update_amount_limit!(ids)
|
||||
|
|
@ -24,6 +29,7 @@ module Mutations
|
|||
|
||||
{
|
||||
updated_ids: gids_of(updated_ids),
|
||||
todos: Todo.id_in(updated_ids),
|
||||
errors: errors_on_objects(todos)
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ module Types
|
|||
description: 'Timestamp of when the commit was authored'
|
||||
field :web_url, type: GraphQL::STRING_TYPE, null: false,
|
||||
description: 'Web URL of the commit'
|
||||
field :web_path, type: GraphQL::STRING_TYPE, null: false,
|
||||
description: 'Web path of the commit'
|
||||
field :signature_html, type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true,
|
||||
description: 'Rendered HTML of the commit signature'
|
||||
field :author_name, type: GraphQL::STRING_TYPE, null: true,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ module Types
|
|||
|
||||
field :web_url, GraphQL::STRING_TYPE, null: true,
|
||||
description: 'Web URL of the blob'
|
||||
field :web_path, GraphQL::STRING_TYPE, null: true,
|
||||
description: 'Web path of the blob'
|
||||
field :lfs_oid, GraphQL::STRING_TYPE, null: true,
|
||||
description: 'LFS ID of the blob',
|
||||
resolve: -> (blob, args, ctx) do
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ module Types
|
|||
|
||||
field :web_url, GraphQL::STRING_TYPE, null: true,
|
||||
description: 'Web URL for the tree entry (directory)'
|
||||
field :web_path, GraphQL::STRING_TYPE, null: true,
|
||||
description: 'Web path for the tree entry (directory)'
|
||||
end
|
||||
# rubocop: enable Graphql/AuthorizeTypes
|
||||
end
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ module Types
|
|||
description: "URL of the user's avatar"
|
||||
field :web_url, GraphQL::STRING_TYPE, null: false,
|
||||
description: 'Web URL of the user'
|
||||
field :web_path, GraphQL::STRING_TYPE, null: false,
|
||||
description: 'Web path of the user'
|
||||
field :todos, Types::TodoType.connection_type, null: false,
|
||||
resolver: Resolvers::TodoResolver,
|
||||
description: 'Todos of the user'
|
||||
|
|
|
|||
|
|
@ -131,6 +131,10 @@ module ServicesHelper
|
|||
integration.configurable_events.present?
|
||||
end
|
||||
|
||||
def project_jira_issues_integration?
|
||||
false
|
||||
end
|
||||
|
||||
extend self
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -97,11 +97,13 @@ module TodosHelper
|
|||
'mr'
|
||||
when Issue
|
||||
'issue'
|
||||
when AlertManagement::Alert
|
||||
'alert'
|
||||
end
|
||||
|
||||
content_tag(:span, nil, class: 'target-status') do
|
||||
content_tag(:span, nil, class: "status-box status-box-#{type}-#{todo.target.state.dasherize}") do
|
||||
todo.target.state.capitalize
|
||||
content_tag(:span, nil, class: "status-box status-box-#{type}-#{todo.target.state.to_s.dasherize}") do
|
||||
todo.target.state.to_s.capitalize
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -214,7 +216,14 @@ module TodosHelper
|
|||
end
|
||||
|
||||
def show_todo_state?(todo)
|
||||
(todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && %w(closed merged).include?(todo.target.state)
|
||||
case todo.target
|
||||
when MergeRequest, Issue
|
||||
%w(closed merged).include?(todo.target.state)
|
||||
when AlertManagement::Alert
|
||||
%i(resolved).include?(todo.target.state)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def todo_group_options
|
||||
|
|
|
|||
|
|
@ -135,6 +135,8 @@ module AlertManagement
|
|||
scope :counts_by_status, -> { group(:status).count }
|
||||
scope :counts_by_project_id, -> { group(:project_id).count }
|
||||
|
||||
alias_method :state, :status_name
|
||||
|
||||
def self.sort_by_attribute(method)
|
||||
case method.to_s
|
||||
when 'started_at_asc' then order_start_time(:asc)
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ class AuditEvent < ApplicationRecord
|
|||
private
|
||||
|
||||
def default_author_value
|
||||
::Gitlab::Audit::NullAuthor.for(author_id, details[:author_name])
|
||||
::Gitlab::Audit::NullAuthor.for(author_id, (self[:author_name] || details[:author_name]))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -228,7 +228,9 @@ module Clusters
|
|||
def calculate_reactive_cache
|
||||
return unless enabled?
|
||||
|
||||
{ connection_status: retrieve_connection_status, nodes: retrieve_nodes }
|
||||
gitlab_kubernetes_nodes = Gitlab::Kubernetes::Node.new(self)
|
||||
|
||||
{ connection_status: retrieve_connection_status, nodes: gitlab_kubernetes_nodes.all.presence }
|
||||
end
|
||||
|
||||
def persisted_applications
|
||||
|
|
@ -383,54 +385,6 @@ module Clusters
|
|||
result[:status]
|
||||
end
|
||||
|
||||
def retrieve_nodes
|
||||
result = ::Gitlab::Kubernetes::KubeClient.graceful_request(id) { kubeclient.get_nodes }
|
||||
|
||||
return unless result[:response]
|
||||
|
||||
cluster_nodes = result[:response]
|
||||
|
||||
result = ::Gitlab::Kubernetes::KubeClient.graceful_request(id) { kubeclient.metrics_client.get_nodes }
|
||||
nodes_metrics = result[:response].to_a
|
||||
|
||||
cluster_nodes.inject([]) do |memo, node|
|
||||
sliced_node = filter_relevant_node_attributes(node)
|
||||
|
||||
matched_node_metric = nodes_metrics.find { |node_metric| node_metric.metadata.name == node.metadata.name }
|
||||
|
||||
sliced_node_metrics = matched_node_metric ? filter_relevant_node_metrics_attributes(matched_node_metric) : {}
|
||||
|
||||
memo << sliced_node.merge(sliced_node_metrics)
|
||||
end
|
||||
end
|
||||
|
||||
def filter_relevant_node_attributes(node)
|
||||
{
|
||||
'metadata' => {
|
||||
'name' => node.metadata.name
|
||||
},
|
||||
'status' => {
|
||||
'capacity' => {
|
||||
'cpu' => node.status.capacity.cpu,
|
||||
'memory' => node.status.capacity.memory
|
||||
},
|
||||
'allocatable' => {
|
||||
'cpu' => node.status.allocatable.cpu,
|
||||
'memory' => node.status.allocatable.memory
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def filter_relevant_node_metrics_attributes(node_metrics)
|
||||
{
|
||||
'usage' => {
|
||||
'cpu' => node_metrics.usage.cpu,
|
||||
'memory' => node_metrics.usage.memory
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
# To keep backward compatibility with AUTO_DEVOPS_DOMAIN
|
||||
# environment variable, we need to ensure KUBE_INGRESS_BASE_DOMAIN
|
||||
# is set if AUTO_DEVOPS_DOMAIN is set on any of the following options:
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class SystemNoteMetadata < ApplicationRecord
|
|||
designs_added designs_modified designs_removed designs_discussion_added
|
||||
title time_tracking branch milestone discussion task moved
|
||||
opened closed merged duplicate locked unlocked outdated
|
||||
tag due_date pinned_embed cherry_pick health_status approved
|
||||
tag due_date pinned_embed cherry_pick health_status approved unapproved
|
||||
].freeze
|
||||
|
||||
validates :note, presence: true
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
|
|||
Gitlab::Routing.url_helpers.project_blob_url(blob.repository.project, File.join(blob.commit_id, blob.path))
|
||||
end
|
||||
|
||||
def web_path
|
||||
Gitlab::Routing.url_helpers.project_blob_path(blob.repository.project, File.join(blob.commit_id, blob.path))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_all_blob_data
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ class CommitPresenter < Gitlab::View::Presenter::Delegated
|
|||
url_builder.build(commit)
|
||||
end
|
||||
|
||||
def web_path
|
||||
url_builder.build(commit, only_path: true)
|
||||
end
|
||||
|
||||
def signature_html
|
||||
return unless commit.has_signature?
|
||||
|
||||
|
|
|
|||
|
|
@ -6,4 +6,8 @@ class TreeEntryPresenter < Gitlab::View::Presenter::Delegated
|
|||
def web_url
|
||||
Gitlab::Routing.url_helpers.project_tree_url(tree.repository.project, File.join(tree.commit_id, tree.path))
|
||||
end
|
||||
|
||||
def web_path
|
||||
Gitlab::Routing.url_helpers.project_tree_path(tree.repository.project, File.join(tree.commit_id, tree.path))
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,4 +6,8 @@ class UserPresenter < Gitlab::View::Presenter::Delegated
|
|||
def web_url
|
||||
Gitlab::Routing.url_helpers.user_url(user)
|
||||
end
|
||||
|
||||
def web_path
|
||||
Gitlab::Routing.url_helpers.user_path(user)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ class AuditEventService
|
|||
def base_payload
|
||||
{
|
||||
author_id: @author.id,
|
||||
author_name: @author.name,
|
||||
entity_id: @entity.id,
|
||||
entity_type: @entity.class.name
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module MergeRequests
|
||||
class RemoveApprovalService < MergeRequests::BaseService
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def execute(merge_request)
|
||||
# paranoid protection against running wrong deletes
|
||||
return unless merge_request.id && current_user.id
|
||||
|
||||
approval = merge_request.approvals.where(user: current_user)
|
||||
|
||||
trigger_approval_hooks(merge_request) do
|
||||
next unless approval.destroy_all # rubocop: disable Cop/DestroyAll
|
||||
|
||||
reset_approvals_cache(merge_request)
|
||||
create_note(merge_request)
|
||||
end
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
private
|
||||
|
||||
def reset_approvals_cache(merge_request)
|
||||
merge_request.approvals.reset
|
||||
end
|
||||
|
||||
def trigger_approval_hooks(merge_request)
|
||||
yield
|
||||
|
||||
execute_hooks(merge_request, 'unapproved')
|
||||
end
|
||||
|
||||
def create_note(merge_request)
|
||||
SystemNoteService.unapprove_mr(merge_request, current_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
MergeRequests::RemoveApprovalService.prepend_if_ee('EE::MergeRequests::RemoveApprovalService')
|
||||
|
|
@ -288,6 +288,10 @@ module SystemNoteService
|
|||
merge_requests_service(noteable, noteable.project, user).approve_mr
|
||||
end
|
||||
|
||||
def unapprove_mr(noteable, user)
|
||||
merge_requests_service(noteable, noteable.project, user).unapprove_mr
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def merge_requests_service(noteable, project, author)
|
||||
|
|
|
|||
|
|
@ -163,7 +163,11 @@ module SystemNotes
|
|||
|
||||
create_note(NoteSummary.new(noteable, project, author, body, action: 'approved'))
|
||||
end
|
||||
|
||||
def unapprove_mr
|
||||
body = "unapproved this merge request"
|
||||
|
||||
create_note(NoteSummary.new(noteable, project, author, body, action: 'unapproved'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
SystemNotes::MergeRequestsService.prepend_if_ee('::EE::SystemNotes::MergeRequestsService')
|
||||
|
|
|
|||
|
|
@ -40,4 +40,6 @@
|
|||
%p
|
||||
= s_("ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster.")
|
||||
|
||||
#js-cluster-remove-actions{ data: { cluster_path: clusterable.cluster_path(@cluster), cluster_name: @cluster.name } }
|
||||
#js-cluster-remove-actions{ data: { cluster_path: clusterable.cluster_path(@cluster),
|
||||
cluster_name: @cluster.name,
|
||||
has_management_project: @cluster.management_project_id? } }
|
||||
|
|
|
|||
|
|
@ -20,21 +20,19 @@
|
|||
= link_to group.name, group_url(group)
|
||||
as #{@member.human_access}.
|
||||
|
||||
- is_member = @member.source.users.include?(current_user)
|
||||
|
||||
- if is_member
|
||||
- if member?
|
||||
%p
|
||||
- member_source = @member.source.is_a?(Group) ? _("group") : _("project")
|
||||
= _("However, you are already a member of this %{member_source}. Sign in using a different account to accept the invitation.") % { member_source: member_source }
|
||||
|
||||
- if @member.invite_email != current_user.email
|
||||
- if !current_user_matches_invite?
|
||||
%p
|
||||
- mail_to_invite_email = mail_to(@member.invite_email)
|
||||
- mail_to_current_user = mail_to(current_user.email)
|
||||
- link_to_current_user = link_to(current_user.to_reference, user_url(current_user))
|
||||
= _("Note that this invitation was sent to %{mail_to_invite_email}, but you are signed in as %{link_to_current_user} with email %{mail_to_current_user}.").html_safe % { mail_to_invite_email: mail_to_invite_email, mail_to_current_user: mail_to_current_user, link_to_current_user: link_to_current_user }
|
||||
|
||||
- unless is_member
|
||||
- unless member?
|
||||
.actions
|
||||
= link_to _("Accept invitation"), accept_invite_url(@token), method: :post, class: "btn btn-success"
|
||||
= link_to _("Decline"), decline_invite_url(@token), method: :post, class: "btn btn-danger prepend-left-10"
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@
|
|||
= render_if_exists 'projects/sidebar/repository_locked_files'
|
||||
|
||||
- if project_nav_tab? :issues
|
||||
= nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do
|
||||
= nav_link(controller: @project.issues_enabled? ? ['projects/issues', :labels, :milestones, :boards] : 'projects/issues') do
|
||||
= link_to project_issues_path(@project), class: 'shortcuts-issues qa-issues-item' do
|
||||
.nav-icon-container
|
||||
= sprite_icon('issues')
|
||||
|
|
@ -91,7 +91,7 @@
|
|||
= number_with_delimiter(@project.open_issues_count(current_user))
|
||||
|
||||
%ul.sidebar-sub-level-items
|
||||
= nav_link(controller: :issues, action: :index, html_options: { class: "fly-out-top-item" } ) do
|
||||
= nav_link(controller: 'projects/issues', action: :index, html_options: { class: "fly-out-top-item" } ) do
|
||||
= link_to project_issues_path(@project) do
|
||||
%strong.fly-out-top-item-name
|
||||
= _('Issues')
|
||||
|
|
@ -120,19 +120,23 @@
|
|||
= link_to project_milestones_path(@project), title: _('Milestones'), class: 'qa-milestones-link' do
|
||||
%span
|
||||
= _('Milestones')
|
||||
- if project_nav_tab? :external_issue_tracker
|
||||
= nav_link do
|
||||
- issue_tracker = @project.external_issue_tracker
|
||||
= link_to issue_tracker.issue_tracker_path, class: 'shortcuts-external_tracker' do
|
||||
.nav-icon-container
|
||||
= sprite_icon('external-link')
|
||||
%span.nav-item-name
|
||||
= issue_tracker.title
|
||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||
= nav_link(html_options: { class: "fly-out-top-item" } ) do
|
||||
= link_to issue_tracker.issue_tracker_path do
|
||||
%strong.fly-out-top-item-name
|
||||
= issue_tracker.title
|
||||
|
||||
- if project_nav_tab?(:external_issue_tracker)
|
||||
- issue_tracker = @project.external_issue_tracker
|
||||
- if issue_tracker.is_a?(JiraService) && project_jira_issues_integration?
|
||||
= render_if_exists 'layouts/nav/sidebar/project_jira_issues_link', issue_tracker: issue_tracker
|
||||
- else
|
||||
= nav_link do
|
||||
= link_to issue_tracker.issue_tracker_path, target: '_blank', rel: 'noopener noreferrer', class: 'shortcuts-external_tracker' do
|
||||
.nav-icon-container
|
||||
= sprite_icon('external-link')
|
||||
%span.nav-item-name
|
||||
= issue_tracker.title
|
||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||
= nav_link(html_options: { class: "fly-out-top-item" } ) do
|
||||
= link_to issue_tracker.issue_tracker_path, target: '_blank', rel: 'noopener noreferrer' do
|
||||
%strong.fly-out-top-item-name
|
||||
= issue_tracker.title
|
||||
|
||||
- if (project_nav_tab? :labels) && !@project.issues_enabled?
|
||||
= nav_link(controller: [:labels]) do
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
- type = local_assigns.fetch(:type)
|
||||
- bulk_issue_health_status_flag = Feature.enabled?(:bulk_update_health_status, @project&.group) && type == :issues && @project&.group&.feature_available?(:issuable_health_status)
|
||||
- epic_bulk_edit_flag = @project&.group&.feature_available?(:epics) && type == :issues
|
||||
|
||||
%aside.issues-bulk-update.js-right-sidebar.right-sidebar{ "aria-live" => "polite", data: { 'signed-in': current_user.present? } }
|
||||
.issuable-sidebar.hidden
|
||||
|
|
@ -27,6 +28,13 @@
|
|||
- field_name = "update[assignee_ids][]"
|
||||
= dropdown_tag(_("Select assignee"), options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: _("Assign to"), filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
|
||||
placeholder: _("Search authors"), data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: field_name } })
|
||||
- if epic_bulk_edit_flag
|
||||
.block
|
||||
.title
|
||||
= _('Epic')
|
||||
.filter-item.epic-bulk-edit
|
||||
#js-epic-select-root{ data: { group_id: @project&.group&.id, show_header: "true" } }
|
||||
%input{ id: 'issue_epic_id', type: 'hidden', name: 'update[epic_id]' }
|
||||
.block
|
||||
.title
|
||||
= _('Milestone')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add refresh rate options to dashboard header
|
||||
merge_request: 35238
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove the second prompt to accept or decline an invitation
|
||||
merge_request: 35777
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add todo pill styling for resolved alert
|
||||
merge_request: 35579
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add parallel persistence for author_name on AuditEvent
|
||||
merge_request: 35130
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix path conflict for Ghost on UpdateRoutesForLostAndFoundGroupAndOrphanedProjects
|
||||
merge_request: 35425
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Todo Mutations should return the mutated todos
|
||||
merge_request: 35998
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Hide cleanup button for clusters with management project
|
||||
merge_request: 35576
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update `rack-timeout` to `0.5.2`
|
||||
merge_request: 36071
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddAuthorNameToAuditEvent < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
unless column_exists?(:audit_events, :author_name)
|
||||
with_lock_retries do
|
||||
add_column :audit_events, :author_name, :text
|
||||
end
|
||||
end
|
||||
|
||||
add_text_limit :audit_events, :author_name, 255
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :audit_events, :author_name
|
||||
end
|
||||
end
|
||||
|
|
@ -136,6 +136,7 @@ class UpdateRoutesForLostAndFoundGroupAndOrphanedProjects < ActiveRecord::Migrat
|
|||
# to ensure the Active Record's knowledge of the table structure is current
|
||||
Namespace.reset_column_information
|
||||
Route.reset_column_information
|
||||
User.reset_column_information
|
||||
|
||||
# Find the ghost user, its namespace and the "lost and found" group
|
||||
ghost_user = User.ghost
|
||||
|
|
@ -158,6 +159,15 @@ class UpdateRoutesForLostAndFoundGroupAndOrphanedProjects < ActiveRecord::Migrat
|
|||
'More info: gitlab.com/gitlab-org/gitlab/-/issues/198603'
|
||||
lost_and_found_group.save!
|
||||
|
||||
# make sure that the ghost namespace has a unique path
|
||||
ghost_namespace.generate_unique_path
|
||||
|
||||
if ghost_namespace.path_changed?
|
||||
ghost_namespace.save!
|
||||
# If the path changed, also update the Ghost User's username to match the new path.
|
||||
ghost_user.update!(username: ghost_namespace.path)
|
||||
end
|
||||
|
||||
# Update the routes for the Ghost user, the "lost and found" group
|
||||
# and all the orphaned projects
|
||||
ghost_namespace.ensure_route!
|
||||
|
|
|
|||
|
|
@ -9366,7 +9366,9 @@ CREATE TABLE public.audit_events (
|
|||
details text,
|
||||
created_at timestamp without time zone,
|
||||
updated_at timestamp without time zone,
|
||||
ip_address inet
|
||||
ip_address inet,
|
||||
author_name text,
|
||||
CONSTRAINT check_83ff8406e2 CHECK ((char_length(author_name) <= 255))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE public.audit_events_id_seq
|
||||
|
|
@ -23561,6 +23563,7 @@ COPY "schema_migrations" (version) FROM STDIN;
|
|||
20200622235737
|
||||
20200623000148
|
||||
20200623000320
|
||||
20200623090030
|
||||
20200623121135
|
||||
20200623141544
|
||||
20200623170000
|
||||
|
|
|
|||
|
|
@ -251,6 +251,19 @@ When Puma is used instead of Unicorn, the following metrics are available:
|
|||
| `puma_idle_threads` | Gauge | 12.0 | Number of spawned threads which are not processing a request |
|
||||
| `puma_killer_terminations_total` | Gauge | 12.0 | Number of workers terminated by PumaWorkerKiller |
|
||||
|
||||
## Redis metrics
|
||||
|
||||
These client metrics are meant to complement Redis server metrics.
|
||||
These metrics are broken down per [Redis
|
||||
instance](https://docs.gitlab.com/omnibus/settings/redis.html#running-with-multiple-redis-instances).
|
||||
These metrics all have a `storage` label which indicates the Redis
|
||||
instance (`cache`, `shared_state` etc.).
|
||||
|
||||
| Metric | Type | Since | Description |
|
||||
|:--------------------------------- |:------- |:----- |:----------- |
|
||||
| `redis_client_exceptions_total` | Counter | 13.2 | Number of Redis client exceptions, broken down by exception class |
|
||||
| `redis_client_requests_total` | Counter | 13.2 | Number of Redis client requests |
|
||||
|
||||
## Metrics shared directory
|
||||
|
||||
GitLab's Prometheus client requires a directory to store metrics data shared between multi-process services.
|
||||
|
|
|
|||
|
|
@ -813,6 +813,11 @@ type Blob implements Entry {
|
|||
"""
|
||||
type: EntryType!
|
||||
|
||||
"""
|
||||
Web path of the blob
|
||||
"""
|
||||
webPath: String
|
||||
|
||||
"""
|
||||
Web URL of the blob
|
||||
"""
|
||||
|
|
@ -1216,6 +1221,11 @@ type Commit {
|
|||
"""
|
||||
titleHtml: String
|
||||
|
||||
"""
|
||||
Web path of the commit
|
||||
"""
|
||||
webPath: String!
|
||||
|
||||
"""
|
||||
Web URL of the commit
|
||||
"""
|
||||
|
|
@ -13119,9 +13129,14 @@ type TodoRestoreManyPayload {
|
|||
errors: [String!]!
|
||||
|
||||
"""
|
||||
The ids of the updated todo items
|
||||
Updated todos
|
||||
"""
|
||||
updatedIds: [ID!]!
|
||||
todos: [Todo!]!
|
||||
|
||||
"""
|
||||
The ids of the updated todo items. Deprecated in 13.2: Use todos
|
||||
"""
|
||||
updatedIds: [ID!]! @deprecated(reason: "Use todos. Deprecated in 13.2")
|
||||
}
|
||||
|
||||
"""
|
||||
|
|
@ -13201,9 +13216,14 @@ type TodosMarkAllDonePayload {
|
|||
errors: [String!]!
|
||||
|
||||
"""
|
||||
Ids of the updated todos
|
||||
Updated todos
|
||||
"""
|
||||
updatedIds: [ID!]!
|
||||
todos: [Todo!]!
|
||||
|
||||
"""
|
||||
Ids of the updated todos. Deprecated in 13.2: Use todos
|
||||
"""
|
||||
updatedIds: [ID!]! @deprecated(reason: "Use todos. Deprecated in 13.2")
|
||||
}
|
||||
|
||||
"""
|
||||
|
|
@ -13367,6 +13387,11 @@ type TreeEntry implements Entry {
|
|||
"""
|
||||
type: EntryType!
|
||||
|
||||
"""
|
||||
Web path for the tree entry (directory)
|
||||
"""
|
||||
webPath: String
|
||||
|
||||
"""
|
||||
Web URL for the tree entry (directory)
|
||||
"""
|
||||
|
|
@ -14268,6 +14293,11 @@ type User {
|
|||
"""
|
||||
username: String!
|
||||
|
||||
"""
|
||||
Web path of the user
|
||||
"""
|
||||
webPath: String!
|
||||
|
||||
"""
|
||||
Web URL of the user
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -2128,6 +2128,20 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "webPath",
|
||||
"description": "Web path of the blob",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "webUrl",
|
||||
"description": "Web URL of the blob",
|
||||
|
|
@ -3290,6 +3304,24 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "webPath",
|
||||
"description": "Web path of the commit",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "webUrl",
|
||||
"description": "Web URL of the commit",
|
||||
|
|
@ -38752,9 +38784,35 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "todos",
|
||||
"description": "Updated todos",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Todo",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "updatedIds",
|
||||
"description": "The ids of the updated todo items",
|
||||
"description": "The ids of the updated todo items. Deprecated in 13.2: Use todos",
|
||||
"args": [
|
||||
|
||||
],
|
||||
|
|
@ -38775,8 +38833,8 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
"isDeprecated": true,
|
||||
"deprecationReason": "Use todos. Deprecated in 13.2"
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
|
|
@ -38987,9 +39045,35 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "todos",
|
||||
"description": "Updated todos",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Todo",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "updatedIds",
|
||||
"description": "Ids of the updated todos",
|
||||
"description": "Ids of the updated todos. Deprecated in 13.2: Use todos",
|
||||
"args": [
|
||||
|
||||
],
|
||||
|
|
@ -39010,8 +39094,8 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
"isDeprecated": true,
|
||||
"deprecationReason": "Use todos. Deprecated in 13.2"
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
|
|
@ -39466,6 +39550,20 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "webPath",
|
||||
"description": "Web path for the tree entry (directory)",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "webUrl",
|
||||
"description": "Web URL for the tree entry (directory)",
|
||||
|
|
@ -41883,6 +41981,24 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "webPath",
|
||||
"description": "Web path of the user",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "webUrl",
|
||||
"description": "Web URL of the user",
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ Autogenerated return type of AwardEmojiToggle
|
|||
| `path` | String! | Path of the entry |
|
||||
| `sha` | String! | Last commit sha for the entry |
|
||||
| `type` | EntryType! | Type of tree entry |
|
||||
| `webPath` | String | Web path of the blob |
|
||||
| `webUrl` | String | Web URL of the blob |
|
||||
|
||||
## Board
|
||||
|
|
@ -227,6 +228,7 @@ Autogenerated return type of BoardListUpdateLimitMetrics
|
|||
| `signatureHtml` | String | Rendered HTML of the commit signature |
|
||||
| `title` | String | Title of the commit message |
|
||||
| `titleHtml` | String | The GitLab Flavored Markdown rendering of `title` |
|
||||
| `webPath` | String! | Web path of the commit |
|
||||
| `webUrl` | String! | Web URL of the commit |
|
||||
|
||||
## CommitCreatePayload
|
||||
|
|
@ -1966,7 +1968,8 @@ Autogenerated return type of TodoRestoreMany
|
|||
| --- | ---- | ---------- |
|
||||
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
|
||||
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
|
||||
| `updatedIds` | ID! => Array | The ids of the updated todo items |
|
||||
| `todos` | Todo! => Array | Updated todos |
|
||||
| `updatedIds` **{warning-solid}** | ID! => Array | **Deprecated:** Use todos. Deprecated in 13.2 |
|
||||
|
||||
## TodoRestorePayload
|
||||
|
||||
|
|
@ -1986,7 +1989,8 @@ Autogenerated return type of TodosMarkAllDone
|
|||
| --- | ---- | ---------- |
|
||||
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
|
||||
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
|
||||
| `updatedIds` | ID! => Array | Ids of the updated todos |
|
||||
| `todos` | Todo! => Array | Updated todos |
|
||||
| `updatedIds` **{warning-solid}** | ID! => Array | **Deprecated:** Use todos. Deprecated in 13.2 |
|
||||
|
||||
## ToggleAwardEmojiPayload
|
||||
|
||||
|
|
@ -2017,6 +2021,7 @@ Represents a directory
|
|||
| `path` | String! | Path of the entry |
|
||||
| `sha` | String! | Last commit sha for the entry |
|
||||
| `type` | EntryType! | Type of tree entry |
|
||||
| `webPath` | String | Web path for the tree entry (directory) |
|
||||
| `webUrl` | String | Web URL for the tree entry (directory) |
|
||||
|
||||
## UpdateAlertStatusPayload
|
||||
|
|
@ -2120,6 +2125,7 @@ Autogenerated return type of UpdateSnippet
|
|||
| `state` | UserState! | State of the user |
|
||||
| `userPermissions` | UserPermissions! | Permissions for the current user on the resource |
|
||||
| `username` | String! | Username of the user. Unique within this instance of GitLab |
|
||||
| `webPath` | String! | Web path of the user |
|
||||
| `webUrl` | String! | Web URL of the user |
|
||||
|
||||
## UserPermissions
|
||||
|
|
|
|||
|
|
@ -640,7 +640,6 @@ There are two options for safely adding new arguments to Sidekiq workers:
|
|||
|
||||
1. Set up a [multi-step deployment](#multi-step-deployment) in which the new argument is first added to the worker
|
||||
1. Use a [parameter hash](#parameter-hash) for additional arguments. This is perhaps the most flexible option.
|
||||
1. Use a parameter hash for additional arguments. This is perhaps the most flexible option.
|
||||
|
||||
##### Multi-step deployment
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
|
|
@ -134,12 +134,24 @@ The changes of a wiki page over time are recorded in the wiki's Git repository,
|
|||
and you can view them by clicking the **Page history** button.
|
||||
|
||||
From the history page you can see the revision of the page (Git commit SHA), its
|
||||
author, the commit message, when it was last updated, and the page markup format.
|
||||
author, the commit message, and when it was last updated.
|
||||
To see how a previous version of the page looked like, click on a revision
|
||||
number.
|
||||
number in the **Page version** column.
|
||||
|
||||

|
||||
|
||||
### Viewing the changes between page versions
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15242) in GitLab 13.2.
|
||||
|
||||
Similar to versioned diff file views, you can see the changes made in a given Wiki page version:
|
||||
|
||||
1. Navigate to the Wiki page you're interested in.
|
||||
1. Click on **Page history** to see all page versions.
|
||||
1. Click on the commit message in the **Changes** column for the version you're interested in:
|
||||
|
||||

|
||||
|
||||
## Wiki activity records
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14902) in GitLab 12.10.
|
||||
|
|
|
|||
|
|
@ -81,6 +81,19 @@ module Gitlab
|
|||
self
|
||||
end
|
||||
|
||||
def count_request
|
||||
@request_counter ||= Gitlab::Metrics.counter(:redis_client_requests_total, 'Client side Redis request count, per Redis server')
|
||||
@request_counter.increment({ storage: storage_key })
|
||||
end
|
||||
|
||||
def count_exception(ex)
|
||||
# This metric is meant to give a client side view of how the Redis
|
||||
# server is doing. Redis itself does not expose error counts. This
|
||||
# metric can be used for Redis alerting and service health monitoring.
|
||||
@exception_counter ||= Gitlab::Metrics.counter(:redis_client_exceptions_total, 'Client side Redis exception count, per Redis server, per exception class')
|
||||
@exception_counter.increment({ storage: storage_key, exception: ex.class.to_s })
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def request_count_key
|
||||
|
|
|
|||
|
|
@ -6,11 +6,14 @@ module Gitlab
|
|||
module Instrumentation
|
||||
module RedisInterceptor
|
||||
def call(*args, &block)
|
||||
instrumentation_class.count_request
|
||||
instrumentation_class.redis_cluster_validate!(args.first)
|
||||
start = Time.now
|
||||
|
||||
instrumentation_class.redis_cluster_validate!(args.first)
|
||||
|
||||
super(*args, &block)
|
||||
rescue ::Redis::BaseError => ex
|
||||
instrumentation_class.count_exception(ex)
|
||||
raise ex
|
||||
ensure
|
||||
duration = (Time.now - start)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Kubernetes
|
||||
class Node
|
||||
def initialize(cluster)
|
||||
@cluster = cluster
|
||||
end
|
||||
|
||||
def all
|
||||
nodes.map do |node|
|
||||
attributes = node(node)
|
||||
attributes.merge(node_metrics(node))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :cluster
|
||||
|
||||
def nodes_from_cluster
|
||||
graceful_request { cluster.kubeclient.get_nodes }
|
||||
end
|
||||
|
||||
def nodes_metrics_from_cluster
|
||||
graceful_request { cluster.kubeclient.metrics_client.get_nodes }
|
||||
end
|
||||
|
||||
def nodes
|
||||
@nodes ||= nodes_from_cluster[:response].to_a
|
||||
end
|
||||
|
||||
def nodes_metrics
|
||||
@nodes_metrics ||= nodes_metrics_from_cluster[:response].to_a
|
||||
end
|
||||
|
||||
def node_metrics_from_node(node)
|
||||
nodes_metrics.find do |node_metric|
|
||||
node_metric.metadata.name == node.metadata.name
|
||||
end
|
||||
end
|
||||
|
||||
def graceful_request(&block)
|
||||
::Gitlab::Kubernetes::KubeClient.graceful_request(cluster.id, &block)
|
||||
end
|
||||
|
||||
def node(node)
|
||||
{
|
||||
'metadata' => {
|
||||
'name' => node.metadata.name
|
||||
},
|
||||
'status' => {
|
||||
'capacity' => {
|
||||
'cpu' => node.status.capacity.cpu,
|
||||
'memory' => node.status.capacity.memory
|
||||
},
|
||||
'allocatable' => {
|
||||
'cpu' => node.status.allocatable.cpu,
|
||||
'memory' => node.status.allocatable.memory
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def node_metrics(node)
|
||||
node_metrics = node_metrics_from_node(node)
|
||||
return {} unless node_metrics
|
||||
|
||||
{
|
||||
'usage' => {
|
||||
'cpu' => node_metrics.usage.cpu,
|
||||
'memory' => node_metrics.usage.memory
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -167,6 +167,11 @@ msgid_plural "%d groups selected"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d hour"
|
||||
msgid_plural "%d hours"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d inaccessible merge request"
|
||||
msgid_plural "%d inaccessible merge requests"
|
||||
msgstr[0] ""
|
||||
|
|
@ -1244,7 +1249,7 @@ msgstr ""
|
|||
msgid "AccessTokens|reset it"
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessibilityReport|Learn More"
|
||||
msgid "AccessibilityReport|Learn more"
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessibilityReport|Message: %{message}"
|
||||
|
|
@ -8986,6 +8991,9 @@ msgstr ""
|
|||
msgid "Epic events"
|
||||
msgstr ""
|
||||
|
||||
msgid "Epic not found for given params"
|
||||
msgstr ""
|
||||
|
||||
msgid "Epics"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -12874,9 +12882,15 @@ msgstr ""
|
|||
msgid "JiraService|If different from Web URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "JiraService|Issue List"
|
||||
msgstr ""
|
||||
|
||||
msgid "JiraService|Jira API URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "JiraService|Jira Issues"
|
||||
msgstr ""
|
||||
|
||||
msgid "JiraService|Jira comments will be created when an issue gets referenced in a commit."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -12886,6 +12900,9 @@ msgstr ""
|
|||
msgid "JiraService|Jira issue tracker"
|
||||
msgstr ""
|
||||
|
||||
msgid "JiraService|Open Jira"
|
||||
msgstr ""
|
||||
|
||||
msgid "JiraService|Password or API token"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -13281,9 +13298,6 @@ msgstr ""
|
|||
msgid "Learn GitLab"
|
||||
msgstr ""
|
||||
|
||||
msgid "Learn More"
|
||||
msgstr ""
|
||||
|
||||
msgid "Learn how to %{link_start}contribute to the built-in templates%{link_end}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -14505,6 +14519,9 @@ msgstr ""
|
|||
msgid "Metrics|Select a value"
|
||||
msgstr ""
|
||||
|
||||
msgid "Metrics|Set refresh rate"
|
||||
msgstr ""
|
||||
|
||||
msgid "Metrics|Star dashboard"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15715,6 +15732,9 @@ msgstr ""
|
|||
msgid "OfSearchInADropdown|Filter"
|
||||
msgstr ""
|
||||
|
||||
msgid "Off"
|
||||
msgstr ""
|
||||
|
||||
msgid "Oh no!"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -20294,7 +20314,7 @@ msgstr ""
|
|||
msgid "SecurityReports|Issue Created"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|Learn More"
|
||||
msgid "SecurityReports|Learn more"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|Learn more about setting up your dashboard"
|
||||
|
|
@ -20519,6 +20539,9 @@ msgstr ""
|
|||
msgid "Select due date"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select epic"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select file"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -4,21 +4,44 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe InvitesController do
|
||||
let(:token) { '123456' }
|
||||
let(:user) { create(:user) }
|
||||
let(:member) { create(:project_member, invite_token: token, invite_email: 'test@abc.com', user: user) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let(:member) { create(:project_member, :invited, invite_token: token, invite_email: user.email) }
|
||||
let(:project_members) { member.source.users }
|
||||
|
||||
before do
|
||||
controller.instance_variable_set(:@member, member)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
describe 'GET #accept' do
|
||||
it 'accepts user' do
|
||||
get :accept, params: { id: token }
|
||||
member.reload
|
||||
describe 'GET #show' do
|
||||
it 'accepts user if invite email matches signed in user' do
|
||||
expect do
|
||||
get :show, params: { id: token }
|
||||
end.to change { project_members.include?(user) }.from(false).to(true)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:found)
|
||||
expect(flash[:notice]).to include 'You have been granted'
|
||||
end
|
||||
|
||||
it 'forces re-confirmation if email does not match signed in user' do
|
||||
member.invite_email = 'bogus@email.com'
|
||||
|
||||
expect do
|
||||
get :show, params: { id: token }
|
||||
end.not_to change { project_members.include?(user) }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(flash[:notice]).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #accept' do
|
||||
it 'accepts user' do
|
||||
expect do
|
||||
post :accept, params: { id: token }
|
||||
end.to change { project_members.include?(user) }.from(false).to(true)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:found)
|
||||
expect(member.user).to eq(user)
|
||||
expect(flash[:notice]).to include 'You have been granted'
|
||||
end
|
||||
end
|
||||
|
|
@ -26,8 +49,8 @@ RSpec.describe InvitesController do
|
|||
describe 'GET #decline' do
|
||||
it 'declines user' do
|
||||
get :decline, params: { id: token }
|
||||
expect {member.reload}.to raise_error ActiveRecord::RecordNotFound
|
||||
|
||||
expect { member.reload }.to raise_error ActiveRecord::RecordNotFound
|
||||
expect(response).to have_gitlab_http_status(:found)
|
||||
expect(flash[:notice]).to include 'You have declined the invitation to join'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -68,6 +68,35 @@ RSpec.describe 'Group' do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'real-time group url validation', :js do
|
||||
it 'shows a message if group url is available' do
|
||||
fill_in 'group_path', with: 'az'
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content('Group path is available')
|
||||
end
|
||||
|
||||
it 'shows an error if group url is taken' do
|
||||
fill_in 'group_path', with: user.username
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content('Group path is already taken')
|
||||
end
|
||||
|
||||
it 'does not break after an invalid form submit' do
|
||||
fill_in 'group_name', with: 'MyGroup'
|
||||
fill_in 'group_path', with: 'z'
|
||||
click_button 'Create group'
|
||||
|
||||
expect(page).to have_content('Group URL is too short')
|
||||
|
||||
fill_in 'group_path', with: 'az'
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content('Group path is available')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Mattermost team creation' do
|
||||
before do
|
||||
stub_mattermost_setting(enabled: mattermost_enabled)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Invites' do
|
||||
let(:user) { create(:user) }
|
||||
RSpec.describe 'Invites', :aggregate_failures do
|
||||
let(:user) { create(:user, email: 'user@example.com') }
|
||||
let(:owner) { create(:user, name: 'John Doe') }
|
||||
let(:group) { create(:group, name: 'Owned') }
|
||||
let(:project) { create(:project, :repository, namespace: group) }
|
||||
|
|
@ -11,7 +11,7 @@ RSpec.describe 'Invites' do
|
|||
|
||||
before do
|
||||
project.add_maintainer(owner)
|
||||
group.add_user(owner, Gitlab::Access::OWNER)
|
||||
group.add_owner(owner)
|
||||
group.add_developer('user@example.com', owner)
|
||||
group_invite.generate_invite_token!
|
||||
end
|
||||
|
|
@ -23,12 +23,12 @@ RSpec.describe 'Invites' do
|
|||
end
|
||||
|
||||
def fill_in_sign_up_form(new_user)
|
||||
fill_in 'new_user_name', with: new_user.name
|
||||
fill_in 'new_user_username', with: new_user.username
|
||||
fill_in 'new_user_email', with: new_user.email
|
||||
fill_in 'new_user_email_confirmation', with: new_user.email
|
||||
fill_in 'new_user_password', with: new_user.password
|
||||
click_button "Register"
|
||||
fill_in 'new_user_name', with: new_user.name
|
||||
fill_in 'new_user_username', with: new_user.username
|
||||
fill_in 'new_user_email', with: new_user.email
|
||||
fill_in 'new_user_email_confirmation', with: new_user.email
|
||||
fill_in 'new_user_password', with: new_user.password
|
||||
click_button 'Register'
|
||||
end
|
||||
|
||||
def fill_in_sign_in_form(user)
|
||||
|
|
@ -48,19 +48,15 @@ RSpec.describe 'Invites' do
|
|||
expect(page).to have_content('To accept this invitation, sign in')
|
||||
end
|
||||
|
||||
it 'sign in and redirects to invitation page' do
|
||||
it 'sign in, grants access and redirects to group page' do
|
||||
fill_in_sign_in_form(user)
|
||||
|
||||
expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
|
||||
expect(page).to have_content(
|
||||
'You have been invited by John Doe to join group Owned as Developer.'
|
||||
)
|
||||
expect(page).to have_link('Accept invitation')
|
||||
expect(page).to have_link('Decline')
|
||||
expect(current_path).to eq(group_path(group))
|
||||
expect(page).to have_content('You have been granted Developer access to group Owned.')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when signed in as an exists member' do
|
||||
context 'when signed in as an existing member' do
|
||||
before do
|
||||
sign_in(owner)
|
||||
end
|
||||
|
|
@ -71,166 +67,166 @@ RSpec.describe 'Invites' do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'accepting the invitation' do
|
||||
before do
|
||||
sign_in(user)
|
||||
visit invite_path(group_invite.raw_invite_token)
|
||||
end
|
||||
|
||||
it 'grants access and redirects to group page' do
|
||||
page.click_link 'Accept invitation'
|
||||
expect(current_path).to eq(group_path(group))
|
||||
expect(page).to have_content(
|
||||
'You have been granted Developer access to group Owned.'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'declining the application' do
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in(user)
|
||||
visit invite_path(group_invite.raw_invite_token)
|
||||
end
|
||||
|
||||
it 'declines application and redirects to dashboard' do
|
||||
page.click_link 'Decline'
|
||||
expect(current_path).to eq(dashboard_projects_path)
|
||||
expect(page).to have_content(
|
||||
'You have declined the invitation to join group Owned.'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when signed out' do
|
||||
before do
|
||||
visit decline_invite_path(group_invite.raw_invite_token)
|
||||
end
|
||||
|
||||
it 'declines application and redirects to sign in page' do
|
||||
expect(current_path).to eq(new_user_session_path)
|
||||
expect(page).to have_content(
|
||||
'You have declined the invitation to join group Owned.'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'invite an user using their email address' do
|
||||
context 'when inviting a user using their email address' do
|
||||
let(:new_user) { build_stubbed(:user) }
|
||||
let(:invite_email) { new_user.email }
|
||||
let(:group_invite) { create(:group_member, :invited, group: group, invite_email: invite_email) }
|
||||
let!(:project_invite) { create(:project_member, :invited, project: project, invite_email: invite_email) }
|
||||
|
||||
before do
|
||||
stub_application_setting(send_user_confirmation_email: send_email_confirmation)
|
||||
visit invite_path(group_invite.raw_invite_token)
|
||||
end
|
||||
|
||||
context 'email confirmation disabled' do
|
||||
let(:send_email_confirmation) { false }
|
||||
|
||||
it 'signs up and redirects to the dashboard page with all the projects/groups invitations automatically accepted' do
|
||||
fill_in_sign_up_form(new_user)
|
||||
|
||||
expect(current_path).to eq(dashboard_projects_path)
|
||||
expect(page).to have_content(project.full_name)
|
||||
|
||||
visit group_path(group)
|
||||
|
||||
expect(page).to have_content(group.full_name)
|
||||
context 'when user has not signed in yet' do
|
||||
before do
|
||||
stub_application_setting(send_user_confirmation_email: send_email_confirmation)
|
||||
visit invite_path(group_invite.raw_invite_token)
|
||||
end
|
||||
|
||||
context 'the user sign-up using a different email address' do
|
||||
let(:invite_email) { build_stubbed(:user).email }
|
||||
context 'email confirmation disabled' do
|
||||
let(:send_email_confirmation) { false }
|
||||
|
||||
it 'signs up and redirects to the invitation page' do
|
||||
it 'signs up and redirects to the dashboard page with all the projects/groups invitations automatically accepted' do
|
||||
fill_in_sign_up_form(new_user)
|
||||
|
||||
expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'email confirmation enabled' do
|
||||
let(:send_email_confirmation) { true }
|
||||
|
||||
context 'when soft email confirmation is not enabled' do
|
||||
before do
|
||||
allow(User).to receive(:allow_unconfirmed_access_for).and_return 0
|
||||
end
|
||||
|
||||
it 'signs up and redirects to root page with all the project/groups invitation automatically accepted' do
|
||||
fill_in_sign_up_form(new_user)
|
||||
confirm_email(new_user)
|
||||
fill_in_sign_in_form(new_user)
|
||||
|
||||
expect(current_path).to eq(root_path)
|
||||
expect(current_path).to eq(dashboard_projects_path)
|
||||
expect(page).to have_content(project.full_name)
|
||||
|
||||
visit group_path(group)
|
||||
|
||||
expect(page).to have_content(group.full_name)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when soft email confirmation is enabled' do
|
||||
before do
|
||||
allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days
|
||||
end
|
||||
context 'the user sign-up using a different email address' do
|
||||
let(:invite_email) { build_stubbed(:user).email }
|
||||
|
||||
it 'signs up and redirects to root page with all the project/groups invitation automatically accepted' do
|
||||
fill_in_sign_up_form(new_user)
|
||||
confirm_email(new_user)
|
||||
it 'signs up and redirects to the invitation page' do
|
||||
fill_in_sign_up_form(new_user)
|
||||
|
||||
expect(current_path).to eq(root_path)
|
||||
expect(page).to have_content(project.full_name)
|
||||
|
||||
visit group_path(group)
|
||||
|
||||
expect(page).to have_content(group.full_name)
|
||||
expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "doesn't accept invitations until the user confirms their email" do
|
||||
fill_in_sign_up_form(new_user)
|
||||
sign_in(owner)
|
||||
|
||||
visit project_project_members_path(project)
|
||||
expect(page).to have_content 'Invited'
|
||||
end
|
||||
|
||||
context 'the user sign-up using a different email address' do
|
||||
let(:invite_email) { build_stubbed(:user).email }
|
||||
context 'email confirmation enabled' do
|
||||
let(:send_email_confirmation) { true }
|
||||
|
||||
context 'when soft email confirmation is not enabled' do
|
||||
before do
|
||||
stub_feature_flags(soft_email_confirmation: false)
|
||||
allow(User).to receive(:allow_unconfirmed_access_for).and_return 0
|
||||
end
|
||||
|
||||
it 'signs up and redirects to the invitation page' do
|
||||
it 'signs up and redirects to root page with all the project/groups invitation automatically accepted' do
|
||||
fill_in_sign_up_form(new_user)
|
||||
confirm_email(new_user)
|
||||
fill_in_sign_in_form(new_user)
|
||||
|
||||
expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
|
||||
expect(current_path).to eq(root_path)
|
||||
expect(page).to have_content(project.full_name)
|
||||
|
||||
visit group_path(group)
|
||||
|
||||
expect(page).to have_content(group.full_name)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when soft email confirmation is enabled' do
|
||||
before do
|
||||
stub_feature_flags(soft_email_confirmation: true)
|
||||
allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days
|
||||
end
|
||||
|
||||
it 'signs up and redirects to the invitation page' do
|
||||
it 'signs up and redirects to root page with all the project/groups invitation automatically accepted' do
|
||||
fill_in_sign_up_form(new_user)
|
||||
confirm_email(new_user)
|
||||
|
||||
expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
|
||||
expect(current_path).to eq(root_path)
|
||||
expect(page).to have_content(project.full_name)
|
||||
|
||||
visit group_path(group)
|
||||
|
||||
expect(page).to have_content(group.full_name)
|
||||
end
|
||||
end
|
||||
|
||||
it "doesn't accept invitations until the user confirms their email" do
|
||||
fill_in_sign_up_form(new_user)
|
||||
sign_in(owner)
|
||||
|
||||
visit project_project_members_path(project)
|
||||
expect(page).to have_content 'Invited'
|
||||
end
|
||||
|
||||
context 'the user sign-up using a different email address' do
|
||||
let(:invite_email) { build_stubbed(:user).email }
|
||||
|
||||
context 'when soft email confirmation is not enabled' do
|
||||
before do
|
||||
stub_feature_flags(soft_email_confirmation: false)
|
||||
allow(User).to receive(:allow_unconfirmed_access_for).and_return 0
|
||||
end
|
||||
|
||||
it 'signs up and redirects to the invitation page' do
|
||||
fill_in_sign_up_form(new_user)
|
||||
confirm_email(new_user)
|
||||
fill_in_sign_in_form(new_user)
|
||||
|
||||
expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when soft email confirmation is enabled' do
|
||||
before do
|
||||
stub_feature_flags(soft_email_confirmation: true)
|
||||
allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days
|
||||
end
|
||||
|
||||
it 'signs up and redirects to the invitation page' do
|
||||
fill_in_sign_up_form(new_user)
|
||||
|
||||
expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when declining the invitation' do
|
||||
let(:send_email_confirmation) { true }
|
||||
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in(user)
|
||||
visit invite_path(group_invite.raw_invite_token)
|
||||
end
|
||||
|
||||
it 'declines application and redirects to dashboard' do
|
||||
page.click_link 'Decline'
|
||||
expect(current_path).to eq(dashboard_projects_path)
|
||||
expect(page).to have_content('You have declined the invitation to join group Owned.')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when signed out' do
|
||||
before do
|
||||
visit decline_invite_path(group_invite.raw_invite_token)
|
||||
end
|
||||
|
||||
it 'declines application and redirects to sign in page' do
|
||||
expect(current_path).to eq(new_user_session_path)
|
||||
expect(page).to have_content('You have declined the invitation to join group Owned.')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when accepting the invitation' do
|
||||
let(:send_email_confirmation) { true }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
visit invite_path(group_invite.raw_invite_token)
|
||||
end
|
||||
|
||||
it 'grants access and redirects to group page' do
|
||||
page.click_link 'Accept invitation'
|
||||
expect(current_path).to eq(group_path(group))
|
||||
expect(page).to have_content('You have been granted Owner access to group Owned.')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,18 +4,7 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe 'User activates Jira', :js do
|
||||
include_context 'project service activation'
|
||||
|
||||
let(:url) { 'http://jira.example.com' }
|
||||
let(:test_url) { 'http://jira.example.com/rest/api/2/serverInfo' }
|
||||
|
||||
def fill_form(disable: false)
|
||||
click_active_toggle if disable
|
||||
|
||||
fill_in 'service_url', with: url
|
||||
fill_in 'service_username', with: 'username'
|
||||
fill_in 'service_password', with: 'password'
|
||||
fill_in 'service_jira_issue_transition_id', with: '25'
|
||||
end
|
||||
include_context 'project service Jira context'
|
||||
|
||||
describe 'user sets and activates Jira Service' do
|
||||
context 'when Jira connection test succeeds' do
|
||||
|
|
@ -33,9 +22,14 @@ RSpec.describe 'User activates Jira', :js do
|
|||
expect(current_path).to eq(edit_project_service_path(project, :jira))
|
||||
end
|
||||
|
||||
it 'shows the Jira link in the menu' do
|
||||
page.within('.nav-sidebar') do
|
||||
expect(page).to have_link('Jira', href: url)
|
||||
unless Gitlab.ee?
|
||||
it 'adds Jira link to sidebar menu' do
|
||||
page.within('.nav-sidebar') do
|
||||
expect(page).not_to have_link('Jira Issues')
|
||||
expect(page).not_to have_link('Issue List', visible: false)
|
||||
expect(page).not_to have_link('Open Jira', href: url, visible: false)
|
||||
expect(page).to have_link('Jira', href: url)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ describe('Remove cluster confirmation modal', () => {
|
|||
|
||||
describe('split button dropdown', () => {
|
||||
const findModal = () => wrapper.find(GlModal).vm;
|
||||
const findSplitButton = () => wrapper.find(SplitButton).vm;
|
||||
const findSplitButton = () => wrapper.find(SplitButton);
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent({ clusterName: 'my-test-cluster' });
|
||||
|
|
@ -36,7 +36,7 @@ describe('Remove cluster confirmation modal', () => {
|
|||
});
|
||||
|
||||
it('opens modal with "cleanup" option', () => {
|
||||
findSplitButton().$emit('remove-cluster-and-cleanup');
|
||||
findSplitButton().vm.$emit('remove-cluster-and-cleanup');
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findModal().show).toHaveBeenCalled();
|
||||
|
|
@ -45,12 +45,23 @@ describe('Remove cluster confirmation modal', () => {
|
|||
});
|
||||
|
||||
it('opens modal without "cleanup" option', () => {
|
||||
findSplitButton().$emit('remove-cluster');
|
||||
findSplitButton().vm.$emit('remove-cluster');
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findModal().show).toHaveBeenCalled();
|
||||
expect(wrapper.vm.confirmCleanup).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with cluster management project', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ hasManagementProject: true });
|
||||
});
|
||||
|
||||
it('renders regular button instead', () => {
|
||||
expect(findSplitButton().exists()).toBe(false);
|
||||
expect(wrapper.find('[data-testid="btnRemove"]').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -84,17 +84,7 @@ exports[`Dashboard template matches the default snapshot 1`] = `
|
|||
<div
|
||||
class="mb-2 pr-2 d-flex d-sm-block"
|
||||
>
|
||||
<gl-deprecated-button-stub
|
||||
class="flex-grow-1"
|
||||
size="md"
|
||||
title="Refresh dashboard"
|
||||
variant="default"
|
||||
>
|
||||
<icon-stub
|
||||
name="retry"
|
||||
size="16"
|
||||
/>
|
||||
</gl-deprecated-button-stub>
|
||||
<refresh-button-stub />
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { metricStates } from '~/monitoring/constants';
|
|||
import Dashboard from '~/monitoring/components/dashboard.vue';
|
||||
|
||||
import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
|
||||
import RefreshButton from '~/monitoring/components/refresh_button.vue';
|
||||
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
|
||||
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
|
||||
import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
|
||||
|
|
@ -592,10 +593,9 @@ describe('Dashboard', () => {
|
|||
setupStoreWithData(store);
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
const refreshBtn = wrapper.find(DashboardHeader).findAll({ ref: 'refreshDashboardBtn' });
|
||||
const refreshBtn = wrapper.find(DashboardHeader).find(RefreshButton);
|
||||
|
||||
expect(refreshBtn).toHaveLength(1);
|
||||
expect(refreshBtn.is(GlDeprecatedButton)).toBe(true);
|
||||
expect(refreshBtn.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,143 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { createStore } from '~/monitoring/stores';
|
||||
import { GlNewDropdown, GlNewDropdownItem, GlButton } from '@gitlab/ui';
|
||||
|
||||
import RefreshButton from '~/monitoring/components/refresh_button.vue';
|
||||
|
||||
describe('RefreshButton', () => {
|
||||
let wrapper;
|
||||
let store;
|
||||
let dispatch;
|
||||
let documentHidden;
|
||||
|
||||
const createWrapper = () => {
|
||||
wrapper = shallowMount(RefreshButton, { store });
|
||||
};
|
||||
|
||||
const findRefreshBtn = () => wrapper.find(GlButton);
|
||||
const findDropdown = () => wrapper.find(GlNewDropdown);
|
||||
const findOptions = () => findDropdown().findAll(GlNewDropdownItem);
|
||||
const findOptionAt = index => findOptions().at(index);
|
||||
|
||||
const expectFetchDataToHaveBeenCalledTimes = times => {
|
||||
const refreshCalls = dispatch.mock.calls.filter(([action, payload]) => {
|
||||
return action === 'monitoringDashboard/fetchDashboardData' && payload === undefined;
|
||||
});
|
||||
expect(refreshCalls).toHaveLength(times);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
jest.spyOn(store, 'dispatch').mockResolvedValue();
|
||||
dispatch = store.dispatch;
|
||||
|
||||
// Document can be mock hidden by overriding the `hidden` property
|
||||
documentHidden = false;
|
||||
Object.defineProperty(document, 'hidden', {
|
||||
configurable: true,
|
||||
get() {
|
||||
return documentHidden;
|
||||
},
|
||||
});
|
||||
|
||||
createWrapper();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
dispatch.mockReset();
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('refreshes data when "refresh" is clicked', () => {
|
||||
findRefreshBtn().vm.$emit('click');
|
||||
expectFetchDataToHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('refresh rate is "Off" in the dropdown', () => {
|
||||
expect(findDropdown().props('text')).toBe('Off');
|
||||
});
|
||||
|
||||
describe('refresh rate options', () => {
|
||||
it('presents multiple options', () => {
|
||||
expect(findOptions().length).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
it('presents an "Off" option as the default option', () => {
|
||||
expect(findOptionAt(0).text()).toBe('Off');
|
||||
expect(findOptionAt(0).props('isChecked')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a refresh rate is chosen', () => {
|
||||
const optIndex = 2; // Other option than "Off"
|
||||
|
||||
beforeEach(() => {
|
||||
findOptionAt(optIndex).vm.$emit('click');
|
||||
return wrapper.vm.$nextTick;
|
||||
});
|
||||
|
||||
it('refresh rate appears in the dropdown', () => {
|
||||
expect(findDropdown().props('text')).toBe('10s');
|
||||
});
|
||||
|
||||
it('refresh rate option is checked', () => {
|
||||
expect(findOptionAt(0).props('isChecked')).toBe(false);
|
||||
expect(findOptionAt(optIndex).props('isChecked')).toBe(true);
|
||||
});
|
||||
|
||||
it('refreshes data when a new refresh rate is chosen', () => {
|
||||
expectFetchDataToHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('refreshes data after two intervals of time have passed', async () => {
|
||||
jest.runOnlyPendingTimers();
|
||||
expectFetchDataToHaveBeenCalledTimes(2);
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
expectFetchDataToHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('does not refresh data if the document is hidden', async () => {
|
||||
documentHidden = true;
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
expectFetchDataToHaveBeenCalledTimes(1);
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
expectFetchDataToHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('data is not refreshed anymore after component is destroyed', () => {
|
||||
expect(jest.getTimerCount()).toBe(1);
|
||||
|
||||
wrapper.destroy();
|
||||
|
||||
expect(jest.getTimerCount()).toBe(0);
|
||||
});
|
||||
|
||||
describe('when "Off" refresh rate is chosen', () => {
|
||||
beforeEach(() => {
|
||||
findOptionAt(0).vm.$emit('click');
|
||||
return wrapper.vm.$nextTick;
|
||||
});
|
||||
|
||||
it('refresh rate is "Off" in the dropdown', () => {
|
||||
expect(findDropdown().props('text')).toBe('Off');
|
||||
});
|
||||
|
||||
it('refresh rate option is appears selected', () => {
|
||||
expect(findOptionAt(0).props('isChecked')).toBe(true);
|
||||
expect(findOptionAt(optIndex).props('isChecked')).toBe(false);
|
||||
});
|
||||
|
||||
it('stops refreshing data', () => {
|
||||
jest.runOnlyPendingTimers();
|
||||
expectFetchDataToHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -10,7 +10,7 @@ exports[`Repository last commit component renders commit widget 1`] = `
|
|||
imgcssclasses=""
|
||||
imgsize="40"
|
||||
imgsrc="https://test.com"
|
||||
linkhref="https://test.com/test"
|
||||
linkhref="/test"
|
||||
tooltipplacement="top"
|
||||
tooltiptext=""
|
||||
username=""
|
||||
|
|
@ -24,7 +24,7 @@ exports[`Repository last commit component renders commit widget 1`] = `
|
|||
>
|
||||
<gl-link-stub
|
||||
class="commit-row-message item-title"
|
||||
href="https://test.com/commit/123"
|
||||
href="/commit/123"
|
||||
>
|
||||
Commit title
|
||||
</gl-link-stub>
|
||||
|
|
@ -36,7 +36,7 @@ exports[`Repository last commit component renders commit widget 1`] = `
|
|||
>
|
||||
<gl-link-stub
|
||||
class="commit-author-link js-user-link"
|
||||
href="https://test.com/test"
|
||||
href="/test"
|
||||
>
|
||||
|
||||
Test
|
||||
|
|
@ -110,7 +110,7 @@ exports[`Repository last commit component renders the signature HTML as returned
|
|||
imgcssclasses=""
|
||||
imgsize="40"
|
||||
imgsrc="https://test.com"
|
||||
linkhref="https://test.com/test"
|
||||
linkhref="/test"
|
||||
tooltipplacement="top"
|
||||
tooltiptext=""
|
||||
username=""
|
||||
|
|
@ -124,7 +124,7 @@ exports[`Repository last commit component renders the signature HTML as returned
|
|||
>
|
||||
<gl-link-stub
|
||||
class="commit-row-message item-title"
|
||||
href="https://test.com/commit/123"
|
||||
href="/commit/123"
|
||||
>
|
||||
Commit title
|
||||
</gl-link-stub>
|
||||
|
|
@ -136,7 +136,7 @@ exports[`Repository last commit component renders the signature HTML as returned
|
|||
>
|
||||
<gl-link-stub
|
||||
class="commit-author-link js-user-link"
|
||||
href="https://test.com/test"
|
||||
href="/test"
|
||||
>
|
||||
|
||||
Test
|
||||
|
|
|
|||
|
|
@ -12,11 +12,13 @@ function createCommitData(data = {}) {
|
|||
titleHtml: 'Commit title',
|
||||
message: 'Commit message',
|
||||
webUrl: 'https://test.com/commit/123',
|
||||
webPath: '/commit/123',
|
||||
authoredDate: '2019-01-01',
|
||||
author: {
|
||||
name: 'Test',
|
||||
avatarUrl: 'https://test.com',
|
||||
webUrl: 'https://test.com/test',
|
||||
webPath: '/test',
|
||||
},
|
||||
pipeline: {
|
||||
detailedStatus: {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ exports[`Repository file preview component renders file HTML 1`] = `
|
|||
/>
|
||||
|
||||
<gl-link-stub
|
||||
href="http://test.com"
|
||||
href="/test.md"
|
||||
>
|
||||
<strong>
|
||||
README.md
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ describe('Repository file preview component', () => {
|
|||
it('renders file HTML', () => {
|
||||
factory({
|
||||
webUrl: 'http://test.com',
|
||||
webPath: '/test.md',
|
||||
name: 'README.md',
|
||||
});
|
||||
|
||||
|
|
@ -44,6 +45,7 @@ describe('Repository file preview component', () => {
|
|||
it('handles hash after render', () => {
|
||||
factory({
|
||||
webUrl: 'http://test.com',
|
||||
webPath: '/test.md',
|
||||
name: 'README.md',
|
||||
});
|
||||
|
||||
|
|
@ -60,6 +62,7 @@ describe('Repository file preview component', () => {
|
|||
it('renders loading icon', () => {
|
||||
factory({
|
||||
webUrl: 'http://test.com',
|
||||
webPath: '/test.md',
|
||||
name: 'README.md',
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ RSpec.describe Mutations::Todos::MarkAllDone do
|
|||
|
||||
describe '#resolve' do
|
||||
it 'marks all pending todos as done' do
|
||||
updated_todo_ids = mutation_for(current_user).resolve.dig(:updated_ids)
|
||||
updated_todo_ids, todos = mutation_for(current_user).resolve.values_at(:updated_ids, :todos)
|
||||
|
||||
expect(todo1.reload.state).to eq('done')
|
||||
expect(todo2.reload.state).to eq('done')
|
||||
|
|
@ -29,6 +29,7 @@ RSpec.describe Mutations::Todos::MarkAllDone do
|
|||
expect(other_user_todo.reload.state).to eq('pending')
|
||||
|
||||
expect(updated_todo_ids).to contain_exactly(global_id_of(todo1), global_id_of(todo3))
|
||||
expect(todos).to contain_exactly(todo1, todo3)
|
||||
end
|
||||
|
||||
it 'behaves as expected if there are no todos for the requesting user' do
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ RSpec.describe Mutations::Todos::RestoreMany do
|
|||
todo_ids = result[:updated_ids]
|
||||
expect(todo_ids.size).to eq(1)
|
||||
expect(todo_ids.first).to eq(todo1.to_global_id.to_s)
|
||||
|
||||
expect(result[:todos]).to contain_exactly(todo1)
|
||||
end
|
||||
|
||||
it 'handles a todo which is already pending as expected' do
|
||||
|
|
@ -33,6 +35,7 @@ RSpec.describe Mutations::Todos::RestoreMany do
|
|||
expect_states_were_not_changed
|
||||
|
||||
expect(result[:updated_ids]).to eq([])
|
||||
expect(result[:todos]).to be_empty
|
||||
end
|
||||
|
||||
it 'ignores requests for todos which do not belong to the current user' do
|
||||
|
|
@ -56,6 +59,7 @@ RSpec.describe Mutations::Todos::RestoreMany do
|
|||
|
||||
returned_todo_ids = result[:updated_ids]
|
||||
expect(returned_todo_ids).to contain_exactly(todo1.to_global_id.to_s, todo4.to_global_id.to_s)
|
||||
expect(result[:todos]).to contain_exactly(todo1, todo4)
|
||||
|
||||
expect(todo1.reload.state).to eq('pending')
|
||||
expect(todo2.reload.state).to eq('pending')
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ RSpec.describe GitlabSchema.types['Commit'] do
|
|||
it 'contains attributes related to commit' do
|
||||
expect(described_class).to have_graphql_fields(
|
||||
:id, :sha, :title, :description, :message, :title_html, :authored_date,
|
||||
:author_name, :author_gravatar, :author, :web_url, :latest_pipeline,
|
||||
:author_name, :author_gravatar, :author, :web_path, :web_url, :latest_pipeline,
|
||||
:pipelines, :signature_html
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,5 +5,5 @@ require 'spec_helper'
|
|||
RSpec.describe Types::Tree::BlobType do
|
||||
specify { expect(described_class.graphql_name).to eq('Blob') }
|
||||
|
||||
specify { expect(described_class).to have_graphql_fields(:id, :sha, :name, :type, :path, :flat_path, :web_url, :lfs_oid) }
|
||||
specify { expect(described_class).to have_graphql_fields(:id, :sha, :name, :type, :path, :flat_path, :web_path, :web_url, :lfs_oid) }
|
||||
end
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue