Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-10-13 15:08:53 +00:00
parent 6e91fbf774
commit 16515bdfcb
174 changed files with 2308 additions and 349 deletions

View file

@ -172,7 +172,7 @@ gem 'diffy', '~> 3.3'
gem 'diff_match_patch', '~> 0.1.0'
# Application server
gem 'rack', '~> 2.0.9'
gem 'rack', '~> 2.1.4'
# https://github.com/sharpstone/rack-timeout/blob/master/README.md#rails-apps-manually
gem 'rack-timeout', '~> 0.5.1', require: 'rack/timeout/base'

View file

@ -852,7 +852,7 @@ GEM
public_suffix (4.0.3)
pyu-ruby-sasl (0.0.3.3)
raabro (1.1.6)
rack (2.0.9)
rack (2.1.4)
rack-accept (0.4.5)
rack (>= 0.4)
rack-attack (6.3.0)
@ -1423,7 +1423,7 @@ DEPENDENCIES
prometheus-client-mmap (~> 0.12.0)
pry-byebug (~> 3.9.0)
pry-rails (~> 0.3.9)
rack (~> 2.0.9)
rack (~> 2.1.4)
rack-attack (~> 6.3.0)
rack-cors (~> 1.0.6)
rack-oauth2 (~> 1.9.3)

View file

@ -0,0 +1,121 @@
<script>
import { GlFormInput, GlFormSelect } from '@gitlab/ui';
import { __ } from '~/locale';
import { PERCENT_ROLLOUT_GROUP_ID } from '../../constants';
import ParameterFormGroup from './parameter_form_group.vue';
export default {
components: {
GlFormInput,
GlFormSelect,
ParameterFormGroup,
},
props: {
strategy: {
required: true,
type: Object,
},
},
i18n: {
percentageDescription: __('Enter a whole number between 0 and 100'),
percentageInvalid: __('Percent rollout must be a whole number between 0 and 100'),
percentageLabel: __('Percentage'),
stickinessDescription: __('Consistency guarantee method'),
stickinessLabel: __('Based on'),
},
stickinessOptions: [
{
value: 'DEFAULT',
text: __('Available ID'),
},
{
value: 'USERID',
text: __('User ID'),
},
{
value: 'SESSIONID',
text: __('Session ID'),
},
{
value: 'RANDOM',
text: __('Random'),
},
],
computed: {
isValid() {
const percentageNum = Number(this.percentage);
return Number.isInteger(percentageNum) && percentageNum >= 0 && percentageNum <= 100;
},
percentage() {
return this.strategy?.parameters?.rollout ?? '100';
},
stickiness() {
return this.strategy?.parameters?.stickiness ?? this.$options.stickinessOptions[0].value;
},
},
methods: {
onPercentageChange(value) {
this.$emit('change', {
parameters: {
groupId: PERCENT_ROLLOUT_GROUP_ID,
rollout: value,
stickiness: this.stickiness,
},
});
},
onStickinessChange(value) {
this.$emit('change', {
parameters: {
groupId: PERCENT_ROLLOUT_GROUP_ID,
rollout: this.percentage,
stickiness: value,
},
});
},
},
};
</script>
<template>
<div class="gl-display-flex">
<div class="gl-mr-7" data-testid="strategy-flexible-rollout-percentage">
<parameter-form-group
:label="$options.i18n.percentageLabel"
:description="isValid ? $options.i18n.percentageDescription : ''"
:invalid-feedback="$options.i18n.percentageInvalid"
:state="isValid"
>
<template #default="{ inputId }">
<div class="gl-display-flex gl-align-items-center">
<gl-form-input
:id="inputId"
:value="percentage"
:state="isValid"
class="rollout-percentage gl-text-right gl-w-9"
type="number"
min="0"
max="100"
@input="onPercentageChange"
/>
<span class="ml-1">%</span>
</div>
</template>
</parameter-form-group>
</div>
<div class="gl-mr-7" data-testid="strategy-flexible-rollout-stickiness">
<parameter-form-group
:label="$options.i18n.stickinessLabel"
:description="$options.i18n.stickinessDescription"
>
<template #default="{ inputId }">
<gl-form-select
:id="inputId"
:value="stickiness"
:options="$options.stickinessOptions"
@change="onStickinessChange"
/>
</template>
</parameter-form-group>
</div>
</div>
</template>

View file

@ -49,7 +49,7 @@ export default {
:state="hasUserLists"
:invalid-feedback="$options.translations.rolloutUserListNoListError"
:label="$options.translations.rolloutUserListLabel"
:description="$options.translations.rolloutUserListDescription"
:description="hasUserLists ? $options.translations.rolloutUserListDescription : ''"
>
<template #default="{ inputId }">
<gl-form-select

View file

@ -15,7 +15,7 @@ export default {
type: Object,
},
},
translations: {
i18n: {
rolloutPercentageDescription: __('Enter a whole number between 0 and 100'),
rolloutPercentageInvalid: s__(
'FeatureFlags|Percent rollout must be a whole number between 0 and 100',
@ -24,10 +24,11 @@ export default {
},
computed: {
isValid() {
return Number(this.percentage) >= 0 && Number(this.percentage) <= 100;
const percentageNum = Number(this.percentage);
return Number.isInteger(percentageNum) && percentageNum >= 0 && percentageNum <= 100;
},
percentage() {
return this.strategy?.parameters?.percentage ?? '';
return this.strategy?.parameters?.percentage ?? '100';
},
},
methods: {
@ -44,9 +45,9 @@ export default {
</script>
<template>
<parameter-form-group
:label="$options.translations.rolloutPercentageLabel"
:description="$options.translations.rolloutPercentageDescription"
:invalid-feedback="$options.translations.rolloutPercentageInvalid"
:label="$options.i18n.rolloutPercentageLabel"
:description="isValid ? $options.i18n.rolloutPercentageDescription : ''"
:invalid-feedback="$options.i18n.rolloutPercentageInvalid"
:state="isValid"
>
<template #default="{ inputId }">

View file

@ -1,15 +1,20 @@
<script>
import Vue from 'vue';
import { isNumber } from 'lodash';
import { GlButton, GlFormSelect, GlFormGroup, GlIcon, GlLink, GlToken } from '@gitlab/ui';
import { GlAlert, GlButton, GlFormSelect, GlFormGroup, GlIcon, GlLink, GlToken } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import { EMPTY_PARAMETERS, STRATEGY_SELECTIONS } from '../constants';
import {
EMPTY_PARAMETERS,
STRATEGY_SELECTIONS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
} from '../constants';
import NewEnvironmentsDropdown from './new_environments_dropdown.vue';
import StrategyParameters from './strategy_parameters.vue';
export default {
components: {
GlAlert,
GlButton,
GlFormGroup,
GlFormSelect,
@ -51,13 +56,13 @@ export default {
i18n: {
allEnvironments: __('All environments'),
environmentsLabel: __('Environments'),
rolloutUserListLabel: s__('FeatureFlag|List'),
rolloutUserListDescription: s__('FeatureFlag|Select a user list'),
rolloutUserListNoListError: s__('FeatureFlag|There are no configured user lists'),
strategyTypeDescription: __('Select strategy activation method.'),
strategyTypeDescription: __('Select strategy activation method'),
strategyTypeLabel: s__('FeatureFlag|Type'),
environmentsSelectDescription: s__(
'FeatureFlag|Select the environment scope for this feature flag.',
'FeatureFlag|Select the environment scope for this feature flag',
),
considerFlexibleRollout: s__(
'FeatureFlags|Consider using the more flexible "Percent rollout" strategy instead.',
),
},
@ -85,6 +90,9 @@ export default {
filteredEnvironments() {
return this.environments.filter(e => !e.shouldBeDestroyed);
},
isPercentUserRollout() {
return this.formStrategy.name === ROLLOUT_STRATEGY_PERCENT_ROLLOUT;
},
},
methods: {
addEnvironment(environment) {
@ -121,73 +129,84 @@ export default {
};
</script>
<template>
<div class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-100 gl-py-6">
<div class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row flex-md-wrap">
<div class="mr-5">
<gl-form-group :label="$options.i18n.strategyTypeLabel" :label-for="strategyTypeId">
<p class="gl-display-inline-block ">{{ $options.i18n.strategyTypeDescription }}</p>
<gl-link :href="strategyTypeDocsPagePath" target="_blank">
<gl-icon name="question" />
</gl-link>
<gl-form-select
:id="strategyTypeId"
:value="formStrategy.name"
:options="$options.strategies"
@change="onStrategyTypeChange"
<div>
<gl-alert v-if="isPercentUserRollout" variant="tip" :dismissible="false">
{{ $options.i18n.considerFlexibleRollout }}
</gl-alert>
<div class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-100 gl-py-6">
<div class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row flex-md-wrap">
<div class="mr-5">
<gl-form-group :label="$options.i18n.strategyTypeLabel" :label-for="strategyTypeId">
<template #description>
{{ $options.i18n.strategyTypeDescription }}
<gl-link :href="strategyTypeDocsPagePath" target="_blank">
<gl-icon name="question" />
</gl-link>
</template>
<gl-form-select
:id="strategyTypeId"
:value="formStrategy.name"
:options="$options.strategies"
@change="onStrategyTypeChange"
/>
</gl-form-group>
</div>
<div data-testid="strategy">
<strategy-parameters
:strategy="strategy"
:user-lists="userLists"
@change="onStrategyChange"
/>
</gl-form-group>
</div>
</div>
<div data-testid="strategy">
<strategy-parameters
:strategy="strategy"
:user-lists="userLists"
@change="onStrategyChange"
/>
</div>
<div
class="align-self-end align-self-md-stretch order-first offset-md-0 order-md-0 gl-ml-auto"
>
<gl-button
data-testid="delete-strategy-button"
variant="danger"
icon="remove"
@click="$emit('delete')"
/>
</div>
</div>
<label class="gl-display-block" :for="environmentsDropdownId">{{
$options.i18n.environmentsLabel
}}</label>
<p class="gl-display-inline-block">{{ $options.i18n.environmentsSelectDescription }}</p>
<gl-link :href="environmentsScopeDocsPath" target="_blank">
<gl-icon name="question" />
</gl-link>
<div class="gl-display-flex gl-flex-direction-column">
<div
class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row align-items-start gl-md-align-items-center"
>
<new-environments-dropdown
:id="environmentsDropdownId"
:endpoint="endpoint"
class="gl-mr-3"
@add="addEnvironment"
/>
<span v-if="appliesToAllEnvironments" class="text-secondary gl-mt-3 mt-md-0 ml-md-3">
{{ $options.i18n.allEnvironments }}
</span>
<div v-else class="gl-display-flex gl-align-items-center">
<gl-token
v-for="environment in filteredEnvironments"
:key="environment.id"
class="gl-mt-3 gl-mr-3 mt-md-0 mr-md-0 ml-md-2 rounded-pill"
@close="removeScope(environment)"
>
{{ environment.environmentScope }}
</gl-token>
<div
class="align-self-end align-self-md-stretch order-first offset-md-0 order-md-0 gl-ml-auto"
>
<gl-button
data-testid="delete-strategy-button"
variant="danger"
icon="remove"
@click="$emit('delete')"
/>
</div>
</div>
<label class="gl-display-block" :for="environmentsDropdownId">{{
$options.i18n.environmentsLabel
}}</label>
<div class="gl-display-flex gl-flex-direction-column">
<div
class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row align-items-start gl-md-align-items-center"
>
<new-environments-dropdown
:id="environmentsDropdownId"
:endpoint="endpoint"
class="gl-mr-3"
@add="addEnvironment"
/>
<span v-if="appliesToAllEnvironments" class="text-secondary gl-mt-3 mt-md-0 ml-md-3">
{{ $options.i18n.allEnvironments }}
</span>
<div v-else class="gl-display-flex gl-align-items-center">
<gl-token
v-for="environment in filteredEnvironments"
:key="environment.id"
class="gl-mt-3 gl-mr-3 mt-md-0 mr-md-0 ml-md-2 rounded-pill"
@close="removeScope(environment)"
>
{{ environment.environmentScope }}
</gl-token>
</div>
</div>
</div>
<span class="gl-display-inline-block gl-py-3">
{{ $options.i18n.environmentsSelectDescription }}
</span>
<gl-link :href="environmentsScopeDocsPath" target="_blank">
<gl-icon name="question" />
</gl-link>
</div>
</div>
</template>

View file

@ -1,18 +1,21 @@
<script>
import {
ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
ROLLOUT_STRATEGY_USER_ID,
ROLLOUT_STRATEGY_GITLAB_USER_LIST,
} from '../constants';
import Default from './strategies/default.vue';
import FlexibleRollout from './strategies/flexible_rollout.vue';
import PercentRollout from './strategies/percent_rollout.vue';
import UsersWithId from './strategies/users_with_id.vue';
import GitlabUserList from './strategies/gitlab_user_list.vue';
const STRATEGIES = Object.freeze({
[ROLLOUT_STRATEGY_ALL_USERS]: Default,
[ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT]: FlexibleRollout,
[ROLLOUT_STRATEGY_PERCENT_ROLLOUT]: PercentRollout,
[ROLLOUT_STRATEGY_USER_ID]: UsersWithId,
[ROLLOUT_STRATEGY_GITLAB_USER_LIST]: GitlabUserList,

View file

@ -3,6 +3,7 @@ import { s__ } from '~/locale';
export const ROLLOUT_STRATEGY_ALL_USERS = 'default';
export const ROLLOUT_STRATEGY_PERCENT_ROLLOUT = 'gradualRolloutUserId';
export const ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT = 'flexibleRollout';
export const ROLLOUT_STRATEGY_USER_ID = 'userWithId';
export const ROLLOUT_STRATEGY_GITLAB_USER_LIST = 'gitlabUserList';
@ -34,6 +35,10 @@ export const STRATEGY_SELECTIONS = [
value: ROLLOUT_STRATEGY_ALL_USERS,
text: s__('FeatureFlags|All users'),
},
{
value: ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
text: s__('FeatureFlags|Percent rollout'),
},
{
value: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
text: s__('FeatureFlags|Percent of users'),

View file

@ -2,6 +2,7 @@ import { s__, n__, sprintf } from '~/locale';
import {
ALL_ENVIRONMENTS_NAME,
ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
ROLLOUT_STRATEGY_USER_ID,
ROLLOUT_STRATEGY_GITLAB_USER_LIST,
@ -12,6 +13,23 @@ const badgeTextByType = {
name: s__('FeatureFlags|All Users'),
parameters: null,
},
[ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT]: {
name: s__('FeatureFlags|Percent rollout'),
parameters: ({ parameters: { rollout, stickiness } }) => {
switch (stickiness) {
case 'USERID':
return sprintf(s__('FeatureFlags|%{percent} by user ID'), { percent: `${rollout}%` });
case 'SESSIONID':
return sprintf(s__('FeatureFlags|%{percent} by session ID'), { percent: `${rollout}%` });
case 'RANDOM':
return sprintf(s__('FeatureFlags|%{percent} randomly'), { percent: `${rollout}%` });
default:
return sprintf(s__('FeatureFlags|%{percent} by available ID'), {
percent: `${rollout}%`,
});
}
},
},
[ROLLOUT_STRATEGY_PERCENT_ROLLOUT]: {
name: s__('FeatureFlags|Percent of users'),
parameters: ({ parameters: { percentage } }) => `${percentage}%`,

View file

@ -1,17 +1,17 @@
<script>
import { GlIcon } from '@gitlab/ui';
import projectFeatureToggle from '~/vue_shared/components/toggle_button.vue';
import { featureAccessLevelNone } from '../constants';
export default {
components: {
GlIcon,
projectFeatureToggle,
},
model: {
prop: 'value',
event: 'change',
},
props: {
name: {
type: String,
@ -34,7 +34,6 @@ export default {
default: false,
},
},
computed: {
featureEnabled() {
return this.value !== 0;
@ -51,7 +50,6 @@ export default {
return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2;
},
},
methods: {
toggleFeature(featureEnabled) {
if (featureEnabled === false || this.options.length < 1) {
@ -96,7 +94,11 @@ export default {
{{ optionName }}
</option>
</select>
<i aria-hidden="true" class="fa fa-chevron-down"> </i>
<gl-icon
name="chevron-down"
aria-hidden="true"
class="gl-absolute gl-top-3 gl-right-3 gl-text-gray-500"
/>
</div>
</div>
</template>

View file

@ -1,5 +1,5 @@
<script>
import { GlSprintf, GlLink, GlFormCheckbox } from '@gitlab/ui';
import { GlIcon, GlSprintf, GlLink, GlFormCheckbox } from '@gitlab/ui';
import settingsMixin from 'ee_else_ce/pages/projects/shared/permissions/mixins/settings_pannel_mixin';
import { s__ } from '~/locale';
@ -22,6 +22,7 @@ export default {
projectFeatureSetting,
projectFeatureToggle,
projectSettingRow,
GlIcon,
GlSprintf,
GlLink,
GlFormCheckbox,
@ -325,7 +326,12 @@ export default {
>{{ s__('ProjectSettings|Public') }}</option
>
</select>
<i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i>
<gl-icon
name="chevron-down"
aria-hidden="true"
data-hidden="true"
class="gl-absolute gl-top-3 gl-right-3 gl-text-gray-500"
/>
</div>
</div>
<span class="form-text text-muted">{{ visibilityLevelDescription }}</span>
@ -540,7 +546,12 @@ export default {
>{{ featureAccessLevelEveryone[1] }}</option
>
</select>
<i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i>
<gl-icon
name="chevron-down"
aria-hidden="true"
data-hidden="true"
class="gl-absolute gl-top-3 gl-right-3 gl-text-gray-500"
/>
</div>
</div>
</project-setting-row>

View file

@ -1,7 +1,6 @@
<script>
import $ from 'jquery';
import { difference, union } from 'lodash';
import { mapState, mapActions } from 'vuex';
import flash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@ -26,47 +25,49 @@ export default {
'projectIssuesPath',
'projectPath',
],
data: () => ({
labelsSelectInProgress: false,
}),
computed: {
...mapState(['selectedLabels']),
},
mounted() {
this.setInitialState({
data() {
return {
isLabelsSelectInProgress: false,
selectedLabels: this.initiallySelectedLabels,
});
};
},
methods: {
...mapActions(['setInitialState', 'replaceSelectedLabels']),
handleDropdownClose() {
$(this.$el).trigger('hidden.gl.dropdown');
},
handleUpdateSelectedLabels(labels) {
handleUpdateSelectedLabels(dropdownLabels) {
const currentLabelIds = this.selectedLabels.map(label => label.id);
const userAddedLabelIds = labels.filter(label => label.set).map(label => label.id);
const userRemovedLabelIds = labels.filter(label => !label.set).map(label => label.id);
const userAddedLabelIds = dropdownLabels.filter(label => label.set).map(label => label.id);
const userRemovedLabelIds = dropdownLabels.filter(label => !label.set).map(label => label.id);
const issuableLabels = difference(
union(currentLabelIds, userAddedLabelIds),
userRemovedLabelIds,
);
const labelIds = difference(union(currentLabelIds, userAddedLabelIds), userRemovedLabelIds);
this.labelsSelectInProgress = true;
this.updateSelectedLabels(labelIds);
},
handleLabelRemove(labelId) {
const currentLabelIds = this.selectedLabels.map(label => label.id);
const labelIds = difference(currentLabelIds, [labelId]);
this.updateSelectedLabels(labelIds);
},
updateSelectedLabels(labelIds) {
this.isLabelsSelectInProgress = true;
axios({
data: {
[this.issuableType]: {
label_ids: issuableLabels,
label_ids: labelIds,
},
},
method: 'put',
url: this.labelsUpdatePath,
})
.then(({ data }) => this.replaceSelectedLabels(data.labels))
.then(({ data }) => {
this.selectedLabels = data.labels;
})
.catch(() => flash(__('An error occurred while updating labels.')))
.finally(() => {
this.labelsSelectInProgress = false;
this.isLabelsSelectInProgress = false;
});
},
},
@ -76,6 +77,7 @@ export default {
<template>
<labels-select
class="block labels js-labels-block"
:allow-label-remove="true"
:allow-label-create="allowLabelCreate"
:allow-label-edit="allowLabelEdit"
:allow-multiselect="true"
@ -86,11 +88,12 @@ export default {
:labels-fetch-path="labelsFetchPath"
:labels-filter-base-path="projectIssuesPath"
:labels-manage-path="labelsManagePath"
:labels-select-in-progress="labelsSelectInProgress"
:labels-select-in-progress="isLabelsSelectInProgress"
:selected-labels="selectedLabels"
:variant="$options.sidebar"
data-qa-selector="labels_block"
@onDropdownClose="handleDropdownClose"
@onLabelRemove="handleLabelRemove"
@updateSelectedLabels="handleUpdateSelectedLabels"
>
{{ __('None') }}

View file

@ -1,7 +1,6 @@
import $ from 'jquery';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import SidebarLabels from './components/labels/sidebar_labels.vue';
@ -17,11 +16,9 @@ import createDefaultClient from '~/lib/graphql';
import { isInIssuePage, isInIncidentPage, parseBoolean } from '~/lib/utils/common_utils';
import createFlash from '~/flash';
import { __ } from '~/locale';
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
Vue.use(Translate);
Vue.use(VueApollo);
Vue.use(Vuex);
function getSidebarOptions(sidebarOptEl = document.querySelector('.js-sidebar-options')) {
return JSON.parse(sidebarOptEl.innerHTML);
@ -94,8 +91,6 @@ export function mountSidebarLabels() {
return false;
}
const labelsStore = new Vuex.Store(labelsSelectModule());
return new Vue({
el,
provide: {
@ -105,7 +100,6 @@ export function mountSidebarLabels() {
allowScopedLabels: parseBoolean(el.dataset.allowScopedLabels),
initiallySelectedLabels: JSON.parse(el.dataset.selectedLabels),
},
store: labelsStore,
render: createElement => createElement(SidebarLabels),
});
}

View file

@ -8,11 +8,13 @@ import {
import { generateBadges } from 'ee_else_ce/vue_shared/components/members/utils';
import { __ } from '~/locale';
import { AVATAR_SIZE } from '../constants';
import { glEmojiTag } from '~/emoji';
export default {
name: 'UserAvatar',
avatarSize: AVATAR_SIZE,
orphanedUserLabel: __('Orphaned member'),
safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
components: {
GlAvatarLink,
GlAvatarLabeled,
@ -38,6 +40,12 @@ export default {
badges() {
return generateBadges(this.member, this.isCurrentUser).filter(badge => badge.show);
},
statusEmoji() {
return this.user?.status?.emoji;
},
},
methods: {
glEmojiTag,
},
};
</script>
@ -60,6 +68,9 @@ export default {
:entity-id="user.id"
>
<template #meta>
<div v-if="statusEmoji" class="gl-p-1">
<span v-safe-html:[$options.safeHtmlConfig]="glEmojiTag(statusEmoji)"></span>
</div>
<div v-for="badge in badges" :key="badge.text" class="gl-p-1">
<gl-badge size="sm" :variant="badge.variant">
{{ badge.text }}

View file

@ -38,8 +38,8 @@ export const FIELDS = [
{
key: 'maxRole',
label: __('Max role'),
thClass: 'col-meta',
tdClass: 'col-meta',
thClass: 'col-max-role',
tdClass: 'col-max-role',
},
{
key: 'expiration',

View file

@ -1,6 +1,6 @@
<script>
import { mapState } from 'vuex';
import { GlTable } from '@gitlab/ui';
import { GlTable, GlBadge } from '@gitlab/ui';
import { FIELDS } from '../constants';
import initUserPopovers from '~/user_popovers';
import MemberAvatar from './member_avatar.vue';
@ -9,17 +9,20 @@ import CreatedAt from './created_at.vue';
import ExpiresAt from './expires_at.vue';
import MemberActionButtons from './member_action_buttons.vue';
import MembersTableCell from './members_table_cell.vue';
import RoleDropdown from './role_dropdown.vue';
export default {
name: 'MembersTable',
components: {
GlTable,
GlBadge,
MemberAvatar,
CreatedAt,
ExpiresAt,
MembersTableCell,
MemberSource,
MemberActionButtons,
RoleDropdown,
},
computed: {
...mapState(['members', 'tableFields']),
@ -77,6 +80,13 @@ export default {
<expires-at :date="expiresAt" />
</template>
<template #cell(maxRole)="{ item: member }">
<members-table-cell #default="{ permissions }" :member="member">
<role-dropdown v-if="permissions.canUpdate" :member="member" />
<gl-badge v-else>{{ member.accessLevel.stringValue }}</gl-badge>
</members-table-cell>
</template>
<template #cell(actions)="{ item: member }">
<members-table-cell #default="{ memberType, isCurrentUser, permissions }" :member="member">
<member-action-buttons

View file

@ -33,7 +33,7 @@ export default {
return MEMBER_TYPES.user;
},
isDirectMember() {
return this.member.source?.id === this.sourceId;
return this.isGroup || this.member.source?.id === this.sourceId;
},
isCurrentUser() {
return this.member.user?.id === this.currentUserId;
@ -44,6 +44,9 @@ export default {
canResend() {
return Boolean(this.member.invite?.canResend);
},
canUpdate() {
return !this.isCurrentUser && this.isDirectMember && this.member.canUpdate;
},
},
render() {
return this.$scopedSlots.default({
@ -53,6 +56,7 @@ export default {
permissions: {
canRemove: this.canRemove,
canResend: this.canResend,
canUpdate: this.canUpdate,
},
});
},

View file

@ -0,0 +1,49 @@
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
export default {
name: 'RoleDropdown',
components: {
GlDropdown,
GlDropdownItem,
},
props: {
member: {
type: Object,
required: true,
},
},
data() {
return {
isDesktop: false,
};
},
mounted() {
this.isDesktop = bp.isDesktop();
},
methods: {
handleSelect() {
// Vuex action will be called here to make API request and update `member.accessLevel`
},
},
};
</script>
<template>
<gl-dropdown
:right="!isDesktop"
:text="member.accessLevel.stringValue"
:header-text="__('Change permissions')"
>
<gl-dropdown-item
v-for="(value, name) in member.validRoles"
:key="value"
is-check-item
:is-checked="value === member.accessLevel.integerValue"
@click="handleSelect"
>
{{ name }}
</gl-dropdown-item>
</gl-dropdown>
</template>

View file

@ -8,8 +8,20 @@ export default {
components: {
GlLabel,
},
props: {
disableLabels: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapState(['selectedLabels', 'allowScopedLabels', 'labelsFilterBasePath']),
...mapState([
'selectedLabels',
'allowLabelRemove',
'allowScopedLabels',
'labelsFilterBasePath',
]),
},
methods: {
labelFilterUrl(label) {
@ -42,7 +54,10 @@ export default {
:background-color="label.color"
:target="labelFilterUrl(label)"
:scoped="scopedLabel(label)"
:show-close-button="allowLabelRemove"
:disabled="disableLabels"
tooltip-placement="top"
@close="$emit('onLabelRemove', label.id)"
/>
</template>
</div>

View file

@ -28,6 +28,11 @@ export default {
DropdownValueCollapsed,
},
props: {
allowLabelRemove: {
type: Boolean,
required: false,
default: false,
},
allowLabelEdit: {
type: Boolean,
required: true,
@ -130,6 +135,7 @@ export default {
mounted() {
this.setInitialState({
variant: this.variant,
allowLabelRemove: this.allowLabelRemove,
allowLabelEdit: this.allowLabelEdit,
allowLabelCreate: this.allowLabelCreate,
allowMultiselect: this.allowMultiselect,
@ -252,7 +258,10 @@ export default {
:allow-label-edit="allowLabelEdit"
:labels-select-in-progress="labelsSelectInProgress"
/>
<dropdown-value>
<dropdown-value
:disable-labels="labelsSelectInProgress"
@onLabelRemove="$emit('onLabelRemove', $event)"
>
<slot></slot>
</dropdown-value>
<dropdown-button v-show="dropdownButtonVisible" class="gl-mt-2" />

View file

@ -54,8 +54,5 @@ export const createLabel = ({ state, dispatch }, label) => {
});
};
export const replaceSelectedLabels = ({ commit }, selectedLabels) =>
commit(types.REPLACE_SELECTED_LABELS, selectedLabels);
export const updateSelectedLabels = ({ commit }, labels) =>
commit(types.UPDATE_SELECTED_LABELS, { labels });

View file

@ -15,7 +15,6 @@ export const RECEIVE_CREATE_LABEL_FAILURE = 'RECEIVE_CREATE_LABEL_FAILURE';
export const TOGGLE_DROPDOWN_BUTTON = 'TOGGLE_DROPDOWN_VISIBILITY';
export const TOGGLE_DROPDOWN_CONTENTS = 'TOGGLE_DROPDOWN_CONTENTS';
export const REPLACE_SELECTED_LABELS = 'REPLACE_SELECTED_LABELS';
export const UPDATE_SELECTED_LABELS = 'UPDATE_SELECTED_LABELS';
export const TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW = 'TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW';

View file

@ -57,10 +57,6 @@ export default {
state.labelCreateInProgress = false;
},
[types.REPLACE_SELECTED_LABELS](state, selectedLabels = []) {
state.selectedLabels = selectedLabels;
},
[types.UPDATE_SELECTED_LABELS](state, { labels }) {
// Find the label to update from all the labels
// and change `set` prop value to represent their current state.

View file

@ -15,6 +15,7 @@ export default () => ({
// UI Flags
variant: '',
allowLabelRemove: false,
allowLabelCreate: false,
allowLabelEdit: false,
allowScopedLabels: false,

View file

@ -216,6 +216,10 @@
width: px-to-rem(150px);
}
.col-max-role {
width: px-to-rem(175px);
}
.col-expiration {
width: px-to-rem(200px);
}

View file

@ -171,7 +171,7 @@ class Admin::UsersController < Admin::ApplicationController
# restore username to keep form action url.
user.username = params[:id]
format.html { render "edit" }
format.json { render json: [result[:message]], status: result[:status] }
format.json { render json: [result[:message]], status: :internal_server_error }
end
end
end

View file

@ -45,7 +45,7 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
if result[:status] == :success
head :ok
else
render json: { message: result[:message] }, status: result[:status]
render json: { message: result[:message] }, status: :internal_server_error
end
end

View file

@ -7,22 +7,22 @@ module Types
graphql_name 'DetailedStatus'
field :group, GraphQL::STRING_TYPE, null: false,
description: 'Group of the pipeline status'
description: 'Group of the status'
field :icon, GraphQL::STRING_TYPE, null: false,
description: 'Icon of the pipeline status'
description: 'Icon of the status'
field :favicon, GraphQL::STRING_TYPE, null: false,
description: 'Favicon of the pipeline status'
field :details_path, GraphQL::STRING_TYPE, null: false,
description: 'Path of the details for the pipeline status'
description: 'Favicon of the status'
field :details_path, GraphQL::STRING_TYPE, null: true,
description: 'Path of the details for the status'
field :has_details, GraphQL::BOOLEAN_TYPE, null: false,
description: 'Indicates if the pipeline status has further details',
description: 'Indicates if the status has further details',
method: :has_details?
field :label, GraphQL::STRING_TYPE, null: false,
description: 'Label of the pipeline status'
description: 'Label of the status'
field :text, GraphQL::STRING_TYPE, null: false,
description: 'Text of the pipeline status'
description: 'Text of the status'
field :tooltip, GraphQL::STRING_TYPE, null: false,
description: 'Tooltip associated with the pipeline status',
description: 'Tooltip associated with the status',
method: :status_tooltip
field :action, Types::Ci::StatusActionType, null: true,
description: 'Action information for the status. This includes method, button title, icon, path, and title',

View file

@ -12,6 +12,9 @@ module Types
description: 'Size of the group'
field :jobs, Ci::JobType.connection_type, null: true,
description: 'Jobs in group'
field :detailed_status, Types::Ci::DetailedStatusType, null: true,
description: 'Detailed status of the group',
resolve: -> (obj, _args, ctx) { obj.detailed_status(ctx[:current_user]) }
end
end
end

View file

@ -10,6 +10,9 @@ module Types
description: 'Name of the job'
field :needs, JobType.connection_type, null: true,
description: 'Builds that must complete before the jobs run'
field :detailed_status, Types::Ci::DetailedStatusType, null: true,
description: 'Detailed status of the job',
resolve: -> (obj, _args, ctx) { obj.detailed_status(ctx[:current_user]) }
end
end
end

View file

@ -10,6 +10,9 @@ module Types
description: 'Name of the stage'
field :groups, Ci::GroupType.connection_type, null: true,
description: 'Group of jobs for the stage'
field :detailed_status, Types::Ci::DetailedStatusType, null: true,
description: 'Detailed status of the stage',
resolve: -> (obj, _args, ctx) { obj.detailed_status(ctx[:current_user]) }
end
end
end

View file

@ -471,7 +471,8 @@ module ProjectsHelper
labels: :read_label,
issues: :read_issue,
project_members: :read_project_member,
wiki: :read_wiki
wiki: :read_wiki,
feature_flags: :read_feature_flag
}
end
@ -482,7 +483,8 @@ module ProjectsHelper
:read_environment,
:read_issue,
:read_sentry_issue,
:read_cluster
:read_cluster,
:read_feature_flag
].any? do |ability|
can?(current_user, ability, project)
end
@ -561,7 +563,11 @@ module ProjectsHelper
end
def sidebar_operations_link_path(project = @project)
metrics_project_environments_path(project) if can?(current_user, :read_environment, project)
if can?(current_user, :read_environment, project)
metrics_project_environments_path(project)
else
project_feature_flags_path(project)
end
end
def project_last_activity(project)
@ -754,6 +760,7 @@ module ProjectsHelper
logs
product_analytics
metrics_dashboard
feature_flags
tracings
]
end

View file

@ -0,0 +1,37 @@
# frozen_string_literal: true
module Ci
class DeletedObject < ApplicationRecord
extend Gitlab::Ci::Model
mount_uploader :file, DeletedObjectUploader
scope :ready_for_destruction, ->(limit) do
where('pick_up_at < ?', Time.current).limit(limit)
end
scope :lock_for_destruction, ->(limit) do
ready_for_destruction(limit)
.select(:id)
.order(:pick_up_at)
.lock('FOR UPDATE SKIP LOCKED')
end
def self.bulk_import(artifacts)
attributes = artifacts.each.with_object([]) do |artifact, accumulator|
record = artifact.to_deleted_object_attrs
accumulator << record if record[:store_dir] && record[:file]
end
self.insert_all(attributes) if attributes.any?
end
def delete_file_from_storage
file.remove!
true
rescue => exception
Gitlab::ErrorTracking.track_exception(exception)
false
end
end
end

View file

@ -290,6 +290,15 @@ module Ci
max_size&.megabytes.to_i
end
def to_deleted_object_attrs
{
file_store: file_store,
store_dir: file.store_dir.to_s,
file: file_identifier,
pick_up_at: expire_at || Time.current
}
end
private
def set_size

View file

@ -841,6 +841,25 @@ module Ci
end
end
def build_with_artifacts_in_self_and_descendants(name)
builds_in_self_and_descendants
.ordered_by_pipeline # find job in hierarchical order
.with_downloadable_artifacts
.find_by_name(name)
end
def builds_in_self_and_descendants
Ci::Build.latest.where(pipeline: self_and_descendants)
end
# Without using `unscoped`, caller scope is also included into the query.
# Using `unscoped` here will be redundant after Rails 6.1
def self_and_descendants
::Gitlab::Ci::PipelineObjectHierarchy
.new(self.class.unscoped.where(id: id), options: { same_project: true })
.base_and_descendants
end
def bridge_triggered?
source_bridge.present?
end

View file

@ -48,6 +48,7 @@ class CommitStatus < ApplicationRecord
scope :ordered_by_stage, -> { order(stage_idx: :asc) }
scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) }
scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
scope :ordered_by_pipeline, -> { order(pipeline_id: :asc) }
scope :before_stage, -> (index) { where('stage_idx < ?', index) }
scope :for_stage, -> (index) { where(stage_idx: index) }
scope :after_stage, -> (index) { where('stage_idx > ?', index) }

View file

@ -76,6 +76,7 @@ class Group < Namespace
validate :visibility_level_allowed_by_projects
validate :visibility_level_allowed_by_sub_groups
validate :visibility_level_allowed_by_parent
validate :two_factor_authentication_allowed
validates :variables, variable_duplicates: true
validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 }
@ -589,6 +590,16 @@ class Group < Namespace
errors.add(:visibility_level, "#{visibility} is not allowed since there are sub-groups with higher visibility.")
end
def two_factor_authentication_allowed
return unless has_parent?
return unless require_two_factor_authentication
ancestor_settings = ancestors.find_by(parent_id: nil).namespace_settings
return if ancestor_settings.allow_mfa_for_subgroups
errors.add(:require_two_factor_authentication, _('is forbidden by a top-level group'))
end
def members_from_self_and_ancestor_group_shares
group_group_link_table = GroupGroupLink.arel_table
group_member_table = GroupMember.arel_table

View file

@ -4,6 +4,7 @@ class NamespaceSetting < ApplicationRecord
belongs_to :namespace, inverse_of: :namespace_settings
validate :default_branch_name_content
validate :allow_mfa_for_group
NAMESPACE_SETTINGS_PARAMS = [:default_branch_name].freeze
@ -16,6 +17,12 @@ class NamespaceSetting < ApplicationRecord
errors.add(:default_branch_name, "can not be an empty string")
end
end
def allow_mfa_for_group
if namespace&.subgroup? && allow_mfa_for_subgroups == false
errors.add(:allow_mfa_for_subgroups, _('is not allowed since the group is not top-level group.'))
end
end
end
NamespaceSetting.prepend_if_ee('EE::NamespaceSetting')

View file

@ -951,7 +951,7 @@ class Project < ApplicationRecord
latest_pipeline = ci_pipelines.latest_successful_for_ref(ref)
return unless latest_pipeline
latest_pipeline.builds.latest.with_downloadable_artifacts.find_by(name: job_name)
latest_pipeline.build_with_artifacts_in_self_and_descendants(job_name)
end
def latest_successful_build_for_sha(job_name, sha)
@ -960,7 +960,7 @@ class Project < ApplicationRecord
latest_pipeline = ci_pipelines.latest_successful_for_sha(sha)
return unless latest_pipeline
latest_pipeline.builds.latest.with_downloadable_artifacts.find_by(name: job_name)
latest_pipeline.build_with_artifacts_in_self_and_descendants(job_name)
end
def latest_successful_build_for_ref!(job_name, ref = default_branch)

View file

@ -27,7 +27,7 @@ class ConfluenceService < Service
end
def description
s_('ConfluenceService|Connect a Confluence Cloud Workspace to your GitLab project')
s_('ConfluenceService|Connect a Confluence Cloud Workspace to GitLab')
end
def detailed_description

View file

@ -16,7 +16,7 @@ class PackagistService < Service
end
def description
'Update your project on Packagist, the main Composer repository'
s_('Integrations|Update your projects on Packagist, the main Composer repository')
end
def self.to_param

View file

@ -0,0 +1,62 @@
# frozen_string_literal: true
module Ci
class DeleteObjectsService
TransactionInProgressError = Class.new(StandardError)
TRANSACTION_MESSAGE = "can't perform network calls inside a database transaction"
BATCH_SIZE = 100
RETRY_IN = 10.minutes
def execute
objects = load_next_batch
destroy_everything(objects)
end
def remaining_batches_count(max_batch_count:)
Ci::DeletedObject
.ready_for_destruction(max_batch_count * BATCH_SIZE)
.size
.fdiv(BATCH_SIZE)
.ceil
end
private
# rubocop: disable CodeReuse/ActiveRecord
def load_next_batch
# `find_by_sql` performs a write in this case and we need to wrap it in
# a transaction to stick to the primary database.
Ci::DeletedObject.transaction do
Ci::DeletedObject.find_by_sql([
next_batch_sql, new_pick_up_at: RETRY_IN.from_now
])
end
end
# rubocop: enable CodeReuse/ActiveRecord
def next_batch_sql
<<~SQL.squish
UPDATE "ci_deleted_objects"
SET "pick_up_at" = :new_pick_up_at
WHERE "ci_deleted_objects"."id" IN (#{locked_object_ids_sql})
RETURNING *
SQL
end
def locked_object_ids_sql
Ci::DeletedObject.lock_for_destruction(BATCH_SIZE).to_sql
end
def destroy_everything(objects)
raise TransactionInProgressError, TRANSACTION_MESSAGE if transaction_open?
return unless objects.any?
deleted = objects.select(&:delete_file_from_storage)
Ci::DeletedObject.id_in(deleted.map(&:id)).delete_all
end
def transaction_open?
Ci::DeletedObject.connection.transaction_open?
end
end
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
class DeletedObjectUploader < GitlabUploader
include ObjectStorage::Concern
storage_options Gitlab.config.artifacts
def store_dir
model.store_dir
end
end

View file

@ -25,8 +25,8 @@
= link_to _('Remove user & report'), admin_abuse_report_path(abuse_report, remove_user: true),
data: { confirm: _("USER %{user} WILL BE REMOVED! Are you sure?") % { user: user.name } }, remote: true, method: :delete, class: "gl-button btn btn-sm btn-block btn-danger js-remove-tr"
- if user && !user.blocked?
= link_to _('Block user'), block_admin_user_path(user), data: {confirm: _('USER WILL BE BLOCKED! Are you sure?')}, method: :put, class: "btn btn-sm btn-block"
= link_to _('Block user'), block_admin_user_path(user), data: {confirm: _('USER WILL BE BLOCKED! Are you sure?')}, method: :put, class: "gl-button btn btn-sm btn-block"
- else
.btn.btn-sm.disabled.btn-block
= _('Already blocked')
= link_to _('Remove report'), [:admin, abuse_report], remote: true, method: :delete, class: "btn btn-sm btn-block btn-close js-remove-tr"
= link_to _('Remove report'), [:admin, abuse_report], remote: true, method: :delete, class: "gl-button btn btn-sm btn-block btn-close js-remove-tr"

View file

@ -40,5 +40,5 @@
= render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes
.form-actions
= f.submit 'Submit', class: "btn btn-success wide"
= link_to "Cancel", admin_applications_path, class: "btn btn-cancel"
= f.submit 'Submit', class: "gl-button btn btn-success wide"
= link_to "Cancel", admin_applications_path, class: "gl-button btn btn-cancel"

View file

@ -4,7 +4,7 @@
%p.light
System OAuth applications don't belong to any user and can only be managed by admins
%hr
%p= link_to 'New application', new_admin_application_path, class: 'btn btn-success'
%p= link_to 'New application', new_admin_application_path, class: 'gl-button btn btn-success'
%table.table
%thead
%tr
@ -23,6 +23,6 @@
%td= @application_counts[application.id].to_i
%td= application.trusted? ? 'Y': 'N'
%td= application.confidential? ? 'Y': 'N'
%td= link_to 'Edit', edit_admin_application_path(application), class: 'btn btn-link'
%td= link_to 'Edit', edit_admin_application_path(application), class: 'gl-button btn btn-link'
%td= render 'delete_form', application: application
= paginate @applications, theme: 'gitlab'

View file

@ -13,7 +13,7 @@
.input-group
%input.label.label-monospace.monospace{ id: "application_id", type: "text", autocomplete: 'off', value: @application.uid, readonly: true }
.input-group-append
= clipboard_button(target: '#application_id', title: _("Copy ID"), class: "btn btn btn-default")
= clipboard_button(target: '#application_id', title: _("Copy ID"), class: "gl-button btn btn-default")
%tr
%td
= _('Secret')
@ -22,7 +22,7 @@
.input-group
%input.label.label-monospace.monospace{ id: "secret", type: "text", autocomplete: 'off', value: @application.secret, readonly: true }
.input-group-append
= clipboard_button(target: '#secret', title: _("Copy secret"), class: "btn btn btn-default")
= clipboard_button(target: '#secret', title: _("Copy secret"), class: "gl-button btn btn-default")
%tr
%td
= _('Callback URL')
@ -45,5 +45,5 @@
= render "shared/tokens/scopes_list", token: @application
.form-actions
= link_to 'Edit', edit_admin_application_path(@application), class: 'btn btn-primary wide float-left'
= link_to 'Edit', edit_admin_application_path(@application), class: 'gl-button btn btn-primary wide float-left'
= render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger gl-ml-3'

View file

@ -29,10 +29,10 @@
.gl-alert-body
= render 'shared/group_tips'
.form-actions
= f.submit _('Create group'), class: "btn btn-success"
= link_to _('Cancel'), admin_groups_path, class: "btn btn-cancel"
= f.submit _('Create group'), class: "gl-button btn btn-success"
= link_to _('Cancel'), admin_groups_path, class: "gl-button btn btn-cancel"
- else
.form-actions
= f.submit _('Save changes'), class: "btn btn-success", data: { qa_selector: 'save_changes_button' }
= link_to _('Cancel'), admin_group_path(@group), class: "btn btn-cancel"
= f.submit _('Save changes'), class: "gl-button btn btn-success", data: { qa_selector: 'save_changes_button' }
= link_to _('Cancel'), admin_group_path(@group), class: "gl-button btn btn-cancel"

View file

@ -10,7 +10,7 @@
= search_field_tag :name, project_name, class: "form-control search-text-input js-search-input", autofocus: true, spellcheck: false, placeholder: 'Search by name', data: { qa_selector: 'group_search_field' }
= sprite_icon('search', css_class: 'search-icon')
= render "shared/groups/dropdown", options_hash: admin_groups_sort_options_hash
= link_to new_admin_group_path, class: "btn btn-success" do
= link_to new_admin_group_path, class: "gl-button btn btn-success" do
= _('New group')
%ul.content-list
= render @groups

View file

@ -115,7 +115,7 @@
.gl-mt-3
= select_tag :access_level, options_for_select(@group.access_level_roles), class: "project-access-select select2"
%hr
= button_tag _('Add users to group'), class: "btn btn-success"
= button_tag _('Add users to group'), class: "gl-button btn btn-success"
= render 'shared/members/requests', membership_source: @group, requesters: @requesters, force_mobile_view: true
.card

View file

@ -9,7 +9,7 @@
%code#health-check-token= Gitlab::CurrentSettings.health_check_access_token
.gl-mt-3
= button_to _("Reset health check access token"), reset_health_check_token_admin_application_settings_path,
method: :put, class: 'btn btn-default',
method: :put, class: 'gl-button btn btn-default',
data: { confirm: _('Are you sure you want to reset the health check token?') }
%p.light
#{ _('Health information can be retrieved from the following endpoints. More information is available') }

View file

@ -14,5 +14,5 @@
= f.text_field :extern_uid, class: 'form-control', required: true
.form-actions
= f.submit _('Save changes'), class: "btn btn-success"
= f.submit _('Save changes'), class: "gl-button btn btn-success"

View file

@ -4,9 +4,9 @@
%td
= identity.extern_uid
%td
= link_to edit_admin_user_identity_path(@user, identity), class: 'btn btn-sm btn-grouped' do
= link_to edit_admin_user_identity_path(@user, identity), class: 'gl-button btn btn-sm btn-grouped' do
= _("Edit")
= link_to [:admin, @user, identity], method: :delete,
class: 'btn btn-sm btn-danger',
class: 'gl-button btn btn-sm btn-danger',
data: { confirm: _("Are you sure you want to remove this identity?") } do
= _('Delete')

View file

@ -3,7 +3,7 @@
- page_title _("Identities"), @user.name, _("Users")
= render 'admin/users/head'
= link_to _('New identity'), new_admin_user_identity_path, class: 'float-right btn btn-success'
= link_to _('New identity'), new_admin_user_identity_path, class: 'float-right gl-button btn btn-success'
- if @identities.present?
.table-holder
%table.table

View file

@ -30,8 +30,8 @@
= dropdown_content
= dropdown_loading
= render 'shared/projects/dropdown'
= link_to new_project_path, class: 'btn btn-success' do
= link_to new_project_path, class: 'gl-button btn btn-success' do
New Project
= button_tag "Search", class: "btn btn-primary btn-search hide"
= button_tag "Search", class: "gl-button btn btn-primary btn-search hide"
= render 'projects'

View file

@ -149,7 +149,7 @@
.form-group.row
.offset-sm-3.col-sm-9
= f.submit _('Transfer'), class: 'btn btn-primary'
= f.submit _('Transfer'), class: 'gl-button btn btn-primary'
.card.repository-check
.card-header
@ -169,7 +169,7 @@
= link_to sprite_icon('question-o'), help_page_path('administration/repository_checks')
.form-group
= f.submit _('Trigger repository check'), class: 'btn btn-primary'
= f.submit _('Trigger repository check'), class: 'gl-button btn btn-primary'
.col-md-6
- if @group

View file

@ -65,15 +65,15 @@
.table-section.table-button-footer.section-10
.btn-group.table-action-buttons
.btn-group
= link_to admin_runner_path(runner), class: 'btn btn-default has-tooltip', title: _('Edit'), ref: 'tooltip', aria: { label: _('Edit') }, data: { placement: 'top', container: 'body'} do
= link_to admin_runner_path(runner), class: 'gl-button btn btn-default has-tooltip', title: _('Edit'), ref: 'tooltip', aria: { label: _('Edit') }, data: { placement: 'top', container: 'body'} do
= sprite_icon('pencil')
.btn-group
- if runner.active?
= link_to [:pause, :admin, runner], method: :get, class: 'btn btn-default btn-svg has-tooltip', title: _('Pause'), ref: 'tooltip', aria: { label: _('Pause') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do
= link_to [:pause, :admin, runner], method: :get, class: 'gl-button btn btn-default btn-svg has-tooltip', title: _('Pause'), ref: 'tooltip', aria: { label: _('Pause') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do
= sprite_icon('pause')
- else
= link_to [:resume, :admin, runner], method: :get, class: 'btn btn-default btn-svg has-tooltip gl-px-3', title: _('Resume'), ref: 'tooltip', aria: { label: _('Resume') }, data: { placement: 'top', container: 'body'} do
= link_to [:resume, :admin, runner], method: :get, class: 'gl-button btn btn-default btn-svg has-tooltip gl-px-3', title: _('Resume'), ref: 'tooltip', aria: { label: _('Resume') }, data: { placement: 'top', container: 'body'} do
= sprite_icon('play')
.btn-group
= link_to [:admin, runner], method: :delete, class: 'btn btn-danger has-tooltip', title: _('Remove'), ref: 'tooltip', aria: { label: _('Remove') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do
= link_to [:admin, runner], method: :delete, class: 'gl-button btn btn-danger has-tooltip', title: _('Remove'), ref: 'tooltip', aria: { label: _('Remove') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do
= sprite_icon('close')

View file

@ -48,7 +48,7 @@
.filtered-search-box
= dropdown_tag(_('Recent searches'),
options: { wrapper_class: 'filtered-search-history-dropdown-wrapper',
toggle_class: 'btn filtered-search-history-dropdown-toggle-button',
toggle_class: 'gl-button btn filtered-search-history-dropdown-toggle-button',
dropdown_class: 'filtered-search-history-dropdown',
content_class: 'filtered-search-history-dropdown-content' }) do
.js-filtered-search-history-dropdown{ data: { full_path: admin_runners_path } }
@ -60,7 +60,7 @@
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item{ data: {hint: "#{'{{hint}}'}", tag: "#{'{{tag}}'}", action: "#{'{{hint === \'search\' ? \'submit\' : \'\' }}'}" } }
= button_tag class: %w[btn btn-link] do
= button_tag class: %w[gl-button btn btn-link] do
-# Encapsulate static class name `{{icon}}` inside #{} to bypass
-# haml lint's ClassAttributeWithStaticValue
%svg
@ -78,21 +78,21 @@
%ul{ data: { dropdown: true } }
- Ci::Runner::AVAILABLE_STATUSES.each do |status|
%li.filter-dropdown-item{ data: { value: status } }
= button_tag class: %w[btn btn-link] do
= button_tag class: %w[gl-button btn btn-link] do
= status.titleize
#js-dropdown-admin-runner-type.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
- Ci::Runner::AVAILABLE_TYPES.each do |runner_type|
%li.filter-dropdown-item{ data: { value: runner_type } }
= button_tag class: %w[btn btn-link] do
= button_tag class: %w[gl-button btn btn-link] do
= runner_type.titleize
#js-dropdown-admin-runner-type.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
- Ci::Runner::AVAILABLE_TYPES.each do |runner_type|
%li.filter-dropdown-item{ data: { value: runner_type } }
= button_tag class: %w[btn btn-link] do
= button_tag class: %w[gl-button btn btn-link] do
= runner_type.titleize
#js-dropdown-runner-tag.filtered-search-input-dropdown-menu.dropdown-menu

View file

@ -49,7 +49,7 @@
= project.full_name
%td
.float-right
= link_to 'Disable', admin_namespace_project_runner_project_path(project.namespace, project, runner_project), method: :delete, class: 'btn btn-danger btn-sm'
= link_to 'Disable', admin_namespace_project_runner_project_path(project.namespace, project, runner_project), method: :delete, class: 'gl-button btn btn-danger btn-sm'
%table.table.unassigned-projects
%thead
@ -73,7 +73,7 @@
.float-right
= form_for project.runner_projects.new, url: admin_namespace_project_runner_projects_path(project.namespace, project), method: :post do |f|
= f.hidden_field :runner_id, value: @runner.id
= f.submit 'Enable', class: 'btn btn-sm'
= f.submit 'Enable', class: 'gl-button btn btn-sm'
= paginate_without_count @projects
.col-md-6

View file

@ -16,7 +16,7 @@
- text, status = @domain.unverified? ? [_('Unverified'), 'badge-danger'] : [_('Verified'), 'badge-success']
.badge{ class: status }
= text
= link_to sprite_icon("redo"), verify_admin_serverless_domain_path(@domain.id), method: :post, class: "btn has-tooltip", title: _("Retry verification")
= link_to sprite_icon("redo"), verify_admin_serverless_domain_path(@domain.id), method: :post, class: "gl-button btn has-tooltip", title: _("Retry verification")
.col-sm-6
= f.label :serverless_domain_dns, _('DNS'), class: 'label-bold'
@ -65,7 +65,7 @@
%span.form-text.text-muted
= _("Upload a private key for your certificate")
= f.submit @domain.persisted? ? _('Save changes') : _('Add domain'), class: "btn btn-success js-serverless-domain-submit", disabled: @domain.persisted?
= f.submit @domain.persisted? ? _('Save changes') : _('Add domain'), class: "gl-button btn btn-success js-serverless-domain-submit", disabled: @domain.persisted?
- if @domain.persisted?
%button.gl-button.btn.btn-danger{ type: 'button', data: { toggle: 'modal', target: "#modal-delete-domain" } }
= _('Delete domain')
@ -88,7 +88,7 @@
= _("You are about to delete %{domain} from your instance. This domain will no longer be available to any Knative application.").html_safe % { domain: "<code>#{@domain.domain}</code>".html_safe }
.modal-footer
%a{ href: '#', data: { dismiss: 'modal' }, class: 'btn btn-default' }
%a{ href: '#', data: { dismiss: 'modal' }, class: 'gl-button btn btn-default' }
= _('Cancel')
= link_to _('Delete domain'),

View file

@ -4,4 +4,4 @@
= password_field_tag 'user[password]', nil, class: 'form-control', required: true, title: _('This field is required.'), data: { qa_selector: 'password_field' }
.submit-container.move-submit-down
= submit_tag _('Enter Admin Mode'), class: 'btn btn-success', data: { qa_selector: 'enter_admin_mode_button' }
= submit_tag _('Enter Admin Mode'), class: 'gl-button btn btn-success', data: { qa_selector: 'enter_admin_mode_button' }

View file

@ -6,4 +6,4 @@
= _("Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.")
.submit-container.move-submit-down
= submit_tag 'Verify code', class: 'btn btn-success'
= submit_tag 'Verify code', class: 'gl-button btn btn-success'

View file

@ -30,10 +30,10 @@
.btn.btn-sm.disabled
Submitted as ham
- else
= link_to 'Submit as ham', mark_as_ham_admin_spam_log_path(spam_log), method: :post, class: 'btn btn-sm btn-warning'
= link_to 'Submit as ham', mark_as_ham_admin_spam_log_path(spam_log), method: :post, class: 'gl-button btn btn-sm btn-warning'
- if user && !user.blocked?
= link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-sm"
= link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "gl-button btn btn-sm"
- else
.btn.btn-sm.disabled
Already blocked
= link_to 'Remove log', [:admin, spam_log], remote: true, method: :delete, class: "btn btn-sm btn-close js-remove-tr"
= link_to 'Remove log', [:admin, spam_log], remote: true, method: :delete, class: "gl-button btn btn-sm btn-close js-remove-tr"

View file

@ -88,7 +88,7 @@
.form-actions
- if @user.new_record?
= f.submit 'Create user', class: "btn gl-button btn-success"
= link_to 'Cancel', admin_users_path, class: "btn btn-cancel"
= link_to 'Cancel', admin_users_path, class: "gl-button btn btn-cancel"
- else
= f.submit 'Save changes', class: "btn gl-button btn-success"
= link_to 'Cancel', admin_user_path(@user), class: "btn gl-button btn-cancel"

View file

@ -24,5 +24,14 @@
%td= subscription.created_at
%td= link_to 'Remove', jira_connect_subscription_path(subscription), class: 'remove-subscription'
%p
%strong Browser limitations:
Adding a namespace currently works only in browsers that allow cross site cookies. Please make sure to use
%a{ href: 'https://www.mozilla.org/en-US/firefox/', target: '_blank', rel: 'noopener noreferrer' } Firefox
or
%a{ href: 'https://www.google.com/chrome/index.html', target: '_blank', rel: 'noopener noreferrer' } Google Chrome
or enable cross-site cookies in your browser when adding a namespace.
%a{ href: 'https://gitlab.com/gitlab-org/gitlab/-/issues/263509', target: '_blank', rel: 'noopener noreferrer' } Learn more
= page_specific_javascript_tag('jira_connect.js')
- add_page_specific_style 'page_bundles/jira_connect'

View file

@ -297,7 +297,11 @@
%span
= _('Environments')
= render_if_exists 'layouts/nav/sidebar/project_feature_flags_link'
- if project_nav_tab? :feature_flags
= nav_link(controller: :feature_flags) do
= link_to project_feature_flags_path(@project), title: _('Feature Flags'), class: 'shortcuts-feature-flags' do
%span
= _('Feature Flags')
- if project_nav_tab?(:product_analytics)
= nav_link(controller: :product_analytics) do

View file

@ -12,7 +12,7 @@
.timeline-entry-inner
.flash-container.timeline-content
.timeline-icon.d-none.d-sm-none.d-md-block
.timeline-icon.d-none.d-md-block
%a.author-link{ href: user_path(current_user) }
= image_tag avatar_icon_for_user(current_user), alt: current_user.to_reference, class: 'avatar s40'
.timeline-content.timeline-content-form

View file

@ -147,6 +147,14 @@
:weight: 1
:idempotent:
:tags: []
- :name: cronjob:ci_schedule_delete_objects_cron
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: cronjob:container_expiration_policy
:feature_category: :container_registry
:has_external_dependencies:
@ -1305,6 +1313,14 @@
:idempotent:
:tags:
- :requires_disk_io
- :name: ci_delete_objects
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: create_commit_signature
:feature_category: :source_code_management
:has_external_dependencies:

View file

@ -0,0 +1,38 @@
# frozen_string_literal: true
module Ci
class DeleteObjectsWorker
include ApplicationWorker
include LimitedCapacity::Worker
feature_category :continuous_integration
idempotent!
def perform_work(*args)
service.execute
end
def remaining_work_count(*args)
@remaining_work_count ||= service
.remaining_batches_count(max_batch_count: remaining_capacity)
end
def max_running_jobs
if ::Feature.enabled?(:ci_delete_objects_low_concurrency)
2
elsif ::Feature.enabled?(:ci_delete_objects_medium_concurrency)
20
elsif ::Feature.enabled?(:ci_delete_objects_high_concurrency)
50
else
0
end
end
private
def service
@service ||= DeleteObjectsService.new
end
end
end

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
module Ci
class ScheduleDeleteObjectsCronWorker
include ApplicationWorker
# rubocop:disable Scalability/CronWorkerContext
# This worker does not perform work scoped to a context
include CronjobQueue
# rubocop:enable Scalability/CronWorkerContext
feature_category :continuous_integration
idempotent!
def perform(*args)
Ci::DeleteObjectsWorker.perform_with_capacity(*args)
end
end
end

View file

@ -0,0 +1,5 @@
---
title: Add unattended database migration option
merge_request: 44392
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Add close button to issue, MR, and epic sidebar labels
merge_request: 42703
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Update integration descriptions to not be project-specific
merge_request: 44893
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Add note about cross site cookies browser limitaion to Jira App page
merge_request: 44898
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Remove duplicated BS display property from Commit/Snippet's HAML
merge_request: 44917
author: Takuya Noguchi
type: other

View file

@ -0,0 +1,5 @@
---
title: Parallelize removal of expired artifacts
merge_request: 39464
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Adds flexible rollout strategy UX and documentation
merge_request: 43611
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Include builds from child pipelines in latest sucessful build for ref/sha
merge_request: 29710
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: 'GrahphQL: Adds status to jobs, stages, and groups'
merge_request: 43069
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Move feature flags to core
merge_request: 44642
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Replace fa-chevron-down with GitLab SVG in project visibility settings
merge_request: 45021
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Improve merge error when pre-receive hooks fail in fast-forward merge
merge_request: 44843
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Update to Rack v2.1.4
merge_request: 44518
author:
type: fixed

View file

@ -0,0 +1,7 @@
---
name: ci_delete_objects_high_concurrency
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39464
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/247103
group: group::continuous integration
type: development
default_enabled: false

View file

@ -0,0 +1,7 @@
---
name: ci_delete_objects_low_concurrency
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39464
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/247103
group: group::continuous integration
type: development
default_enabled: false

View file

@ -0,0 +1,7 @@
---
name: ci_delete_objects_medium_concurrency
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39464
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/247103
group: group::continuous integration
type: development
default_enabled: false

View file

@ -435,6 +435,9 @@ production: &base
# Remove expired build artifacts
expire_build_artifacts_worker:
cron: "50 * * * *"
# Remove files from object storage
ci_schedule_delete_objects_worker:
cron: "*/16 * * * *"
# Stop expired environments
environments_auto_stop_cron_worker:
cron: "24 * * * *"

View file

@ -416,6 +416,9 @@ Settings.cron_jobs['pipeline_schedule_worker']['job_class'] = 'PipelineScheduleW
Settings.cron_jobs['expire_build_artifacts_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['expire_build_artifacts_worker']['cron'] ||= '50 * * * *'
Settings.cron_jobs['expire_build_artifacts_worker']['job_class'] = 'ExpireBuildArtifactsWorker'
Settings.cron_jobs['ci_schedule_delete_objects_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['ci_schedule_delete_objects_worker']['cron'] ||= '*/16 * * * *'
Settings.cron_jobs['ci_schedule_delete_objects_worker']['job_class'] = 'Ci::ScheduleDeleteObjectsCronWorker'
Settings.cron_jobs['environments_auto_stop_cron_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['environments_auto_stop_cron_worker']['cron'] ||= '24 * * * *'
Settings.cron_jobs['environments_auto_stop_cron_worker']['job_class'] = 'Environments::AutoStopCronWorker'

View file

@ -87,7 +87,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
delete :leave, on: :collection
end
resources :group_links, only: [:create, :update, :destroy], constraints: { id: /\d+/ }
resources :group_links, only: [:create, :update, :destroy], constraints: { id: /\d+|:id/ }
resources :uploads, only: [:create] do
collection do

View file

@ -50,6 +50,8 @@
- 2
- - ci_batch_reset_minutes
- 1
- - ci_delete_objects
- 1
- - container_repository
- 1
- - create_commit_signature

View file

@ -0,0 +1,29 @@
# frozen_string_literal: true
class CreateCiDeletedObjects < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
create_table :ci_deleted_objects, if_not_exists: true do |t|
t.integer :file_store, limit: 2, default: 1, null: false
t.datetime_with_timezone :pick_up_at, null: false, default: -> { 'now()' }, index: true
t.text :store_dir, null: false
# rubocop:disable Migration/AddLimitToTextColumns
# This column depends on the `file` column from `ci_job_artifacts` table
# which doesn't have a constraint limit on it.
t.text :file, null: false
# rubocop:enable Migration/AddLimitToTextColumns
end
add_text_limit(:ci_deleted_objects, :store_dir, 1024)
end
def down
drop_table :ci_deleted_objects
end
end

View file

@ -0,0 +1 @@
5f7a5fa697d769f5ccc9f0a6f19a91c8935f2559e019d50895574819494baf7e

View file

@ -10074,6 +10074,24 @@ CREATE SEQUENCE ci_daily_build_group_report_results_id_seq
ALTER SEQUENCE ci_daily_build_group_report_results_id_seq OWNED BY ci_daily_build_group_report_results.id;
CREATE TABLE ci_deleted_objects (
id bigint NOT NULL,
file_store smallint DEFAULT 1 NOT NULL,
pick_up_at timestamp with time zone DEFAULT now() NOT NULL,
store_dir text NOT NULL,
file text NOT NULL,
CONSTRAINT check_5e151d6912 CHECK ((char_length(store_dir) <= 1024))
);
CREATE SEQUENCE ci_deleted_objects_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE ci_deleted_objects_id_seq OWNED BY ci_deleted_objects.id;
CREATE TABLE ci_freeze_periods (
id bigint NOT NULL,
project_id bigint NOT NULL,
@ -17293,6 +17311,8 @@ ALTER TABLE ONLY ci_builds_runner_session ALTER COLUMN id SET DEFAULT nextval('c
ALTER TABLE ONLY ci_daily_build_group_report_results ALTER COLUMN id SET DEFAULT nextval('ci_daily_build_group_report_results_id_seq'::regclass);
ALTER TABLE ONLY ci_deleted_objects ALTER COLUMN id SET DEFAULT nextval('ci_deleted_objects_id_seq'::regclass);
ALTER TABLE ONLY ci_freeze_periods ALTER COLUMN id SET DEFAULT nextval('ci_freeze_periods_id_seq'::regclass);
ALTER TABLE ONLY ci_group_variables ALTER COLUMN id SET DEFAULT nextval('ci_group_variables_id_seq'::regclass);
@ -18282,6 +18302,9 @@ ALTER TABLE ONLY ci_builds_runner_session
ALTER TABLE ONLY ci_daily_build_group_report_results
ADD CONSTRAINT ci_daily_build_group_report_results_pkey PRIMARY KEY (id);
ALTER TABLE ONLY ci_deleted_objects
ADD CONSTRAINT ci_deleted_objects_pkey PRIMARY KEY (id);
ALTER TABLE ONLY ci_freeze_periods
ADD CONSTRAINT ci_freeze_periods_pkey PRIMARY KEY (id);
@ -19820,6 +19843,8 @@ CREATE UNIQUE INDEX index_ci_builds_runner_session_on_build_id ON ci_builds_runn
CREATE INDEX index_ci_daily_build_group_report_results_on_last_pipeline_id ON ci_daily_build_group_report_results USING btree (last_pipeline_id);
CREATE INDEX index_ci_deleted_objects_on_pick_up_at ON ci_deleted_objects USING btree (pick_up_at);
CREATE INDEX index_ci_freeze_periods_on_project_id ON ci_freeze_periods USING btree (project_id);
CREATE UNIQUE INDEX index_ci_group_variables_on_group_id_and_key ON ci_group_variables USING btree (group_id, key);

View file

@ -4,9 +4,11 @@ group: Progressive Delivery
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Feature Flags API **(PREMIUM)**
# Feature Flags API **(CORE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9566) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.5.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9566) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.5.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212318) to [GitLab Starter](https://about.gitlab.com/pricing/) in 13.4.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212318) to [GitLab Core](https://about.gitlab.com/pricing/) in 13.5.
NOTE: **Note:**
This API is behind a [feature flag](../operations/feature_flags.md#enable-or-disable-feature-flag-strategies).

View file

@ -4,9 +4,11 @@ group: Progressive Delivery
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Legacy Feature Flags API **(PREMIUM)**
# Legacy Feature Flags API **(CORE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9566) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.5.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9566) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.5.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212318) to [GitLab Starter](https://about.gitlab.com/pricing/) in 13.4.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212318) to [GitLab Core](https://about.gitlab.com/pricing/) in 13.5.
CAUTION: **Deprecation:**
This API is deprecated and [scheduled for removal in GitLab 14.0](https://gitlab.com/gitlab-org/gitlab/-/issues/213369). Use [this API](feature_flags.md) instead.

View file

@ -2010,6 +2010,11 @@ type BurnupChartDailyTotals {
}
type CiGroup {
"""
Detailed status of the group
"""
detailedStatus: DetailedStatus
"""
Jobs in group
"""
@ -2082,6 +2087,11 @@ type CiGroupEdge {
}
type CiJob {
"""
Detailed status of the job
"""
detailedStatus: DetailedStatus
"""
Name of the job
"""
@ -2154,6 +2164,11 @@ Identifier of Ci::Pipeline
scalar CiPipelineID
type CiStage {
"""
Detailed status of the stage
"""
detailedStatus: DetailedStatus
"""
Group of jobs for the stage
"""
@ -5295,42 +5310,42 @@ type DetailedStatus {
action: StatusAction
"""
Path of the details for the pipeline status
Path of the details for the status
"""
detailsPath: String!
detailsPath: String
"""
Favicon of the pipeline status
Favicon of the status
"""
favicon: String!
"""
Group of the pipeline status
Group of the status
"""
group: String!
"""
Indicates if the pipeline status has further details
Indicates if the status has further details
"""
hasDetails: Boolean!
"""
Icon of the pipeline status
Icon of the status
"""
icon: String!
"""
Label of the pipeline status
Label of the status
"""
label: String!
"""
Text of the pipeline status
Text of the status
"""
text: String!
"""
Tooltip associated with the pipeline status
Tooltip associated with the status
"""
tooltip: String!
}

View file

@ -5367,6 +5367,20 @@
"name": "CiGroup",
"description": null,
"fields": [
{
"name": "detailedStatus",
"description": "Detailed status of the group",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "DetailedStatus",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "jobs",
"description": "Jobs in group",
@ -5573,6 +5587,20 @@
"name": "CiJob",
"description": null,
"fields": [
{
"name": "detailedStatus",
"description": "Detailed status of the job",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "DetailedStatus",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": "Name of the job",
@ -5775,6 +5803,20 @@
"name": "CiStage",
"description": null,
"fields": [
{
"name": "detailedStatus",
"description": "Detailed status of the stage",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "DetailedStatus",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "groups",
"description": "Group of jobs for the stage",
@ -14479,25 +14521,21 @@
},
{
"name": "detailsPath",
"description": "Path of the details for the pipeline status",
"description": "Path of the details for the status",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "favicon",
"description": "Favicon of the pipeline status",
"description": "Favicon of the status",
"args": [
],
@ -14515,7 +14553,7 @@
},
{
"name": "group",
"description": "Group of the pipeline status",
"description": "Group of the status",
"args": [
],
@ -14533,7 +14571,7 @@
},
{
"name": "hasDetails",
"description": "Indicates if the pipeline status has further details",
"description": "Indicates if the status has further details",
"args": [
],
@ -14551,7 +14589,7 @@
},
{
"name": "icon",
"description": "Icon of the pipeline status",
"description": "Icon of the status",
"args": [
],
@ -14569,7 +14607,7 @@
},
{
"name": "label",
"description": "Label of the pipeline status",
"description": "Label of the status",
"args": [
],
@ -14587,7 +14625,7 @@
},
{
"name": "text",
"description": "Text of the pipeline status",
"description": "Text of the status",
"args": [
],
@ -14605,7 +14643,7 @@
},
{
"name": "tooltip",
"description": "Tooltip associated with the pipeline status",
"description": "Tooltip associated with the status",
"args": [
],

View file

@ -324,6 +324,7 @@ Represents the total number of issues and their weights for a particular day.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `detailedStatus` | DetailedStatus | Detailed status of the group |
| `name` | String | Name of the job group |
| `size` | Int | Size of the group |
@ -331,12 +332,14 @@ Represents the total number of issues and their weights for a particular day.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `detailedStatus` | DetailedStatus | Detailed status of the job |
| `name` | String | Name of the job |
### CiStage
| Field | Type | Description |
| ----- | ---- | ----------- |
| `detailedStatus` | DetailedStatus | Detailed status of the stage |
| `name` | String | Name of the stage |
### ClusterAgent
@ -851,14 +854,14 @@ Autogenerated return type of DestroySnippet.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `action` | StatusAction | Action information for the status. This includes method, button title, icon, path, and title |
| `detailsPath` | String! | Path of the details for the pipeline status |
| `favicon` | String! | Favicon of the pipeline status |
| `group` | String! | Group of the pipeline status |
| `hasDetails` | Boolean! | Indicates if the pipeline status has further details |
| `icon` | String! | Icon of the pipeline status |
| `label` | String! | Label of the pipeline status |
| `text` | String! | Text of the pipeline status |
| `tooltip` | String! | Tooltip associated with the pipeline status |
| `detailsPath` | String | Path of the details for the status |
| `favicon` | String! | Favicon of the status |
| `group` | String! | Group of the status |
| `hasDetails` | Boolean! | Indicates if the status has further details |
| `icon` | String! | Icon of the status |
| `label` | String! | Label of the status |
| `text` | String! | Text of the status |
| `tooltip` | String! | Tooltip associated with the status |
### DiffPosition

View file

@ -63,6 +63,11 @@ the given reference name and job, provided the job finished successfully. This
is the same as [getting the job's artifacts](#get-job-artifacts), but by
defining the job's name instead of its ID.
NOTE: **Note:**
If a pipeline is [parent of other child pipelines](../ci/parent_child_pipelines.md), artifacts
are searched in hierarchical order from parent to child. For example, if both parent and
child pipelines have a job with the same name, the artifact from the parent pipeline will be returned.
```plaintext
GET /projects/:id/jobs/artifacts/:ref_name/download?job=name
```
@ -157,6 +162,11 @@ Download a single artifact file for a specific job of the latest successful
pipeline for the given reference name from within the job's artifacts archive.
The file is extracted from the archive and streamed to the client.
In [GitLab 13.5](https://gitlab.com/gitlab-org/gitlab/-/issues/201784) and later, artifacts
for [parent and child pipelines](../ci/parent_child_pipelines.md) are searched in hierarchical
order from parent to child. For example, if both parent and child pipelines have a
job with the same name, the artifact from the parent pipeline is returned.
```plaintext
GET /projects/:id/jobs/artifacts/:ref_name/raw/*artifact_path?job=name
```

View file

@ -343,6 +343,11 @@ The latest artifacts are created by jobs in the **most recent** successful pipel
for the specific ref. If you run two types of pipelines for the same ref, timing determines the latest
artifact. For example, if a merge request creates a branch pipeline at the same time as a scheduled pipeline, the pipeline that completed most recently creates the latest artifact.
In [GitLab 13.5](https://gitlab.com/gitlab-org/gitlab/-/issues/201784) and later, artifacts
for [parent and child pipelines](../parent_child_pipelines.md) are searched in hierarchical
order from parent to child. For example, if both parent and child pipelines have a
job with the same name, the artifact from the parent pipeline is returned.
Artifacts for other pipelines can be accessed with direct access to them.
The structure of the URL to download the whole artifacts archive is the following:

Some files were not shown because too many files have changed in this diff Show more