Commit a90a4974 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into feature/gb/update-pending-builds-namespace-traversal-ids

parents e9c614cc a0626617
......@@ -88,7 +88,7 @@ rules:
- pattern: test_fixtures/**
group: internal
alphabetize:
order: asc
order: ignore
overrides:
- files:
- '**/spec/**/*'
......
......@@ -75,17 +75,3 @@ ui-docs-links lint:
needs: []
script:
- bundle exec haml-lint -i DocumentationLinks
deprecations-doc check:
variables:
SETUP_DB: "false"
extends:
- .default-retry
- .rails-cache
- .default-before_script
- .docs:rules:deprecations
stage: test
needs: []
script:
- bundle exec rake gitlab:docs:check_deprecations
allow_failure: true
......@@ -150,13 +150,6 @@
- ".markdownlint.yml"
- "scripts/lint-doc.sh"
.docs-deprecations-patterns: &docs-deprecations-patterns
- "doc/deprecations/index.md"
- "data/deprecations/*.yml"
- "data/deprecations/templates/_deprecation_template.md.erb"
- "lib/tasks/gitlab/docs/compile_deprecations.rake"
- "tooling/deprecations/docs.rb"
.bundler-patterns: &bundler-patterns
- '{Gemfile.lock,*/Gemfile.lock,*/*/Gemfile.lock}'
......@@ -470,12 +463,6 @@
changes: *docs-patterns
when: on_success
.docs:rules:deprecations:
rules:
- <<: *if-default-refs
changes: *docs-deprecations-patterns
when: on_success
##################
# GraphQL rules #
##################
......
This diff is collapsed.
ccd8ea3e1436d6464ac5f67e6bebd1ae317f4d50
e6374eaf67addb939516b5ad5d242418a6f2b688
......@@ -154,7 +154,7 @@ gem 'html-pipeline', '~> 2.13.2'
gem 'deckar01-task_list', '2.3.1'
gem 'gitlab-markup', '~> 1.7.1'
gem 'github-markup', '~> 1.7.0', require: 'github/markup'
gem 'commonmarker', '~> 0.21'
gem 'commonmarker', '~> 0.23.2'
gem 'kramdown', '~> 2.3.1'
gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~> 6.3.2'
......@@ -474,7 +474,7 @@ end
gem 'spamcheck', '~> 0.1.0'
# Gitaly GRPC protocol definitions
gem 'gitaly', '~> 14.3.0.pre.rc1'
gem 'gitaly', '~> 14.3.0.pre.rc2'
# KAS GRPC protocol definitions
gem 'kas-grpc', '~> 0.0.2'
......
......@@ -200,8 +200,7 @@ GEM
open4 (~> 1.3)
coderay (1.1.3)
colored2 (3.1.2)
commonmarker (0.21.0)
ruby-enum (~> 0.5)
commonmarker (0.23.2)
concurrent-ruby (1.1.9)
connection_pool (2.2.2)
contracts (0.11.0)
......@@ -453,7 +452,7 @@ GEM
rails (>= 3.2.0)
git (1.7.0)
rchardet (~> 1.8)
gitaly (14.3.0.pre.rc1)
gitaly (14.3.0.pre.rc2)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab (4.16.1)
......@@ -1116,8 +1115,6 @@ GEM
rubocop-rspec (1.44.1)
rubocop (~> 0.87)
rubocop-ast (>= 0.7.1)
ruby-enum (0.8.0)
i18n
ruby-fogbugz (0.2.1)
crack (~> 0.4)
ruby-magic (0.4.0)
......@@ -1414,7 +1411,7 @@ DEPENDENCIES
capybara-screenshot (~> 1.0.22)
carrierwave (~> 1.3)
charlock_holmes (~> 0.7.7)
commonmarker (~> 0.21)
commonmarker (~> 0.23.2)
concurrent-ruby (~> 1.1)
connection_pool (~> 2.0)
countries (~> 3.0)
......@@ -1464,7 +1461,7 @@ DEPENDENCIES
gettext (~> 3.3)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly (~> 14.3.0.pre.rc1)
gitaly (~> 14.3.0.pre.rc2)
github-markup (~> 1.7.0)
gitlab-chronic (~> 0.10.5)
gitlab-dangerfiles (~> 2.3.0)
......
14.3.0-pre
\ No newline at end of file
14.4.0-pre
\ No newline at end of file
......@@ -6,7 +6,7 @@ import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
});
export default (containerId = 'js-artifacts-settings-app') => {
......
/* global $ */
/* eslint-disable import/order */
import jQuery from 'jquery';
import Cookies from 'js-cookie';
......@@ -15,6 +14,7 @@ import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { initRails } from '~/lib/utils/rails_ujs';
import * as popovers from '~/popovers';
import * as tooltips from '~/tooltips';
import { initHeaderSearchApp } from '~/header_search';
import initAlertHandler from './alert_handler';
import { removeFlashClickListener } from './flash';
import initTodoToggle from './header';
......@@ -36,7 +36,6 @@ import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
import initBroadcastNotifications from './broadcast_notification';
import { initTopNav } from './nav';
import { initHeaderSearchApp } from '~/header_search';
import 'ee_else_ce/main_ee';
import 'jh_else_ce/main_jh';
......
<script>
import { GlAlert, GlFormGroup, GlFormInputGroup, GlSprintf } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
export default {
components: {
GlFormGroup,
GlAlert,
GlFormInputGroup,
GlSprintf,
ClipboardButton,
TitleArea,
},
inject: ['groupPath', 'dependencyProxyAvailable'],
i18n: {
subTitle: __(
'Create a local proxy for storing frequently used upstream images. %{docLinkStart}Learn more%{docLinkEnd} about dependency proxies.',
),
proxyNotAvailableText: __('Dependency proxy feature is limited to public groups for now.'),
proxyImagePrefix: __('Dependency proxy image prefix'),
copyImagePrefixText: __('Copy prefix'),
blobCountAndSize: __('Contains %{count} blobs of images (%{size})'),
},
data() {
return {
dependencyProxyTotalSize: 0,
dependencyProxyImagePrefix: '',
dependencyProxyBlobCount: 0,
};
},
computed: {
infoMessages() {
return [
{
text: this.$options.i18n.subTitle,
link: helpPagePath('user/packages/dependency_proxy/index'),
},
];
},
humanizedTotalSize() {
return numberToHumanSize(this.dependencyProxyTotalSize);
},
},
};
</script>
<template>
<div>
<title-area :title="__('Dependency Proxy')" :info-messages="infoMessages" />
<gl-alert v-if="!dependencyProxyAvailable" :dismissible="false">
{{ $options.i18n.proxyNotAvailableText }}
</gl-alert>
<div v-else data-testid="main-area">
<gl-form-group :label="$options.i18n.proxyImagePrefix">
<gl-form-input-group
readonly
:value="dependencyProxyImagePrefix"
class="gl-layout-w-limited"
>
<template #append>
<clipboard-button
:text="dependencyProxyImagePrefix"
:title="$options.i18n.copyImagePrefixText"
/>
</template>
</gl-form-input-group>
<template #description>
<span data-qa-selector="dependency_proxy_count" data-testid="proxy-count">
<gl-sprintf :message="$options.i18n.blobCountAndSize">
<template #count>{{ dependencyProxyBlobCount }}</template>
<template #size>{{ humanizedTotalSize }}</template>
</gl-sprintf>
</span>
</template>
</gl-form-group>
</div>
</div>
</template>
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import app from '~/packages_and_registries/dependency_proxy/app.vue';
import { apolloProvider } from '~/packages_and_registries/package_registry/graphql';
import Translate from '~/vue_shared/translate';
Vue.use(Translate);
export const initDependencyProxyApp = () => {
const el = document.getElementById('js-dependency-proxy');
if (!el) {
return null;
}
const { dependencyProxyAvailable, ...dataset } = el.dataset;
return new Vue({
el,
apolloProvider,
provide: {
dependencyProxyAvailable: parseBoolean(dependencyProxyAvailable),
...dataset,
},
render(createElement) {
return createElement(app);
},
});
};
/* eslint-disable @gitlab/require-string-literal-i18n-helpers */
import { __, s__ } from '~/locale';
import { s__ } from '~/locale';
export const PACKAGE_TYPE_CONAN = 'CONAN';
export const PACKAGE_TYPE_MAVEN = 'MAVEN';
......@@ -71,7 +70,7 @@ export const DELETE_PACKAGE_ERROR_MESSAGE = s__(
'PackageRegistry|Something went wrong while deleting the package.',
);
export const DELETE_PACKAGE_FILE_ERROR_MESSAGE = s__(
__('PackageRegistry|Something went wrong while deleting the package file.'),
'PackageRegistry|Something went wrong while deleting the package file.',
);
export const DELETE_PACKAGE_FILE_SUCCESS_MESSAGE = s__(
'PackageRegistry|Package file deleted successfully',
......
......@@ -4,6 +4,7 @@ import {
GlButtonGroup,
GlDropdown,
GlDropdownItem,
GlDropdownText,
GlDropdownSectionHeader,
GlLoadingIcon,
GlSearchBoxByType,
......@@ -20,6 +21,7 @@ export default {
GlButtonGroup,
GlDropdown,
GlDropdownItem,
GlDropdownText,
GlDropdownSectionHeader,
GlLoadingIcon,
GlSearchBoxByType,
......@@ -57,8 +59,20 @@ export default {
userNamespace() {
return this.currentUser.namespace || {};
},
hasGroupMatches() {
return this.userGroups.length;
},
hasNamespaceMatches() {
return this.userNamespace.fullPath?.toLowerCase().includes(this.search.toLowerCase());
},
hasNoMatches() {
return !this.hasGroupMatches && !this.hasNamespaceMatches;
},
},
methods: {
focusInput() {
this.$refs.search.focusInput();
},
handleClick({ id, fullPath }) {
this.selectedNamespace = {
id: getIdFromGraphQLId(id),
......@@ -78,18 +92,24 @@ export default {
toggle-class="gl-rounded-top-right-base! gl-rounded-bottom-right-base!"
data-qa-selector="select_namespace_dropdown"
@show="track('activate_form_input', { label: trackLabel, property: 'project_path' })"
@shown="focusInput"
>
<gl-search-box-by-type v-model.trim="search" />
<gl-search-box-by-type ref="search" v-model.trim="search" />
<gl-loading-icon v-if="$apollo.queries.currentUser.loading" />
<template v-else>
<gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header>
<gl-dropdown-item v-for="group of userGroups" :key="group.id" @click="handleClick(group)">
{{ group.fullPath }}
</gl-dropdown-item>
<gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header>
<gl-dropdown-item @click="handleClick(userNamespace)">
{{ userNamespace.fullPath }}
</gl-dropdown-item>
<template v-if="hasGroupMatches">
<gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header>
<gl-dropdown-item v-for="group of userGroups" :key="group.id" @click="handleClick(group)">
{{ group.fullPath }}
</gl-dropdown-item>
</template>
<template v-if="hasNamespaceMatches">
<gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header>
<gl-dropdown-item @click="handleClick(userNamespace)">
{{ userNamespace.fullPath }}
</gl-dropdown-item>
</template>
<gl-dropdown-text v-if="hasNoMatches">{{ __('No matches found') }}</gl-dropdown-text>
</template>
</gl-dropdown>
......
......@@ -2,8 +2,8 @@
import { escape, find, countBy } from 'lodash';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { n__, s__, __, sprintf } from '~/locale';
import { getUsers, getGroups, getDeployKeys } from './api/access_dropdown_api';
import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVELS, ACCESS_LEVEL_NONE } from './constants';
export default class AccessDropdown {
......@@ -16,9 +16,6 @@ export default class AccessDropdown {
this.accessLevelsData = accessLevelsData.roles;
this.$dropdown = $dropdown;
this.$wrap = this.$dropdown.closest(`.${this.accessLevel}-container`);
this.usersPath = '/-/autocomplete/users.json';
this.groupsPath = '/-/autocomplete/project_groups.json';
this.deployKeysPath = '/-/autocomplete/deploy_keys_with_owners.json';
this.defaultLabel = this.$dropdown.data('defaultLabel');
this.setSelectedItems([]);
......@@ -318,9 +315,9 @@ export default class AccessDropdown {
getData(query, callback) {
if (this.hasLicense) {
Promise.all([
this.getDeployKeys(query),
this.getUsers(query),
this.groupsData ? Promise.resolve(this.groupsData) : this.getGroups(),
getDeployKeys(query),
getUsers(query),
this.groupsData ? Promise.resolve(this.groupsData) : getGroups(),
])
.then(([deployKeysResponse, usersResponse, groupsResponse]) => {
this.groupsData = groupsResponse;
......@@ -332,7 +329,7 @@ export default class AccessDropdown {
createFlash({ message: __('Failed to load groups, users and deploy keys.') });
});
} else {
this.getDeployKeys(query)
getDeployKeys(query)
.then((deployKeysResponse) => callback(this.consolidateData(deployKeysResponse.data)))
.catch(() => createFlash({ message: __('Failed to load deploy keys.') }));
}
......@@ -473,46 +470,6 @@ export default class AccessDropdown {
return consolidatedData;
}
getUsers(query) {
return axios.get(this.buildUrl(gon.relative_url_root, this.usersPath), {
params: {
search: query,
per_page: 20,
active: true,
project_id: gon.current_project_id,
push_code: true,
},
});
}
getGroups() {
return axios.get(this.buildUrl(gon.relative_url_root, this.groupsPath), {
params: {
project_id: gon.current_project_id,
},
});
}
getDeployKeys(query) {
return axios.get(this.buildUrl(gon.relative_url_root, this.deployKeysPath), {
params: {
search: query,
per_page: 20,
active: true,
project_id: gon.current_project_id,
push_code: true,
},
});
}
buildUrl(urlRoot, url) {
let newUrl;
if (urlRoot != null) {
newUrl = urlRoot.replace(/\/$/, '') + url;
}
return newUrl;
}
renderRow(item) {
let criteria = {};
let groupRowEl;
......
import axios from '~/lib/utils/axios_utils';
const USERS_PATH = '/-/autocomplete/users.json';
const GROUPS_PATH = '/-/autocomplete/project_groups.json';
const DEPLOY_KEYS_PATH = '/-/autocomplete/deploy_keys_with_owners.json';
const buildUrl = (urlRoot, url) => {
let newUrl;
if (urlRoot != null) {
newUrl = urlRoot.replace(/\/$/, '') + url;
}
return newUrl;
};
export const getUsers = (query) => {
return axios.get(buildUrl(gon.relative_url_root || '', USERS_PATH), {
params: {
search: query,
per_page: 20,
active: true,
project_id: gon.current_project_id,
push_code: true,
},
});
};
export const getGroups = () => {
return axios.get(buildUrl(gon.relative_url_root || '', GROUPS_PATH), {
params: {
project_id: gon.current_project_id,
},
});
};
export const getDeployKeys = (query) => {
return axios.get(buildUrl(gon.relative_url_root || '', DEPLOY_KEYS_PATH), {
params: {
search: query,
per_page: 20,
active: true,
project_id: gon.current_project_id,
push_code: true,
},
});
};
<script>
import {
GlDropdown,
GlDropdownItem,
GlDropdownSectionHeader,
GlDropdownDivider,
GlSearchBoxByType,
GlLoadingIcon,
GlAvatar,
GlSprintf,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import createFlash from '~/flash';
import { __, s__, n__ } from '~/locale';
import { getUsers, getGroups, getDeployKeys } from '../api/access_dropdown_api';
import { LEVEL_TYPES, ACCESS_LEVELS } from '../constants';
export const i18n = {
selectUsers: s__('ProtectedEnvironment|Select users'),
rolesSectionHeader: s__('AccessDropdown|Roles'),
groupsSectionHeader: s__('AccessDropdown|Groups'),
usersSectionHeader: s__('AccessDropdown|Users'),
deployKeysSectionHeader: s__('AccessDropdown|Deploy Keys'),
ownedBy: __('Owned by %{image_tag}'),
};
export default {
i18n,
components: {
GlDropdown,
GlDropdownItem,
GlDropdownSectionHeader,
GlDropdownDivider,
GlSearchBoxByType,
GlLoadingIcon,
GlAvatar,
GlSprintf,
},
props: {
accessLevelsData: {
type: Array,
required: true,
},
accessLevel: {
required: true,
type: String,
},
hasLicense: {
required: false,
type: Boolean,
default: true,
},
},
data() {
return {
loading: false,
query: '',
users: [],
groups: [],
roles: [],
deployKeys: [],
selected: {
[LEVEL_TYPES.GROUP]: [],
[LEVEL_TYPES.USER]: [],
[LEVEL_TYPES.ROLE]: [],
[LEVEL_TYPES.DEPLOY_KEY]: [],
},
};
},
computed: {
showDeployKeys() {
return this.accessLevel === ACCESS_LEVELS.PUSH && this.deployKeys.length;
},
toggleLabel() {
const counts = Object.entries(this.selected).reduce((acc, [key, value]) => {
acc[key] = value.length;
return acc;
}, {});
const isOnlyRoleSelected =
counts[LEVEL_TYPES.ROLE] === 1 &&
[counts[LEVEL_TYPES.USER], counts[LEVEL_TYPES.GROUP], counts[LEVEL_TYPES.DEPLOY_KEY]].every(
(count) => count === 0,
);
if (isOnlyRoleSelected) {
return this.selected[LEVEL_TYPES.ROLE][0].text;
}
const labelPieces = [];
if (counts[LEVEL_TYPES.ROLE] > 0) {
labelPieces.push(n__('1 role', '%d roles', counts[LEVEL_TYPES.ROLE]));
}
if (counts[LEVEL_TYPES.USER] > 0) {
labelPieces.push(n__('1 user', '%d users', counts[LEVEL_TYPES.USER]));
}
if (counts[LEVEL_TYPES.DEPLOY_KEY] > 0) {
labelPieces.push(n__('1 deploy key', '%d deploy keys', counts[LEVEL_TYPES.DEPLOY_KEY]));
}
if (counts[LEVEL_TYPES.GROUP] > 0) {
labelPieces.push(n__('1 group', '%d groups', counts[LEVEL_TYPES.GROUP]));
}
return labelPieces.join(', ') || i18n.selectUsers;
},
toggleClass() {
return this.toggleLabel === i18n.selectUsers ? 'gl-text-gray-500!' : '';
},
},
watch: {
query: debounce(function debouncedSearch() {
return this.getData();
}, 500),
},
created() {
this.getData();
},
methods: {
focusInput() {
this.$refs.search.focusInput();
},
getData() {
this.loading = true;
if (this.hasLicense) {
Promise.all([
getDeployKeys(this.query),
getUsers(this.query),
this.groups.length ? Promise.resolve({ data: this.groups }) : getGroups(),
])
.then(([deployKeysResponse, usersResponse, groupsResponse]) =>
this.consolidateData(deployKeysResponse.data, usersResponse.data, groupsResponse.data),
)
.catch(() =>
createFlash({ message: __('Failed to load groups, users and deploy keys.') }),
)
.finally(() => {
this.loading = false;
});
} else {
getDeployKeys(this.query)
.then((deployKeysResponse) => this.consolidateData(deployKeysResponse.data))
.catch(() => createFlash({ message: __('Failed to load deploy keys.') }))
.finally(() => {
this.loading = false;
});
}
},
consolidateData(deployKeysResponse, usersResponse = [], groupsResponse = []) {
// This re-assignment is intentional as level.type property is being used for comparision,
// and accessLevelsData is provided by gon.create_access_levels which doesn't have `type` included.
// See this discussion https://gitlab.com/gitlab-org/gitlab/merge_requests/1629#note_31285823
this.roles = this.accessLevelsData.map((role) => ({ ...role, type: LEVEL_TYPES.ROLE }));
if (this.hasLicense) {
this.groups = groupsResponse.map((group) => ({ ...group, type: LEVEL_TYPES.GROUP }));
this.users = usersResponse.map((user) => ({ ...user, type: LEVEL_TYPES.USER }));
}
this.deployKeys = deployKeysResponse.map((response) => {
const {
id,
fingerprint,
title,
owner: { avatar_url, name, username },
} = response;
const shortFingerprint = `(${fingerprint.substring(0, 14)}...)`;
return {
id,
title: title.concat(' ', shortFingerprint),
avatar_url,
fullname: name,
username,
type: LEVEL_TYPES.DEPLOY_KEY,
};
});
},
onItemClick(item) {
this.toggleSelection(this.selected[item.type], item);
this.emitUpdate();
},
toggleSelection(arr, item) {
const itemIndex = arr.indexOf(item);
if (itemIndex > -1) {
arr.splice(itemIndex, 1);
} else arr.push(item);
},
isSelected(item) {
return this.selected[item.type].some((selected) => selected.id === item.id);
},
emitUpdate() {
const selected = Object.values(this.selected).flat();
this.$emit('select', selected);
},
},
};
</script>
<template>
<gl-dropdown
:text="toggleLabel"
class="gl-display-block"
:toggle-class="toggleClass"
aria-labelledby="allowed-users-label"
@shown="focusInput"
>
<template #header>
<gl-search-box-by-type ref="search" v-model.trim="query" />
<gl-loading-icon v-if="loading" size="sm" />
</template>
<template v-if="roles.length">
<gl-dropdown-section-header>{{
$options.i18n.rolesSectionHeader
}}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="role in roles"
:key="role.id"
is-check-item
:is-checked="isSelected(role)"
@click.native.capture.stop="onItemClick(role)"
>
{{ role.text }}
</gl-dropdown-item>
<gl-dropdown-divider v-if="groups.length || users.length || showDeployKeys" />
</template>
<template v-if="groups.length">
<gl-dropdown-section-header>{{
$options.i18n.groupsSectionHeader
}}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="group in groups"
:key="group.id"
:avatar-url="group.avatar_url"
is-check-item
:is-checked="isSelected(group)"
@click.native.capture.stop="onItemClick(group)"
>
{{ group.name }}
</gl-dropdown-item>
<gl-dropdown-divider v-if="users.length || showDeployKeys" />
</template>
<template v-if="users.length">
<gl-dropdown-section-header>{{
$options.i18n.usersSectionHeader
}}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="user in users"
:key="user.id"
:avatar-url="user.avatar_url"
:secondary-text="user.username"
is-check-item
:is-checked="isSelected(user)"
@click.native.capture.stop="onItemClick(user)"
>
{{ user.name }}
</gl-dropdown-item>
<gl-dropdown-divider v-if="showDeployKeys" />
</template>
<template v-if="showDeployKeys">
<gl-dropdown-section-header>{{
$options.i18n.deployKeysSectionHeader
}}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="key in deployKeys"
:key="key.id"
is-check-item
:is-checked="isSelected(key)"
class="gl-text-truncate"
@click.native.capture.stop="onItemClick(key)"
>
<div class="gl-text-truncate gl-font-weight-bold">{{ key.title }}</div>
<div class="gl-text-gray-700 gl-text-truncate">
<gl-sprintf :message="$options.i18n.ownedBy">
<template #image_tag>
<gl-avatar :src="key.avatar_url" :size="24" />
</template> </gl-sprintf
>{{ key.fullname }} ({{ key.username }})
</div>
</gl-dropdown-item>
</template>
</gl-dropdown>
</template>
import Vue from 'vue';
import AccessDropdown from './components/access_dropdown.vue';
export const initAccessDropdown = (el, options) => {
if (!el) {
return false;
}
const { accessLevelsData, accessLevel } = options;
return new Vue({
el,
render(createElement) {
const vm = this;
return createElement(AccessDropdown, {
props: {
accessLevel,
accessLevelsData: accessLevelsData.roles,
},
on: {
select(selected) {
vm.$emit('select', selected);
},
},
});
},
});
};
......@@ -7,6 +7,7 @@ import {
GlLoadingIcon,
GlIcon,
GlHoverLoadDirective,
GlSafeHtmlDirective,
} from '@gitlab/ui';
import { escapeRegExp } from 'lodash';
import filesQuery from 'shared_queries/repository/files.query.graphql';
......@@ -33,6 +34,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
GlHoverLoad: GlHoverLoadDirective,
SafeHtml: GlSafeHtmlDirective,
},
apollo: {
commit: {
......@@ -178,6 +180,7 @@ export default {
this.$apollo.query({ query, variables });
},
},
safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
};
</script>
......@@ -228,10 +231,10 @@ export default {
<td class="d-none d-sm-table-cell tree-commit cursor-default">
<gl-link
v-if="commit"
v-safe-html:[$options.safeHtmlConfig]="commit.titleHtml"
:href="commit.commitPath"
:title="commit.message"
class="str-truncated-100 tree-commit-link"
v-html="commit.titleHtml /* eslint-disable-line vue/no-v-html */"
/>
<gl-skeleton-loading v-else :lines="1" class="h-auto" />
</td>
......
<script>
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import { GlAlert } from '@gitlab/ui';
import { slugifyWithUnderscore } from '~/lib/utils/text_utility';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
export default {
components: {
GlAlert,
GlSprintf,
GlLink,
LocalStorageSync,
},
props: {
......@@ -15,10 +13,6 @@ export default {
type: String,
required: true,
},
feedbackLink: {
type: String,
required: true,
},
},
data() {
return {
......@@ -44,19 +38,8 @@ export default {
<template>
<div v-show="showAlert">
<local-storage-sync v-model="isDismissed" :storage-key="storageKey" as-json />
<gl-alert v-if="showAlert" class="gl-mt-5" @dismiss="dismissFeedbackAlert">
<gl-sprintf
:message="
__(
'Please share your feedback about %{featureName} %{linkStart}in this issue%{linkEnd} to help us improve the experience.',
)
"
>
<template #featureName>{{ featureName }}</template>
<template #link="{ content }">
<gl-link :href="feedbackLink" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
<gl-alert v-if="showAlert" @dismiss="dismissFeedbackAlert">
<slot></slot>
</gl-alert>
</div>
</template>
......@@ -22,7 +22,7 @@ module Clusters
attr_reader :project, :current_user, :params
def can_read_cluster_agents?
project.feature_available?(:cluster_agents) && current_user.can?(:read_cluster, project)
current_user.can?(:read_cluster, project)
end
end
end
......@@ -64,8 +64,8 @@ module Ci
delegate :gitlab_deploy_token, to: :project
delegate :trigger_short_token, to: :trigger_request, allow_nil: true
ignore_columns :id_convert_to_bigint, remove_with: '14.1', remove_after: '2021-07-22'
ignore_columns :stage_id_convert_to_bigint, remove_with: '14.1', remove_after: '2021-07-22'
ignore_columns :id_convert_to_bigint, remove_with: '14.5', remove_after: '2021-10-22'
ignore_columns :stage_id_convert_to_bigint, remove_with: '14.5', remove_after: '2021-10-22'
##
# Since Gitlab 11.5, deployments records started being created right after
......
......@@ -82,7 +82,8 @@ module Ci
# Merge requests for which the current pipeline is running against
# the merge request's latest commit.
has_many :merge_requests_as_head_pipeline, foreign_key: "head_pipeline_id", class_name: 'MergeRequest'
has_many :package_build_infos, class_name: 'Packages::BuildInfo', dependent: :nullify, inverse_of: :pipeline # rubocop:disable Cop/ActiveRecordDependent
has_many :package_file_build_infos, class_name: 'Packages::PackageFileBuildInfo', dependent: :nullify, inverse_of: :pipeline # rubocop:disable Cop/ActiveRecordDependent
has_many :pending_builds, -> { pending }, foreign_key: :commit_id, class_name: 'Ci::Build', inverse_of: :pipeline
has_many :failed_builds, -> { latest.failed }, foreign_key: :commit_id, class_name: 'Ci::Build', inverse_of: :pipeline
has_many :retryable_builds, -> { latest.failed_or_canceled.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build', inverse_of: :pipeline
......
......@@ -14,6 +14,12 @@ module Ci
before_create :ensure_resource
enum process_mode: {
unordered: 0,
oldest_first: 1,
newest_first: 2
}
##
# NOTE: This is concurrency-safe method that the subquery in the `UPDATE`
# works as explicit locking.
......@@ -25,8 +31,34 @@ module Ci
resources.retained_by(processable).update_all(build_id: nil) > 0
end
def upcoming_processables
if unordered? || Feature.disabled?(:ci_resource_group_process_modes, project, default_enabled: :yaml)
processables.waiting_for_resource
elsif oldest_first?
processables.waiting_for_resource_or_upcoming
.order(Arel.sql("commit_id ASC, #{sort_by_job_status}"))
elsif newest_first?
processables.waiting_for_resource_or_upcoming
.order(Arel.sql("commit_id DESC, #{sort_by_job_status}"))
else
Ci::Processable.none
end
end
private
# In order to avoid deadlock, we do NOT specify the job execution order in the same pipeline.
# The system processes wherever ready to transition to `pending` status from `waiting_for_resource`.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/202186 for more information.
def sort_by_job_status
<<~SQL
CASE status
WHEN 'waiting_for_resource' THEN 0
ELSE 1
END ASC
SQL
end
def ensure_resource
# Currently we only support one resource per group, which means
# maximum one build can be set to the resource group, thus builds
......
......@@ -95,6 +95,7 @@ module Ci
scope :failed_or_canceled, -> { with_status(:failed, :canceled) }
scope :complete, -> { with_status(completed_statuses) }
scope :incomplete, -> { without_statuses(completed_statuses) }
scope :waiting_for_resource_or_upcoming, -> { with_status(:created, :scheduled, :waiting_for_resource) }
scope :cancelable, -> do
where(status: [:running, :waiting_for_resource, :preparing, :pending, :created, :scheduled])
......
......@@ -72,12 +72,10 @@ module HasRepository
end
def default_branch
@default_branch ||= repository.root_ref || default_branch_from_preferences
@default_branch ||= repository.empty? ? default_branch_from_preferences : repository.root_ref
end
def default_branch_from_preferences
return unless empty_repo?
(default_branch_from_group_preferences || Gitlab::CurrentSettings.default_branch_name).presence
end
......
......@@ -15,13 +15,8 @@ module Integrations
end
def help
'This service sends notifications about projects events to a Unify Circuit conversation.<br />
To set up this service:
<ol>
<li><a href="https://www.circuit.com/unifyportalfaqdetail?articleId=164448" target="_blank" rel="noopener noreferrer">Set up an incoming webhook for your conversation</a>. All notifications will come to this conversation.</li>
<li>Paste the <strong>Webhook URL</strong> into the field below.</li>
<li>Select events below to enable notifications.</li>
</ol>'
docs_link = ActionController::Base.helpers.link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/unify_circuit'), target: '_blank', rel: 'noopener noreferrer'
s_('Integrations|Send notifications about project events to a Unify Circuit conversation. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
def event_field(event)
......@@ -37,7 +32,7 @@ module Integrations
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: "e.g. https://circuit.com/rest/v2/webhooks/incoming/…", required: true },
{ type: 'text', name: 'webhook', placeholder: "https://yourcircuit.com/rest/v2/webhooks/incoming/…", required: true },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{ type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
]
......
# frozen_string_literal: true
module Ci
class ResourceGroupPolicy < BasePolicy
delegate { @subject.project }
end
end
......@@ -357,6 +357,8 @@ class ProjectPolicy < BasePolicy
enable :update_commit_status
enable :create_build
enable :update_build
enable :read_resource_group
enable :update_resource_group
enable :create_merge_request_from
enable :create_wiki
enable :push_code
......
......@@ -66,14 +66,15 @@ we gain the following benefits:
### Presenter definition
Every presenter should inherit from `Gitlab::View::Presenter::Simple`, which
provides a `.presents` the method which allows you to define an accessor for the
If you need a presenter class that has only necessary interfaces for the view-related context,
inherit from `Gitlab::View::Presenter::Simple`.
It provides a `.presents` the method which allows you to define an accessor for the
presented object. It also includes common helpers like `Gitlab::Routing` and
`Gitlab::Allowable`.
```ruby
class LabelPresenter < Gitlab::View::Presenter::Simple
presents :label
presents ::Label, as: :label
def text_color
label.color.to_s
......@@ -85,13 +86,14 @@ class LabelPresenter < Gitlab::View::Presenter::Simple
end
```
In some cases, it can be more practical to transparently delegate all missing
method calls to the presented object, in these cases, you can make your
presenter inherit from `Gitlab::View::Presenter::Delegated`:
If you need a presenter class that delegates missing method calls to the presented object,
inherit from `Gitlab::View::Presenter::Delegated`.
This is more like an "extension" in the sense that the produced object is going to have
all of interfaces of the presented object **AND** all of the interfaces in the presenter class:
```ruby
class LabelPresenter < Gitlab::View::Presenter::Delegated
presents :label
presents ::Label, as: :label
def text_color
# color is delegated to label
......@@ -152,3 +154,82 @@ You can also present the model in the view:
%div{ class: label.text_color }
= render partial: label, label: label
```
### Validate accidental overrides
We use presenters in many places, such as Controller, Haml, GraphQL/Rest API,
it's very handy to extend the core/backend logic of Active Record models,
however, there is a risk that it accidentally overrides important logic.
For example, [this production incident](https://gitlab.com/gitlab-com/gl-infra/production/-/issues/5498)
was caused by [including `ActionView::Helpers::UrlHelper` in a presenter](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69537/diffs#4b581cff00ef3cc9780efd23682af383de302e7d_3_3).
The `tag` accesor in `Ci::Build` was accidentally overridden by `ActionView::Helpers::TagHelper#tag`,
and as a conseuqence, a wrong `tag` value was persited into database.
Starting from GitLab 14.4, we validate the presenters (specifically all of the subclasses of `Gitlab::View::Presenter::Delegated`)
that they do not accidentally override core/backend logic. In such case, a pipeline in merge requests fails with an error message,
here is an example:
```plaintext
We've detected that a presetner is overriding a specific method(s) on a subject model.
There is a risk that it accidentally modifies the backend/core logic that leads to production incident.
Please follow https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/presenters/README.md#validate-accidental-overrides
to resolve this error with caution.
Here are the conflict details.
- Ci::PipelinePresenter#tag is overriding Ci::Pipeline#tag. delegator_location: /devkitkat/services/rails/cache/ruby/2.7.0/gems/actionview-6.1.3.2/lib/action_view/helpers/tag_helper.rb:271 original_location: /devkitkat/services/rails/cache/ruby/2.7.0/gems/activemodel-6.1.3.2/lib/active_model/attribute_methods.rb:254
```
Here are the potential solutions:
- If the conflict happens on an instance method in the presenter:
- If you intend to override the core/backend logic, define `delegator_override <method-name>` on top of the conflicted method.
This explicitly adds the method to an allowlist.
- If you do NOT intend to override the core/backend logic, rename the method name in the presenter.
- If the conflict happens on an included module in the presenter, remove the module from the presenter and find a workaround.
### How to use the `Gitlab::Utils::DelegatorOverride` validator
If a presenter class inhertis from `Gitlab::View::Presenter::Delegated`,
you should define what object class is presented:
```ruby
class WebHookLogPresenter < Gitlab::View::Presenter::Delegated
presents ::WebHookLog, as: :web_hook_log # This defines that the presenter presents `WebHookLog` Active Record model.
```
These presenters are validated not to accidentaly override the methods in the presented object.
You can run the validation locally with:
```shell
bundle exec rake lint:static_verification
```
To add a method to an allowlist, use `delegator_override`. For example:
```ruby
class VulnerabilityPresenter < Gitlab::View::Presenter::Delegated
presents ::Vulnerability, as: :vulnerability
delegator_override :description # This adds the `description` method to an allowlist that the override is intentional.
def description
vulnerability.description || finding.description
end
```
To add methods of a module to an allowlist, use `delegator_override_with`. For example:
```ruby
module Ci
class PipelinePresenter < Gitlab::View::Presenter::Delegated
include Gitlab::Utils::StrongMemoize
include ActionView::Helpers::UrlHelper
delegator_override_with Gitlab::Utils::StrongMemoize # TODO: Remove `Gitlab::Utils::StrongMemoize` inclusion as it's duplicate
delegator_override_with ActionView::Helpers::TagHelper # TODO: Remove `ActionView::Helpers::UrlHelper` inclusion as it overrides `Ci::Pipeline#tag`
```
Keep in mind that if you use `delegator_override_with`,
there is a high chance that you're doing **something wrong**.
Read the [Validate Accidental Overrides](#validate-accidental-overrides) for more information.
......@@ -5,6 +5,9 @@ module AlertManagement
include IncidentManagement::Settings
include ActionView::Helpers::UrlHelper
presents ::AlertManagement::Alert
delegator_override_with Gitlab::Utils::StrongMemoize # TODO: Remove `Gitlab::Utils::StrongMemoize` inclusion as it's duplicate
MARKDOWN_LINE_BREAK = " \n"
HORIZONTAL_LINE = "\n\n---\n\n"
......@@ -29,6 +32,7 @@ module AlertManagement
started_at&.strftime('%d %B %Y, %-l:%M%p (%Z)')
end
delegator_override :details_url
def details_url
details_project_alert_management_url(project, alert.iid)
end
......
# frozen_string_literal: true
class AwardEmojiPresenter < Gitlab::View::Presenter::Delegated
presents :award_emoji
presents ::AwardEmoji, as: :award_emoji
def description
as_emoji['description']
......
......@@ -7,7 +7,7 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
include TreeHelper
include ChecksCollaboration
presents :blob
presents ::Blob, as: :blob
def highlight(to: nil, plain: nil)
load_all_blob_data
......
......@@ -6,6 +6,8 @@ module Blobs
include ActiveModel::AttributeAssignment
include Gitlab::Utils::StrongMemoize
presents ::Blob
attribute :full, :boolean, default: false
attribute :since, :integer, default: 1
attribute :to, :integer, default: 1
......
# frozen_string_literal: true
class BoardPresenter < Gitlab::View::Presenter::Delegated
presents :board
presents ::Board, as: :board
end
......@@ -2,6 +2,9 @@
module Ci
class BridgePresenter < ProcessablePresenter
presents ::Ci::Bridge
delegator_override :detailed_status
def detailed_status
@detailed_status ||= subject.detailed_status(user)
end
......
......@@ -9,8 +9,9 @@ module Ci
job_timeout_source: 'job'
}.freeze
presents :metadata
presents ::Ci::BuildMetadata, as: :metadata
delegator_override :timeout_source
def timeout_source
return unless metadata.timeout_source?
......
......@@ -2,6 +2,8 @@
module Ci
class BuildPresenter < ProcessablePresenter
presents ::Ci::Build
def erased_by_user?
# Build can be erased through API, therefore it does not have
# `erased_by` user assigned in that case.
......
......@@ -2,7 +2,7 @@
module Ci
class GroupVariablePresenter < Gitlab::View::Presenter::Delegated
presents :variable
presents ::Ci::GroupVariable, as: :variable
def placeholder
'GROUP_VARIABLE'
......
......@@ -2,7 +2,7 @@
module Ci
class LegacyStagePresenter < Gitlab::View::Presenter::Delegated
presents :legacy_stage
presents ::Ci::LegacyStage, as: :legacy_stage
def latest_ordered_statuses
preload_statuses(legacy_stage.statuses.latest_ordered)
......
......@@ -5,6 +5,9 @@ module Ci
include Gitlab::Utils::StrongMemoize
include ActionView::Helpers::UrlHelper
delegator_override_with Gitlab::Utils::StrongMemoize # TODO: Remove `Gitlab::Utils::StrongMemoize` inclusion as it's duplicate
delegator_override_with ActionView::Helpers::TagHelper # TODO: Remove `ActionView::Helpers::UrlHelper` inclusion as it overrides `Ci::Pipeline#tag`
# We use a class method here instead of a constant, allowing EE to redefine
# the returned `Hash` more easily.
def self.failure_reasons
......@@ -20,8 +23,9 @@ module Ci
user_blocked: 'The user who created this pipeline is blocked.' }
end
presents :pipeline
presents ::Ci::Pipeline, as: :pipeline
delegator_override :failed_builds
def failed_builds
return [] unless can?(current_user, :read_build, pipeline)
......@@ -30,6 +34,7 @@ module Ci
end
end
delegator_override :failure_reason
def failure_reason
return unless pipeline.failure_reason?
......
......@@ -2,5 +2,6 @@
module Ci
class ProcessablePresenter < CommitStatusPresenter
presents ::Ci::Processable
end
end
......@@ -2,11 +2,14 @@
module Ci
class RunnerPresenter < Gitlab::View::Presenter::Delegated
presents :runner
presents ::Ci::Runner, as: :runner
delegator_override :locked?
def locked?
read_attribute(:locked) && project_type?
end
delegator_override :locked
alias_method :locked, :locked?
end
end
......@@ -2,7 +2,7 @@
module Ci
class StagePresenter < Gitlab::View::Presenter::Delegated
presents :stage
presents ::Ci::Stage, as: :stage
PRELOADED_RELATIONS = [:pipeline, :metadata, :tags, :job_artifacts_archive, :downstream_pipeline].freeze
......
......@@ -2,12 +2,13 @@
module Ci
class TriggerPresenter < Gitlab::View::Presenter::Delegated
presents :trigger
presents ::Ci::Trigger, as: :trigger
def has_token_exposed?
can?(current_user, :admin_trigger, trigger)
end
delegator_override :token
def token
if has_token_exposed?
trigger.token
......
......@@ -2,7 +2,7 @@
module Ci
class VariablePresenter < Gitlab::View::Presenter::Delegated
presents :variable
presents ::Ci::Variable, as: :variable
def placeholder
'PROJECT_VARIABLE'
......
# frozen_string_literal: true
class ClusterablePresenter < Gitlab::View::Presenter::Delegated
presents :clusterable
presents ::Project, ::Group, ::Clusters::Instance, as: :clusterable
def self.fabricate(clusterable, **attributes)
presenter_class = "#{clusterable.class.name}ClusterablePresenter".constantize
......
......@@ -7,7 +7,9 @@ module Clusters
include ActionView::Helpers::UrlHelper
include IconsHelper
presents :cluster
delegator_override_with ::Gitlab::Utils::StrongMemoize # TODO: Remove `::Gitlab::Utils::StrongMemoize` inclusion as it's duplicate
presents ::Clusters::Cluster, as: :cluster
# We do not want to show the group path for clusters belonging to the
# clusterable, only for the ancestor clusters.
......
......@@ -2,7 +2,7 @@
module Clusters
class IntegrationPresenter < Gitlab::View::Presenter::Delegated
presents :integration
presents ::Clusters::Integrations::Prometheus, ::Clusters::Integrations::ElasticStack, as: :integration
def application_type
integration.class.name.demodulize.underscore
......
......@@ -3,7 +3,7 @@
class CommitPresenter < Gitlab::View::Presenter::Delegated
include GlobalID::Identification
presents :commit
presents ::Commit, as: :commit
def status_for(ref)
return unless can?(current_user, :read_commit_status, commit.project)
......
......@@ -33,7 +33,7 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
private_constant :CALLOUT_FAILURE_MESSAGES
presents :build
presents ::CommitStatus, as: :build
def self.callout_failure_messages
CALLOUT_FAILURE_MESSAGES
......
......@@ -2,6 +2,8 @@
module DevOpsReport
class MetricPresenter < Gitlab::View::Presenter::Simple
presents ::DevOpsReport::Metric
delegate :created_at, to: :subject
def cards
......
......@@ -3,7 +3,7 @@
class EnvironmentPresenter < Gitlab::View::Presenter::Delegated
include ActionView::Helpers::UrlHelper
presents :environment
presents ::Environment, as: :environment
def path
project_environment_path(project, self)
......
# frozen_string_literal: true
class EventPresenter < Gitlab::View::Presenter::Delegated
presents :event
presents ::Event, as: :event
def initialize(subject, **attributes)
super
......@@ -10,6 +10,7 @@ class EventPresenter < Gitlab::View::Presenter::Delegated
end
# Caching `visible_to_user?` method in the presenter beause it might be called multiple times.
delegator_override :visible_to_user?
def visible_to_user?(user = nil)
@visible_to_user_cache.fetch(user&.id) { super(user) }
end
......
......@@ -12,7 +12,7 @@ module Gitlab
include TreeHelper
include IconsHelper
presents :blame
presents nil, as: :blame
CommitData = Struct.new(
:author_avatar,
......
......@@ -4,6 +4,8 @@ class GroupClusterablePresenter < ClusterablePresenter
extend ::Gitlab::Utils::Override
include ActionView::Helpers::UrlHelper
presents ::Group
override :cluster_status_cluster_path
def cluster_status_cluster_path(cluster, params = {})
cluster_status_group_cluster_path(clusterable, cluster, params)
......
# frozen_string_literal: true
class GroupMemberPresenter < MemberPresenter
presents ::GroupMember
private
def admin_member_permission
......
......@@ -4,6 +4,8 @@ class InstanceClusterablePresenter < ClusterablePresenter
extend ::Gitlab::Utils::Override
include ActionView::Helpers::UrlHelper
presents ::Clusters::Instance
def self.fabricate(clusterable, **attributes)
attributes_with_presenter_class = attributes.merge(presenter_class: InstanceClusterablePresenter)
......
# frozen_string_literal: true
class InvitationPresenter < Gitlab::View::Presenter::Delegated
presents :invitation
presents nil, as: :invitation
end
# frozen_string_literal: true
class IssuePresenter < Gitlab::View::Presenter::Delegated
presents :issue
presents ::Issue, as: :issue
def issue_path
url_builder.build(issue, only_path: true)
end
delegator_override :subscribed?
def subscribed?
issue.subscribed?(current_user, issue.project)
end
......
# frozen_string_literal: true
class LabelPresenter < Gitlab::View::Presenter::Delegated
presents :label
presents ::Label, as: :label
delegate :name, :full_name, to: :label_subject, prefix: :subject
delegator_override :subject # TODO: Fix `Gitlab::View::Presenter::Delegated#subject` not to override `Label#subject`.
def edit_path
case label
when GroupLabel then edit_group_label_path(label.group, label)
......
# frozen_string_literal: true
class MemberPresenter < Gitlab::View::Presenter::Delegated
presents :member
presents ::Member, as: :member
def access_level_roles
member.class.access_level_roles
......
......@@ -3,7 +3,7 @@
class MembersPresenter < Gitlab::View::Presenter::Delegated
include Enumerable
presents :members
presents nil, as: :members
def to_ary
to_a
......
......@@ -8,9 +8,11 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
include ChecksCollaboration
include Gitlab::Utils::StrongMemoize
delegator_override_with Gitlab::Utils::StrongMemoize # TODO: Remove `Gitlab::Utils::StrongMemoize` inclusion as it's duplicate
APPROVALS_WIDGET_BASE_TYPE = 'base'
presents :merge_request
presents ::MergeRequest, as: :merge_request
def ci_status
if pipeline
......@@ -183,6 +185,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
.can_push_to_branch?(source_branch)
end
delegator_override :can_remove_source_branch?
def can_remove_source_branch?
source_branch_exists? && merge_request.can_remove_source_branch?(current_user)
end
......@@ -202,6 +205,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
end
delegator_override :subscribed?
def subscribed?
merge_request.subscribed?(current_user, merge_request.target_project)
end
......
# frozen_string_literal: true
class MilestonePresenter < Gitlab::View::Presenter::Delegated
presents :milestone
presents ::Milestone, as: :milestone
def milestone_path
url_builder.build(milestone, only_path: true)
......
# frozen_string_literal: true
class PagesDomainPresenter < Gitlab::View::Presenter::Delegated
presents :pages_domain
presents ::PagesDomain, as: :pages_domain
delegator_override :subject # TODO: Fix `Gitlab::View::Presenter::Delegated#subject` not to override `PagesDomain#subject`.
def needs_verification?
Gitlab::CurrentSettings.pages_domain_verification_enabled? && unverified?
......
......@@ -4,6 +4,8 @@ class ProjectClusterablePresenter < ClusterablePresenter
extend ::Gitlab::Utils::Override
include ActionView::Helpers::UrlHelper
presents ::Project
override :cluster_status_cluster_path
def cluster_status_cluster_path(cluster, params = {})
cluster_status_project_cluster_path(clusterable, cluster, params)
......
# frozen_string_literal: true
class ProjectHookPresenter < Gitlab::View::Presenter::Delegated
presents :project_hook
presents ::ProjectHook, as: :project_hook
def logs_details_path(log)
project_hook_hook_log_path(project, self, log)
......
# frozen_string_literal: true
class ProjectMemberPresenter < MemberPresenter
presents ::ProjectMember
private
def admin_member_permission
......
......@@ -12,7 +12,10 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
include Gitlab::Utils::StrongMemoize
include Gitlab::Experiment::Dsl
presents :project
delegator_override_with GitlabRoutingHelper # TODO: Remove `GitlabRoutingHelper` inclusion as it's duplicate
delegator_override_with Gitlab::Utils::StrongMemoize # TODO: Remove `Gitlab::Utils::StrongMemoize` inclusion as it's duplicate
presents ::Project, as: :project
AnchorData = Struct.new(:is_link, :label, :link, :class_modifier, :icon, :itemprop, :data)
MAX_TOPICS_TO_SHOW = 3
......
......@@ -5,16 +5,24 @@ module Projects
class ProjectExportPresenter < Gitlab::View::Presenter::Delegated
include ActiveModel::Serializers::JSON
presents :project
presents ::Project, as: :project
# TODO: Remove `ActiveModel::Serializers::JSON` inclusion as it's duplicate
delegator_override_with ActiveModel::Serializers::JSON
delegator_override_with ActiveModel::Naming
delegator_override :include_root_in_json, :include_root_in_json?
delegator_override :project_members
def project_members
super + converted_group_members
end
delegator_override :description
def description
self.respond_to?(:override_description) ? override_description : super
end
delegator_override :protected_branches
def protected_branches
project.exported_protected_branches
end
......
......@@ -5,7 +5,7 @@ module Projects
class DeployKeysPresenter < Gitlab::View::Presenter::Simple
include Gitlab::Utils::StrongMemoize
presents :project
presents ::Project, as: :project
delegate :size, to: :enabled_keys, prefix: true
delegate :size, to: :available_project_keys, prefix: true
delegate :size, to: :available_public_keys, prefix: true
......
......@@ -3,7 +3,7 @@
class PrometheusAlertPresenter < Gitlab::View::Presenter::Delegated
include ActionView::Helpers::UrlHelper
presents :prometheus_alert
presents ::PrometheusAlert, as: :prometheus_alert
def humanized_text
operator_text =
......
......@@ -3,8 +3,10 @@
class ReleasePresenter < Gitlab::View::Presenter::Delegated
include ActionView::Helpers::UrlHelper
presents :release
presents ::Release, as: :release
# TODO: Remove `delegate` as it's redundant due to SimpleDelegator.
delegator_override :tag, :project
delegate :project, :tag, to: :release
def commit_path
......@@ -51,6 +53,7 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated
edit_project_release_url(project, release)
end
delegator_override :assets_count
def assets_count
if can_download_code?
release.assets_count
......@@ -59,6 +62,7 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated
end
end
delegator_override :name
def name
can_download_code? ? release.name : "Release-#{release.id}"
end
......
......@@ -4,7 +4,7 @@ module Releases
class EvidencePresenter < Gitlab::View::Presenter::Delegated
include ActionView::Helpers::UrlHelper
presents :evidence
presents ::Releases::Evidence, as: :evidence
def filepath
release = evidence.release
......
......@@ -3,7 +3,7 @@
class SearchServicePresenter < Gitlab::View::Presenter::Delegated
include RendersCommits
presents :search_service
presents ::SearchService, as: :search_service
SCOPE_PRELOAD_METHOD = {
projects: :with_web_entity_associations,
......@@ -18,6 +18,7 @@ class SearchServicePresenter < Gitlab::View::Presenter::Delegated
SORT_ENABLED_SCOPES = %w(issues merge_requests epics).freeze
delegator_override :search_objects
def search_objects
@search_objects ||= begin
objects = search_service.search_objects(SCOPE_PRELOAD_METHOD[scope.to_sym])
......
# frozen_string_literal: true
class SentryErrorPresenter < Gitlab::View::Presenter::Delegated
presents :error
presents nil, as: :error
FrequencyStruct = Struct.new(:time, :count, keyword_init: true)
......
# frozen_string_literal: true
class ServiceHookPresenter < Gitlab::View::Presenter::Delegated
presents :service_hook
presents ::ServiceHook, as: :service_hook
def logs_details_path(log)
project_service_hook_log_path(integration.project, integration, log)
......
......@@ -3,6 +3,8 @@
class SnippetBlobPresenter < BlobPresenter
include GitlabRoutingHelper
presents ::SnippetBlob
def rich_data
return unless blob.rich_viewer
......
# frozen_string_literal: true
class SnippetPresenter < Gitlab::View::Presenter::Delegated
presents :snippet
presents ::Snippet, as: :snippet
def raw_url
url_builder.build(snippet, raw: true)
end
delegator_override :ssh_url_to_repo
def ssh_url_to_repo
snippet.ssh_url_to_repo if snippet.repository_exists?
end
delegator_override :http_url_to_repo
def http_url_to_repo
snippet.http_url_to_repo if snippet.repository_exists?
end
......@@ -31,6 +33,7 @@ class SnippetPresenter < Gitlab::View::Presenter::Delegated
snippet.submittable_as_spam_by?(current_user)
end
delegator_override :blob
def blob
return snippet.blob if snippet.empty_repo?
......
......@@ -4,7 +4,7 @@ module Terraform
class ModulesPresenter < Gitlab::View::Presenter::Simple
attr_accessor :packages, :system
presents :modules
presents nil, as: :modules
def initialize(packages, system)
@packages = packages
......
# frozen_string_literal: true
class TodoPresenter < Gitlab::View::Presenter::Delegated
presents :todo
presents ::Todo, as: :todo
end
# frozen_string_literal: true
class TreeEntryPresenter < Gitlab::View::Presenter::Delegated
presents :tree
presents nil, as: :tree
def web_url
Gitlab::Routing.url_helpers.project_tree_url(tree.repository.project, File.join(tree.commit_id, tree.path))
......
# frozen_string_literal: true
class UserPresenter < Gitlab::View::Presenter::Delegated
presents :user
presents ::User, as: :user
def group_memberships
should_be_private? ? GroupMember.none : user.group_members
......
# frozen_string_literal: true
class WebHookLogPresenter < Gitlab::View::Presenter::Delegated
presents :web_hook_log
presents ::WebHookLog, as: :web_hook_log
def details_path
web_hook.present.logs_details_path(self)
......
......@@ -9,7 +9,7 @@ module Ci
free_resources = resource_group.resources.free.count
resource_group.processables.waiting_for_resource.take(free_resources).each do |processable|
resource_group.upcoming_processables.take(free_resources).each do |processable|
processable.enqueue_waiting_for_resource
end
end
......
......@@ -2,7 +2,7 @@
module Ci
class UpdatePendingBuildService
VALID_PARAMS = %i[instance_runners_enabled namespace_traversal_ids].freeze
VALID_PARAMS = %i[instance_runners_enabled namespace_id namespace_traversal_ids].freeze
InvalidParamsError = Class.new(StandardError)
InvalidModelError = Class.new(StandardError)
......
......@@ -29,6 +29,7 @@ module Groups
update_group_attributes
ensure_ownership
update_integrations
update_pending_builds!
end
post_update_hooks(@updated_project_ids)
......@@ -217,6 +218,15 @@ module Groups
PropagateIntegrationWorker.perform_async(integration.id)
end
end
def update_pending_builds!
update_params = {
namespace_traversal_ids: group.traversal_ids,
namespace_id: group.id
}
::Ci::UpdatePendingBuildService.new(group, update_params).execute
end
end
end
......
......@@ -104,6 +104,8 @@ module Projects
update_repository_configuration(@new_path)
execute_system_hooks
update_pending_builds!
end
post_update_hooks(project)
......@@ -253,6 +255,15 @@ module Projects
project.integrations.with_default_settings.delete_all
Integration.create_from_active_default_integrations(project, :project_id)
end
def update_pending_builds!
update_params = {
namespace_id: new_namespace.id,
namespace_traversal_ids: new_namespace.traversal_ids
}
::Ci::UpdatePendingBuildService.new(project, update_params).execute
end
end
end
......
= gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-files-limits-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
%legend.h5.gl-border-none
= _('Unauthenticated API request rate limit')
.form-group
= f.gitlab_ui_checkbox_component :throttle_unauthenticated_files_api_enabled,
_('Enable unauthenticated API request rate limit'),
help_text: _('Helps reduce request volume (e.g. from crawlers or abusive bots)'),
checkbox_options: { data: { qa_selector: 'throttle_unauthenticated_files_api_checkbox' } }
.form-group
= f.label :throttle_unauthenticated_files_api_requests_per_period, 'Max unauthenticated API requests per period per IP', class: 'label-bold'
= f.number_field :throttle_unauthenticated_files_api_requests_per_period, class: 'form-control gl-form-input'
.form-group
= f.label :throttle_unauthenticated_files_api_period_in_seconds, 'Unauthenticated API rate limit period in seconds', class: 'label-bold'
= f.number_field :throttle_unauthenticated_files_api_period_in_seconds, class: 'form-control gl-form-input'
%fieldset
%legend.h5.gl-border-none
= _('Authenticated API request rate limit')
.form-group
= f.gitlab_ui_checkbox_component :throttle_authenticated_files_api_enabled,
_('Enable authenticated API request rate limit'),
help_text: _('Helps reduce request volume (e.g. from crawlers or abusive bots)'),
checkbox_options: { data: { qa_selector: 'throttle_authenticated_files_api_checkbox' } }
.form-group
= f.label :throttle_authenticated_files_api_requests_per_period, 'Max authenticated API requests per period per user', class: 'label-bold'
= f.number_field :throttle_authenticated_files_api_requests_per_period, class: 'form-control gl-form-input'
.form-group
= f.label :throttle_authenticated_files_api_period_in_seconds, 'Authenticated API rate limit period in seconds', class: 'label-bold'
= f.number_field :throttle_authenticated_files_api_period_in_seconds, class: 'form-control gl-form-input'
= f.submit 'Save changes', class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
= gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: anchor), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
= _("Rate limits can help reduce request volume (like from crawlers or abusive bots).")
%fieldset
.form-group
= f.gitlab_ui_checkbox_component :"throttle_unauthenticated_#{setting_fragment}_enabled",
_('Enable unauthenticated API request rate limit'),
checkbox_options: { data: { qa_selector: "throttle_unauthenticated_#{setting_fragment}_checkbox" } },
label_options: { class: 'label-bold' }
.form-group
= f.label :"throttle_unauthenticated_#{setting_fragment}_requests_per_period", _('Maximum unauthenticated API requests per rate limit period per IP'), class: 'label-bold'
= f.number_field :"throttle_unauthenticated_#{setting_fragment}_requests_per_period", class: 'form-control gl-form-input'
.form-group
= f.label :"throttle_unauthenticated_#{setting_fragment}_period_in_seconds", _('Unauthenticated API rate limit period in seconds'), class: 'label-bold'
= f.number_field :"throttle_unauthenticated_#{setting_fragment}_period_in_seconds", class: 'form-control gl-form-input'
%fieldset
.form-group
= f.gitlab_ui_checkbox_component :"throttle_authenticated_#{setting_fragment}_enabled",
_('Enable authenticated API request rate limit'),
checkbox_options: { data: { qa_selector: "throttle_authenticated_#{setting_fragment}_checkbox" } },
label_options: { class: 'label-bold' }
.form-group
= f.label :"throttle_authenticated_#{setting_fragment}_requests_per_period", _('Maximum authenticated API requests per rate limit period per user'), class: 'label-bold'
= f.number_field :"throttle_authenticated_#{setting_fragment}_requests_per_period", class: 'form-control gl-form-input'
.form-group
= f.label :"throttle_authenticated_#{setting_fragment}_period_in_seconds", _('Authenticated API rate limit period in seconds'), class: 'label-bold'
= f.number_field :"throttle_authenticated_#{setting_fragment}_period_in_seconds", class: 'form-control gl-form-input'
= f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
= form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-packages-limits-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
= _("The package registry rate limits can help reduce request volume (like from crawlers or abusive bots).")
%fieldset
.form-group
.form-check
= f.check_box :throttle_unauthenticated_packages_api_enabled, class: 'form-check-input', data: { qa_selector: 'throttle_unauthenticated_packages_api_checkbox' }
= f.label :throttle_unauthenticated_packages_api_enabled, class: 'form-check-label label-bold' do
= _('Enable unauthenticated API request rate limit')
.form-group
= f.label :throttle_unauthenticated_packages_api_requests_per_period, _('Maximum unauthenticated API requests per rate limit period per IP'), class: 'label-bold'
= f.number_field :throttle_unauthenticated_packages_api_requests_per_period, class: 'form-control gl-form-input'
.form-group
= f.label :throttle_unauthenticated_packages_api_period_in_seconds, _('Unauthenticated API rate limit period in seconds'), class: 'label-bold'
= f.number_field :throttle_unauthenticated_packages_api_period_in_seconds, class: 'form-control gl-form-input'
%hr
.form-group
.form-check
= f.check_box :throttle_authenticated_packages_api_enabled, class: 'form-check-input', data: { qa_selector: 'throttle_authenticated_packages_api_checkbox' }
= f.label :throttle_authenticated_packages_api_enabled, class: 'form-check-label label-bold' do
= _('Enable authenticated API request rate limit')
.form-group
= f.label :throttle_authenticated_packages_api_requests_per_period, _('Maximum authenticated API requests per rate limit period per user'), class: 'label-bold'
= f.number_field :throttle_authenticated_packages_api_requests_per_period, class: 'form-control gl-form-input'
.form-group
= f.label :throttle_authenticated_packages_api_period_in_seconds, _('Authenticated API rate limit period in seconds'), class: 'label-bold'
= f.number_field :throttle_authenticated_packages_api_period_in_seconds, class: 'form-control gl-form-input'
= f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
......@@ -6,5 +6,5 @@
= f.label :terminal_max_session_time, _('Max session time'), class: 'label-bold'
= f.number_field :terminal_max_session_time, class: 'form-control gl-form-input'
.form-text.text-muted
= _('Maximum time for web terminal websocket connection (in seconds). 0 for unlimited.')
= _('Maximum time, in seconds, for a web terminal websocket connection. 0 for unlimited.')
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
......@@ -79,7 +79,8 @@
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Set max session time for web terminal.')
= _('Set the maximum session time for a web terminal.')
= link_to _('How do I use a web terminal?'), help_page_path('ci/environments/index.md', anchor: 'web-terminals'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'terminal'
......
......@@ -35,9 +35,10 @@
= _('Set rate limits for package registry API requests that supersede the general user and IP rate limits.')
= link_to _('Learn more.'), help_page_path('user/admin_area/settings/package_registry_rate_limits.md'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'package_registry_limits'
= render partial: 'network_rate_limits', locals: { anchor: 'js-packages-limits-settings', setting_fragment: 'packages_api' }
- if Feature.enabled?(:files_api_throttling, default_enabled: :yaml)
%section.settings.as-files-limits.no-animate#js-files-limits-settings{ class: ('expanded' if expanded_by_default?), data: { testid: 'files-limits-settings' } }
%section.settings.as-files-limits.no-animate#js-files-limits-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
= _('Files API Rate Limits')
......@@ -46,7 +47,7 @@
%p
= _('Configure specific limits for Files API requests that supersede the general user and IP rate limits.')
.settings-content
= render 'files_limits'
= render partial: 'network_rate_limits', locals: { anchor: 'js-files-limits-settings', setting_fragment: 'files_api' }
%section.settings.as-git-lfs-limits.no-animate#js-git-lfs-limits-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'git_lfs_limits_content' } }
.settings-header
......
......@@ -12,6 +12,6 @@
'role-arn' => @aws_role.role_arn,
'instance-types' => @instance_types,
'kubernetes-integration-help-path' => help_page_path('user/project/clusters/index'),
'account-and-external-ids-help-path' => help_page_path('user/project/clusters/add_eks_clusters.md', anchor: 'create-a-new-certificate-based-eks-cluster'),
'create-role-arn-help-path' => help_page_path('user/project/clusters/add_eks_clusters.md', anchor: 'create-a-new-certificate-based-eks-cluster'),
'account-and-external-ids-help-path' => help_page_path('user/project/clusters/add_eks_clusters.md', anchor: 'how-to-create-a-new-cluster-on-eks-through-cluster-certificates-deprecated'),
'create-role-arn-help-path' => help_page_path('user/project/clusters/add_eks_clusters.md', anchor: 'how-to-create-a-new-cluster-on-eks-through-cluster-certificates-deprecated'),
'external-link-icon' => sprite_icon('external-link') } }
......@@ -6,7 +6,7 @@
.row
.col-sm-12
= s_('UsageQuota|Usage of project resources across the %{strong_start}%{project_name}%{strong_end} project').html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, project_name: @project.name } + '.'
%a{ href: help_page_path('user/usage_quotas.md') }
%a{ href: help_page_path('user/usage_quotas.md'), target: '_blank', rel: 'noopener noreferrer' }
= s_('UsageQuota|Learn more about usage quotas') + '.'
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
......
......@@ -1890,7 +1890,7 @@
:tags: []
- :name: default
:worker_name:
:feature_category:
:feature_category: :not_owned
:has_external_dependencies:
:urgency:
:resource_boundary:
......@@ -2215,7 +2215,7 @@
:tags: []
- :name: mailers
:worker_name: ActionMailer::MailDeliveryJob
:feature_category: :issue_tracking
:feature_category: :not_owned
:has_external_dependencies:
:urgency: low
:resource_boundary:
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment