Commit f036299e authored by Jan Provaznik's avatar Jan Provaznik

Merge branch '36820-fj-add-generic-canonical-url' into 'master'

Generate canonical url and remove trailing slash

See merge request gitlab-org/gitlab!46435
parents 55be3f42 307f2c29
......@@ -16,7 +16,7 @@ inherit_mode:
- Include
AllCops:
TargetRubyVersion: 2.6
TargetRubyVersion: 2.7
TargetRailsVersion: 6.0
Exclude:
- 'vendor/**/*'
......
5783f980c3c83022dd5a0173186fba4158948062
fa974a4ab21aa6acc4c3a00456265248a4d70703
import Vue from 'vue';
import DevopsAdoptionApp from './components/devops_adoption_app.vue';
export default () => {
const el = document.querySelector('.js-devops-adoption');
if (!el) return false;
const { emptyStateSvgPath } = el.dataset;
return new Vue({
el,
provide: {
emptyStateSvgPath,
},
render(h) {
return h(DevopsAdoptionApp);
},
});
};
// EE-specific feature. Find the implementation in the `ee/`-folder
export default () => {};
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
export default {
components: {
GlLink,
GlSprintf,
},
props: {
message: {
type: String,
required: true,
},
link: {
type: String,
required: true,
},
},
};
</script>
<template>
<span class="gl-text-gray-500">
<gl-sprintf :message="message">
<template #link="{ content }">
<gl-link class="gl-display-inline-block" :href="link" target="_blank">{{
content
}}</gl-link>
</template>
</gl-sprintf>
</span>
</template>
......@@ -81,7 +81,6 @@ export default {
<div class="incident-management-list">
<h5 class="gl-font-lg">{{ $options.i18n.title }}</h5>
<gl-table
:empty-text="$options.i18n.emptyState"
:items="integrations"
:fields="$options.fields"
:busy="loading"
......@@ -115,6 +114,14 @@ export default {
<template #table-busy>
<gl-loading-icon size="lg" color="dark" class="mt-3" />
</template>
<template #empty>
<div
class="gl-border-t-solid gl-border-b-solid gl-border-1 gl-border gl-border-gray-100 mt-n3"
>
<p class="gl-text-gray-400 gl-py-3 gl-my-3">{{ $options.i18n.emptyState }}</p>
</div>
</template>
</gl-table>
</div>
</template>
......@@ -56,7 +56,7 @@ export default {
data() {
return {
loading: false,
selectedIntegration: integrationTypes[1].value,
selectedIntegration: integrationTypes[0].value,
options: integrationTypes,
active: false,
authKey: '',
......@@ -88,34 +88,34 @@ export default {
];
},
isPrometheus() {
return this.selectedIntegration === 'prometheus';
return this.selectedIntegration === 'PROMETHEUS';
},
isOpsgenie() {
return this.selectedIntegration === 'opsgenie';
return this.selectedIntegration === 'OPSGENIE';
},
selectedIntegrationType() {
switch (this.selectedIntegration) {
case 'generic': {
case 'HTTP': {
return {
url: this.generic.url,
authKey: this.generic.authorizationKey,
activated: this.generic.activated,
authKey: this.generic.authKey,
active: this.generic.active,
resetKey: this.resetKey.bind(this),
};
}
case 'prometheus': {
case 'PROMETHEUS': {
return {
url: this.prometheus.prometheusUrl,
authKey: this.prometheus.authorizationKey,
activated: this.prometheus.activated,
resetKey: this.resetKey.bind(this, 'prometheus'),
url: this.prometheus.url,
authKey: this.prometheus.authKey,
active: this.prometheus.active,
resetKey: this.resetKey.bind(this, 'PROMETHEUS'),
targetUrl: this.prometheus.prometheusApiUrl,
};
}
case 'opsgenie': {
case 'OPSGENIE': {
return {
targetUrl: this.opsgenie.opsgenieMvcTargetUrl,
activated: this.opsgenie.activated,
active: this.opsgenie.active,
};
}
default: {
......@@ -161,16 +161,12 @@ export default {
},
},
mounted() {
if (
this.prometheus.activated ||
this.generic.activated ||
!this.opsgenie.opsgenieMvcIsAvailable
) {
if (this.prometheus.active || this.generic.active || !this.opsgenie.opsgenieMvcIsAvailable) {
this.removeOpsGenieOption();
} else if (this.opsgenie.activated) {
} else if (this.opsgenie.active) {
this.setOpsgenieAsDefault();
}
this.active = this.selectedIntegrationType.activated;
this.active = this.selectedIntegrationType.active;
this.authKey = this.selectedIntegrationType.authKey ?? '';
},
methods: {
......@@ -183,19 +179,19 @@ export default {
},
setOpsgenieAsDefault() {
this.options = this.options.map(el => {
if (el.value !== 'opsgenie') {
if (el.value !== 'OPSGENIE') {
return { ...el, disabled: true };
}
return { ...el, disabled: false };
});
this.selectedIntegration = this.options.find(({ value }) => value === 'opsgenie').value;
this.selectedIntegration = this.options.find(({ value }) => value === 'OPSGENIE').value;
if (this.targetUrl === null) {
this.targetUrl = this.selectedIntegrationType.targetUrl;
}
},
removeOpsGenieOption() {
this.options = this.options.map(el => {
if (el.value !== 'opsgenie') {
if (el.value !== 'OPSGENIE') {
return { ...el, disabled: false };
}
return { ...el, disabled: true };
......@@ -204,7 +200,7 @@ export default {
resetFormValues() {
this.testAlert.json = null;
this.targetUrl = this.selectedIntegrationType.targetUrl;
this.active = this.selectedIntegrationType.activated;
this.active = this.selectedIntegrationType.active;
},
dismissFeedback() {
this.serverError = null;
......@@ -212,7 +208,7 @@ export default {
this.isFeedbackDismissed = false;
},
resetKey(key) {
const fn = key === 'prometheus' ? this.resetPrometheusKey() : this.resetGenericKey();
const fn = key === 'PROMETHEUS' ? this.resetPrometheusKey() : this.resetGenericKey();
return fn
.then(({ data: { token } }) => {
......@@ -242,9 +238,10 @@ export default {
},
toggleActivated(value) {
this.loading = true;
const path = this.isOpsgenie ? this.opsgenie.formPath : this.generic.formPath;
return service
.updateGenericActive({
endpoint: this[this.selectedIntegration].formPath,
endpoint: path,
params: this.isOpsgenie
? { service: { opsgenie_mvc_target_url: this.targetUrl, opsgenie_mvc_enabled: value } }
: { service: { active: value } },
......@@ -345,7 +342,7 @@ export default {
if (this.canSaveForm) {
this.canSaveForm = false;
this.active = this.selectedIntegrationType.activated;
this.active = this.selectedIntegrationType.active;
}
},
},
......@@ -402,9 +399,9 @@ export default {
</gl-sprintf>
</span>
</gl-form-group>
<gl-form-group :label="$options.i18n.activeLabel" label-for="activated">
<gl-form-group :label="$options.i18n.activeLabel" label-for="active">
<toggle-button
id="activated"
id="active"
:disabled-input="loading"
:is-loading="loading"
:value="active"
......
<script>
import produce from 'immer';
import { s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { fetchPolicies } from '~/lib/graphql';
import createFlash, { FLASH_TYPES } from '~/flash';
import getIntegrationsQuery from '../graphql/queries/get_integrations.query.graphql';
import createHttpIntegrationMutation from '../graphql/mutations/create_http_integration.mutation.graphql';
import createPrometheusIntegrationMutation from '../graphql/mutations/create_prometheus_integration.mutation.graphql';
import IntegrationsList from './alerts_integrations_list.vue';
import SettingsFormOld from './alerts_settings_form_old.vue';
import SettingsFormNew from './alerts_settings_form_new.vue';
import { typeSet } from '../constants';
export default {
typeSet,
i18n: {
changesSaved: s__(
'AlertsIntegrations|The integration has been successfully saved. Alerts from this new integration should now appear on your alerts list.',
),
},
components: {
IntegrationsList,
SettingsFormOld,
......@@ -49,6 +60,7 @@ export default {
data() {
return {
errored: false,
isUpdating: false,
integrations: {},
};
},
......@@ -61,16 +73,85 @@ export default {
{
name: s__('AlertSettings|HTTP endpoint'),
type: s__('AlertsIntegrations|HTTP endpoint'),
active: this.generic.activated,
active: this.generic.active,
},
{
name: s__('AlertSettings|External Prometheus'),
type: s__('AlertsIntegrations|Prometheus'),
active: this.prometheus.activated,
active: this.prometheus.active,
},
];
},
},
methods: {
onCreateNewIntegration({ type, variables }) {
this.isUpdating = true;
this.$apollo
.mutate({
mutation:
type === this.$options.typeSet.http
? createHttpIntegrationMutation
: createPrometheusIntegrationMutation,
variables: {
...variables,
projectPath: this.projectPath,
},
update: this.updateIntegrations,
})
.then(({ data: { httpIntegrationCreate, prometheusIntegrationCreate } = {} } = {}) => {
const error = httpIntegrationCreate?.errors[0] || prometheusIntegrationCreate?.errors[0];
if (error) {
return createFlash({ message: error });
}
return createFlash({
message: this.$options.i18n.changesSaved,
type: FLASH_TYPES.SUCCESS,
});
})
.catch(err => {
this.errored = true;
createFlash({ message: err });
})
.finally(() => {
this.isUpdating = false;
});
},
updateIntegrations(
store,
{
data: { httpIntegrationCreate, prometheusIntegrationCreate },
},
) {
const integration =
httpIntegrationCreate?.integration || prometheusIntegrationCreate?.integration;
if (!integration) {
return;
}
const sourceData = store.readQuery({
query: getIntegrationsQuery,
variables: {
projectPath: this.projectPath,
},
});
const data = produce(sourceData, draftData => {
// eslint-disable-next-line no-param-reassign
draftData.project.alertManagementIntegrations.nodes = [
integration,
...draftData.project.alertManagementIntegrations.nodes,
];
});
store.writeQuery({
query: getIntegrationsQuery,
variables: {
projectPath: this.projectPath,
},
data,
});
},
},
};
</script>
......@@ -80,7 +161,11 @@ export default {
:integrations="glFeatures.httpIntegrationsList ? integrations.list : intergrationsOptionsOld"
:loading="loading"
/>
<settings-form-new v-if="glFeatures.httpIntegrationsList" />
<settings-form-new
v-if="glFeatures.httpIntegrationsList"
:loading="loading"
@on-create-new-integration="onCreateNewIntegration"
/>
<settings-form-old v-else />
</div>
</template>
import { s__ } from '~/locale';
// TODO: Remove this as part of the form old removal
export const i18n = {
usageSection: s__(
'AlertSettings|You must provide this URL and authorization key to authorize an external service to send alerts to GitLab. You can provide this URL and key to multiple services. After configuring an external service, alerts from your service will display on the GitLab %{linkStart}Alerts%{linkEnd} page.',
......@@ -39,13 +40,23 @@ export const i18n = {
integration: s__('AlertSettings|Integration'),
};
// TODO: Delete as part of old form removal in 13.6
export const integrationTypes = [
{ value: 'HTTP', text: s__('AlertSettings|HTTP Endpoint') },
{ value: 'PROMETHEUS', text: s__('AlertSettings|External Prometheus') },
{ value: 'OPSGENIE', text: s__('AlertSettings|Opsgenie') },
];
export const integrationTypesNew = [
{ value: '', text: s__('AlertSettings|Select integration type') },
{ value: 'generic', text: s__('AlertSettings|HTTP Endpoint') },
{ value: 'prometheus', text: s__('AlertSettings|External Prometheus') },
{ value: 'opsgenie', text: s__('AlertSettings|Opsgenie') },
...integrationTypes,
];
export const typeSet = {
http: 'HTTP',
prometheus: 'PROMETHEUS',
};
export const JSON_VALIDATE_DELAY = 250;
export const targetPrometheusUrlPlaceholder = 'http://prometheus.example.com/';
......
#import "../fragments/integration_item.fragment.graphql"
mutation createHttpIntegration($projectPath: ID!, $name: String!, $active: Boolean!) {
httpIntegrationCreate(input: { projectPath: $projectPath, name: $name, active: $active }) {
errors
integration {
...IntegrationItem
}
}
}
#import "../fragments/integration_item.fragment.graphql"
mutation createPrometheusIntegration($projectPath: ID!, $apiUrl: String!, $active: Boolean!) {
prometheusIntegrationCreate(
input: { projectPath: $projectPath, apiUrl: $apiUrl, active: $active }
) {
errors
integration {
...IntegrationItem
}
}
}
......@@ -48,9 +48,9 @@ export default el => {
el,
provide: {
prometheus: {
activated: parseBoolean(prometheusActivated),
prometheusUrl,
authorizationKey: prometheusAuthorizationKey,
active: parseBoolean(prometheusActivated),
url: prometheusUrl,
authKey: prometheusAuthorizationKey,
prometheusFormPath,
prometheusResetKeyPath,
prometheusApiUrl,
......@@ -58,14 +58,14 @@ export default el => {
generic: {
alertsSetupUrl,
alertsUsageUrl,
activated: parseBoolean(activatedStr),
active: parseBoolean(activatedStr),
formPath,
authorizationKey,
authKey: authorizationKey,
url,
},
opsgenie: {
formPath: opsgenieMvcFormPath,
activated: parseBoolean(opsgenieMvcEnabled),
active: parseBoolean(opsgenieMvcEnabled),
opsgenieMvcTargetUrl,
opsgenieMvcIsAvailable: parseBoolean(opsgenieMvcAvailable),
},
......
......@@ -70,6 +70,7 @@ const Api = {
featureFlagUserLists: '/api/:version/projects/:id/feature_flags_user_lists',
featureFlagUserList: '/api/:version/projects/:id/feature_flags_user_lists/:list_iid',
billableGroupMembersPath: '/api/:version/groups/:id/billable_members',
containerRegistryDetailsPath: '/api/:version/registry/repositories/:id/',
group(groupId, callback = () => {}) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
......@@ -106,6 +107,11 @@ const Api = {
return axios.delete(url);
},
containerRegistryDetails(registryId, options = {}) {
const url = Api.buildUrl(this.containerRegistryDetailsPath).replace(':id', registryId);
return axios.get(url, options);
},
groupMembers(id, options) {
const url = Api.buildUrl(this.groupMembersPath).replace(':id', encodeURIComponent(id));
......
......@@ -168,9 +168,6 @@ export default class CreateMergeRequestDropdown {
disable() {
this.disableCreateAction();
this.dropdownToggle.classList.add('disabled');
this.dropdownToggle.setAttribute('disabled', 'disabled');
}
disableCreateAction() {
......@@ -189,9 +186,6 @@ export default class CreateMergeRequestDropdown {
this.createTargetButton.classList.remove('disabled');
this.createTargetButton.removeAttribute('disabled');
this.dropdownToggle.classList.remove('disabled');
this.dropdownToggle.removeAttribute('disabled');
}
static findByValue(objects, ref, returnFirstMatch = false) {
......
<script>
import { GlIcon } from '@gitlab/ui';
import { GlButtonGroup, GlButton } from '@gitlab/ui';
const SCALE_STEP_SIZE = 0.2;
const DEFAULT_SCALE = 1;
......@@ -8,7 +8,8 @@ const MAX_SCALE = 2;
export default {
components: {
GlIcon,
GlButtonGroup,
GlButton,
},
data() {
return {
......@@ -49,17 +50,9 @@ export default {
</script>
<template>
<div class="design-scaler btn-group" role="group">
<button class="btn" :disabled="disableDecrease" @click="decrementScale">
<span class="gl-display-flex gl-justify-content-center gl-align-items-center gl-icon s16">
</span>
</button>
<button class="btn" :disabled="disableReset" @click="resetScale">
<gl-icon name="redo" />
</button>
<button class="btn" :disabled="disableIncrease" @click="incrementScale">
<gl-icon name="plus" />
</button>
</div>
<gl-button-group class="gl-z-index-1">
<gl-button icon="dash" :disabled="disableDecrease" @click="decrementScale" />
<gl-button icon="redo" :disabled="disableReset" @click="resetScale" />
<gl-button icon="plus" :disabled="disableIncrease" @click="incrementScale" />
</gl-button-group>
</template>
......@@ -626,7 +626,7 @@ export function switchToFullDiffFromRenamedFile({ commit, dispatch, state }, { d
.then(({ data }) => {
const lines = data.map((line, index) =>
prepareLineForRenamedFile({
diffViewType: state.diffViewType,
diffViewType: window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType,
line,
diffFile,
index,
......@@ -638,6 +638,7 @@ export function switchToFullDiffFromRenamedFile({ commit, dispatch, state }, { d
viewer: {
...diffFile.alternate_viewer,
automaticallyCollapsed: false,
manuallyCollapsed: false,
},
});
commit(types.SET_CURRENT_VIEW_DIFF_FILE_LINES, { filePath: diffFile.file_path, lines });
......
......@@ -378,8 +378,13 @@ export default {
},
[types.SET_CURRENT_VIEW_DIFF_FILE_LINES](state, { filePath, lines }) {
const file = state.diffFiles.find(f => f.file_path === filePath);
const currentDiffLinesKey =
state.diffViewType === 'inline' ? 'highlighted_diff_lines' : 'parallel_diff_lines';
let currentDiffLinesKey;
if (window.gon?.features?.unifiedDiffLines || state.diffViewType === 'inline') {
currentDiffLinesKey = 'highlighted_diff_lines';
} else {
currentDiffLinesKey = 'parallel_diff_lines';
}
file[currentDiffLinesKey] = lines;
},
......
......@@ -8,9 +8,9 @@ import {
GlBadge,
GlAlert,
GlSprintf,
GlDeprecatedDropdown,
GlDeprecatedDropdownItem,
GlDeprecatedDropdownDivider,
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlIcon,
} from '@gitlab/ui';
import { deprecatedCreateFlash as createFlash } from '~/flash';
......@@ -43,9 +43,9 @@ export default {
GlBadge,
GlAlert,
GlSprintf,
GlDeprecatedDropdown,
GlDeprecatedDropdownItem,
GlDeprecatedDropdownDivider,
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
TimeAgoTooltip,
},
directives: {
......@@ -331,38 +331,38 @@ export default {
</gl-button>
</form>
</div>
<gl-deprecated-dropdown
<gl-dropdown
text="Options"
class="error-details-options d-md-none"
right
:disabled="issueUpdateInProgress"
>
<gl-deprecated-dropdown-item
<gl-dropdown-item
data-qa-selector="update_ignore_status_button"
@click="onIgnoreStatusUpdate"
>{{ ignoreBtnLabel }}</gl-deprecated-dropdown-item
>{{ ignoreBtnLabel }}</gl-dropdown-item
>
<gl-deprecated-dropdown-item
<gl-dropdown-item
data-qa-selector="update_resolve_status_button"
@click="onResolveStatusUpdate"
>{{ resolveBtnLabel }}</gl-deprecated-dropdown-item
>{{ resolveBtnLabel }}</gl-dropdown-item
>
<gl-deprecated-dropdown-divider />
<gl-deprecated-dropdown-item
<gl-dropdown-divider />
<gl-dropdown-item
v-if="error.gitlabIssuePath"
data-qa-selector="view_issue_button"
:href="error.gitlabIssuePath"
variant="success"
>{{ __('View issue') }}</gl-deprecated-dropdown-item
>{{ __('View issue') }}</gl-dropdown-item
>
<gl-deprecated-dropdown-item
<gl-dropdown-item
v-if="!error.gitlabIssuePath"
:loading="issueCreationInProgress"
data-qa-selector="create_issue_button"
@click="createIssue"
>{{ __('Create issue') }}</gl-deprecated-dropdown-item
>{{ __('Create issue') }}</gl-dropdown-item
>
</gl-deprecated-dropdown>
</gl-dropdown>
</div>
</div>
<div>
......
......@@ -8,9 +8,9 @@ import {
GlLoadingIcon,
GlTable,
GlFormInput,
GlDeprecatedDropdown,
GlDeprecatedDropdownItem,
GlDeprecatedDropdownDivider,
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlTooltipDirective,
GlPagination,
} from '@gitlab/ui';
......@@ -72,9 +72,9 @@ export default {
components: {
GlEmptyState,
GlButton,
GlDeprecatedDropdown,
GlDeprecatedDropdownItem,
GlDeprecatedDropdownDivider,
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlIcon,
GlLink,
GlLoadingIcon,
......@@ -233,30 +233,30 @@ export default {
>
<div class="search-box flex-fill mb-1 mb-md-0">
<div class="filtered-search-box mb-0">
<gl-deprecated-dropdown
<gl-dropdown
:text="__('Recent searches')"
class="filtered-search-history-dropdown-wrapper"
toggle-class="filtered-search-history-dropdown-toggle-button"
toggle-class="filtered-search-history-dropdown-toggle-button gl-shadow-none! gl-border-r-gray-200! gl-border-1! gl-rounded-0!"
:disabled="loading"
>
<div v-if="!$options.hasLocalStorage" class="px-3">
{{ __('This feature requires local storage to be enabled') }}
</div>
<template v-else-if="recentSearches.length > 0">
<gl-deprecated-dropdown-item
<gl-dropdown-item
v-for="searchQuery in recentSearches"
:key="searchQuery"
@click="setSearchText(searchQuery)"
>{{ searchQuery }}
</gl-deprecated-dropdown-item>
<gl-deprecated-dropdown-divider />
<gl-deprecated-dropdown-item ref="clearRecentSearches" @click="clearRecentSearches"
</gl-dropdown-item>
<gl-dropdown-divider />
<gl-dropdown-item ref="clearRecentSearches" @click="clearRecentSearches"
>{{ __('Clear recent searches') }}
</gl-deprecated-dropdown-item>
</gl-dropdown-item>
</template>
<div v-else class="px-3">{{ __("You don't have any recent searches") }}</div>
</gl-deprecated-dropdown>
<div class="filtered-search-input-container flex-fill">
</gl-dropdown>
<div class="filtered-search-input-container gl-flex-fill-1">
<gl-form-input
v-model="errorSearchQuery"
class="pl-2 filtered-search"
......@@ -280,49 +280,44 @@ export default {
</div>
</div>
<gl-deprecated-dropdown
<gl-dropdown
:text="$options.statusFilters[statusFilter]"
class="status-dropdown mx-md-1 mb-1 mb-md-0"
menu-class="dropdown"
:disabled="loading"
right
>
<gl-deprecated-dropdown-item
<gl-dropdown-item
v-for="(label, status) in $options.statusFilters"
:key="status"
@click="filterErrors(status, label)"
>
<span class="d-flex">
<gl-icon
class="flex-shrink-0 append-right-4"
class="gl-new-dropdown-item-check-icon"
:class="{ invisible: !isCurrentStatusFilter(status) }"
name="mobile-issue-close"
/>
{{ label }}
</span>
</gl-deprecated-dropdown-item>
</gl-deprecated-dropdown>
</gl-dropdown-item>
</gl-dropdown>
<gl-deprecated-dropdown
:text="$options.sortFields[sortField]"
left
:disabled="loading"
menu-class="dropdown"
>
<gl-deprecated-dropdown-item
<gl-dropdown :text="$options.sortFields[sortField]" right :disabled="loading">
<gl-dropdown-item
v-for="(label, field) in $options.sortFields"
:key="field"
@click="sortByField(field)"
>
<span class="d-flex">
<gl-icon
class="flex-shrink-0 append-right-4"
class="gl-new-dropdown-item-check-icon"
:class="{ invisible: !isCurrentSortField(field) }"
name="mobile-issue-close"
/>
{{ label }}
</span>
</gl-deprecated-dropdown-item>
</gl-deprecated-dropdown>
</gl-dropdown-item>
</gl-dropdown>
</div>
<div v-if="loading" class="py-3">
......
<script>
import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { getDisplayName } from '../utils';
export default {
components: {
GlDeprecatedDropdown,
GlDeprecatedDropdownItem,
GlDropdown,
GlDropdownItem,
},
props: {
dropdownLabel: {
......@@ -52,22 +52,22 @@ export default {
<div :class="{ 'gl-show-field-errors': isProjectInvalid }">
<label class="label-bold" for="project-dropdown">{{ __('Project') }}</label>
<div class="row">
<gl-deprecated-dropdown
<gl-dropdown
id="project-dropdown"
class="col-8 col-md-9 gl-pr-0"
:disabled="!hasProjects"
menu-class="w-100 mw-100"
toggle-class="dropdown-menu-toggle w-100 gl-field-error-outline"
toggle-class="dropdown-menu-toggle gl-field-error-outline"
:text="dropdownLabel"
>
<gl-deprecated-dropdown-item
<gl-dropdown-item
v-for="project in projects"
:key="`${project.organizationSlug}.${project.slug}`"
class="w-100"
@click="$emit('select-project', project)"
>{{ getDisplayName(project) }}</gl-deprecated-dropdown-item
>{{ getDisplayName(project) }}</gl-dropdown-item
>
</gl-deprecated-dropdown>
</gl-dropdown>
</div>
<p v-if="isProjectInvalid" class="js-project-dropdown-error gl-field-error">
{{ invalidProjectLabel }}
......
......@@ -27,7 +27,7 @@ export default {
rolloutUserListLabel: s__('FeatureFlag|User List'),
rolloutUserListDescription: s__('FeatureFlag|Select a user list'),
rolloutUserListNoListError: s__('FeatureFlag|There are no configured user lists'),
defaultDropdownText: s__('FeatureFlags|Select a user list'),
defaultDropdownText: s__('FeatureFlags|No user list selected'),
},
computed: {
...mapGetters(['hasUserLists', 'isLoading', 'hasError', 'userListOptions']),
......@@ -36,7 +36,7 @@ export default {
return this.strategy?.userList?.id ?? '';
},
dropdownText() {
return this.strategy?.userList?.name ?? this.$options.defaultDropdownText;
return this.strategy?.userList?.name ?? this.$options.translations.defaultDropdownText;
},
},
mounted() {
......
......@@ -37,8 +37,6 @@ const restartJobsPolling = () => {
if (eTagPoll) eTagPoll.restart();
};
const setFilter = ({ commit }, filter) => commit(types.SET_FILTER, filter);
const setImportTarget = ({ commit }, { repoId, importTarget }) =>
commit(types.SET_IMPORT_TARGET, { repoId, importTarget });
......@@ -172,12 +170,9 @@ const fetchNamespacesFactory = (namespacesPath = isRequired()) => ({ commit }) =
});
};
const setPage = ({ state, commit, dispatch }, page) => {
if (page === state.pageInfo.page) {
return null;
}
const setFilter = ({ commit, dispatch }, filter) => {
commit(types.SET_FILTER, filter);
commit(types.SET_PAGE, page);
return dispatch('fetchRepos');
};
......@@ -188,7 +183,6 @@ export default ({ endpoints = isRequired() }) => ({
setFilter,
setImportTarget,
importAll,
setPage,
fetchRepos: fetchReposFactory({ reposPath: endpoints.reposPath }),
fetchImport: fetchImportFactory(endpoints.importPath),
fetchJobs: fetchJobsFactory(endpoints.jobsPath),
......
......@@ -81,7 +81,7 @@ export default {
})
.then(({ data }) => {
if (data.updateIssue.errors.length) {
createFlash(data.updateIssue.errors.join('. '));
createFlash({ message: data.updateIssue.errors.join('. ') });
return;
}
......@@ -95,7 +95,7 @@ export default {
// Dispatch event which updates open/close state, shared among the issue show page
document.dispatchEvent(new CustomEvent('issuable_vue_app:change', payload));
})
.catch(() => createFlash(__('Update failed. Please try again.')))
.catch(() => createFlash({ message: __('Update failed. Please try again.') }))
.finally(() => {
this.isUpdatingState = false;
});
......
<script>
/* eslint-disable vue/no-v-html */
import { GlButton, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { GlButton, GlIcon, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
import notesEventHub from '../event_hub';
export default {
i18n: {
information: s__(
"Notes|You're only seeing %{boldStart}other activity%{boldEnd} in the feed. To add a comment, switch to one of the following options.",
),
},
components: {
GlButton,
GlIcon,
},
computed: {
timelineContent() {
return sprintf(
__(
"You're only seeing %{startTag}other activity%{endTag} in the feed. To add a comment, switch to one of the following options.",
),
{
startTag: `<b>`,
endTag: `</b>`,
},
false,
);
},
GlSprintf,
},
methods: {
selectFilter(value) {
......@@ -41,12 +32,18 @@ export default {
<gl-icon name="comment" />
</div>
<div class="timeline-content">
<div ref="timelineContent" v-html="timelineContent"></div>
<div data-testid="discussion-filter-timeline-content">
<gl-sprintf :message="$options.i18n.information">
<template #bold="{ content }">
<b>{{ content }}</b>
</template>
</gl-sprintf>
</div>
<div class="discussion-filter-actions mt-2">
<gl-button ref="showAllActivity" variant="default" @click="selectFilter(0)">
<gl-button variant="default" @click="selectFilter(0)">
{{ __('Show all activity') }}
</gl-button>
<gl-button ref="showComments" variant="default" @click="selectFilter(1)">
<gl-button variant="default" @click="selectFilter(1)">
{{ __('Show comments only') }}
</gl-button>
</div>
......
import initDevopAdoption from 'ee_else_ce/admin/dev_ops_report/devops_adoption';
import initDevOpsScoreEmptyState from '~/admin/dev_ops_report/devops_score_empty_state';
import initDevopAdoption from '~/admin/dev_ops_report/devops_adoption';
initDevOpsScoreEmptyState();
initDevopAdoption();
......@@ -21,35 +21,32 @@ function mountRemoveMemberModal() {
});
}
document.addEventListener('DOMContentLoaded', () => {
groupsSelect();
memberExpirationDate();
memberExpirationDate('.js-access-expiration-date-groups');
mountRemoveMemberModal();
const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
initGroupMembersApp(
document.querySelector('.js-group-members-list'),
SHARED_FIELDS.concat(['source', 'granted']),
memberRequestFormatter,
);
initGroupMembersApp(
document.querySelector('.js-group-linked-list'),
SHARED_FIELDS.concat('granted'),
groupLinkRequestFormatter,
);
initGroupMembersApp(
document.querySelector('.js-group-invited-members-list'),
SHARED_FIELDS.concat('invited'),
memberRequestFormatter,
);
initGroupMembersApp(
document.querySelector('.js-group-access-requests-list'),
SHARED_FIELDS.concat('requested'),
memberRequestFormatter,
);
const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
groupsSelect();
memberExpirationDate();
memberExpirationDate('.js-access-expiration-date-groups');
mountRemoveMemberModal();
initGroupMembersApp(
document.querySelector('.js-group-members-list'),
SHARED_FIELDS.concat(['source', 'granted']),
memberRequestFormatter,
);
initGroupMembersApp(
document.querySelector('.js-group-linked-list'),
SHARED_FIELDS.concat('granted'),
groupLinkRequestFormatter,
);
initGroupMembersApp(
document.querySelector('.js-group-invited-members-list'),
SHARED_FIELDS.concat('invited'),
memberRequestFormatter,
);
initGroupMembersApp(
document.querySelector('.js-group-access-requests-list'),
SHARED_FIELDS.concat('requested'),
memberRequestFormatter,
);
new Members(); // eslint-disable-line no-new
new UsersSelect(); // eslint-disable-line no-new
});
new Members(); // eslint-disable-line no-new
new UsersSelect(); // eslint-disable-line no-new
......@@ -7,11 +7,11 @@ document.addEventListener('DOMContentLoaded', () => {
if (gon?.features?.ciLintVue) {
import(/* webpackChunkName: 'ciLintIndex' */ '~/ci_lint/index')
.then(module => module.default())
.catch(() => createFlash(ERROR));
.catch(() => createFlash({ message: ERROR }));
} else {
import(/* webpackChunkName: 'ciLintEditor' */ '../ci_lint_editor')
// eslint-disable-next-line new-cap
.then(module => new module.default())
.catch(() => createFlash(ERROR));
.catch(() => createFlash({ message: ERROR }));
}
});
......@@ -7,11 +7,11 @@ document.addEventListener('DOMContentLoaded', () => {
if (gon?.features?.ciLintVue) {
import(/* webpackChunkName: 'ciLintIndex' */ '~/ci_lint/index')
.then(module => module.default())
.catch(() => createFlash(ERROR));
.catch(() => createFlash({ message: ERROR }));
} else {
import(/* webpackChunkName: 'ciLintEditor' */ '../ci_lint_editor')
// eslint-disable-next-line new-cap
.then(module => new module.default())
.catch(() => createFlash(ERROR));
.catch(() => createFlash({ message: ERROR }));
}
});
......@@ -40,7 +40,7 @@ document.addEventListener('DOMContentLoaded', () => {
new Diff();
})
.catch(() => {
flash(__('An error occurred while retrieving diff files'));
flash({ message: __('An error occurred while retrieving diff files') });
});
} else {
new Diff();
......
......@@ -57,7 +57,7 @@ export default {
<tooltip-on-truncate :title="jobName" truncate-target="child" placement="top">
<div
:id="jobId"
class="pipeline-job-pill gl-bg-white gl-text-center gl-text-truncate gl-rounded-pill gl-mb-3 gl-px-5 gl-py-2 gl-relative gl-z-index-1 gl-transition-duration-slow gl-transition-timing-function-ease"
class="gl-w-15 gl-bg-white gl-text-center gl-text-truncate gl-rounded-pill gl-mb-3 gl-px-5 gl-py-2 gl-relative gl-z-index-1 gl-transition-duration-slow gl-transition-timing-function-ease"
:class="jobPillClasses"
@mouseover="onMouseEnter"
@mouseleave="onMouseLeave"
......
......@@ -97,15 +97,20 @@ export default {
this.reportFailure(DRAW_FAILURE);
}
},
getStageBackgroundClass(index) {
getStageBackgroundClasses(index) {
const { length } = this.pipelineData.stages;
// It's possible for a graph to have only one stage, in which
// case we concatenate both the left and right rounding classes
if (length === 1) {
return 'stage-rounded';
} else if (index === 0) {
return 'stage-left-rounded';
} else if (index === length - 1) {
return 'stage-right-rounded';
return 'gl-rounded-bottom-left-6 gl-rounded-top-left-6 gl-rounded-bottom-right-6 gl-rounded-top-right-6';
}
if (index === 0) {
return 'gl-rounded-bottom-left-6 gl-rounded-top-left-6';
}
if (index === length - 1) {
return 'gl-rounded-bottom-right-6 gl-rounded-top-right-6';
}
return '';
......@@ -190,7 +195,8 @@ export default {
>
<div
class="gl-display-flex gl-align-items-center gl-bg-white gl-w-full gl-px-8 gl-py-4 gl-mb-5"
:class="getStageBackgroundClass(index)"
:class="getStageBackgroundClasses(index)"
data-testid="stage-background"
>
<stage-pill :stage-name="stage.name" :is-empty="stage.groups.length === 0" />
</div>
......
......@@ -26,7 +26,7 @@ export default {
<template>
<tooltip-on-truncate :title="stageName" truncate-target="child" placement="top">
<div
class="gl-px-5 gl-py-2 gl-text-white gl-text-center gl-text-truncate gl-rounded-pill pipeline-stage-pill"
class="gl-px-5 gl-py-2 gl-text-white gl-text-center gl-text-truncate gl-rounded-pill gl-w-20"
:class="emptyClass"
>
{{ stageName }}
......
......@@ -15,6 +15,10 @@ export const DELETE_TAGS_SUCCESS_MESSAGE = s__(
'ContainerRegistry|Tags successfully marked for deletion.',
);
export const FETCH_IMAGE_DETAILS_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while fetching the image details.',
);
export const TAGS_LIST_TITLE = s__('ContainerRegistry|Image tags');
export const DIGEST_LABEL = s__('ContainerRegistry|Digest: %{imageId}');
export const CREATED_AT_LABEL = s__('ContainerRegistry|Published %{timeInfo}');
......
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import Api from '~/api';
import * as types from './mutation_types';
import {
FETCH_IMAGES_LIST_ERROR_MESSAGE,
DEFAULT_PAGE,
DEFAULT_PAGE_SIZE,
FETCH_TAGS_LIST_ERROR_MESSAGE,
FETCH_IMAGE_DETAILS_ERROR_MESSAGE,
} from '../constants/index';
import { decodeAndParse } from '../utils';
......@@ -61,6 +63,19 @@ export const requestTagsList = ({ commit, dispatch }, { pagination = {}, params
});
};
export const requestImageDetailsAndTagsList = ({ dispatch, commit }, id) => {
commit(types.SET_MAIN_LOADING, true);
return Api.containerRegistryDetails(id)
.then(({ data }) => {
commit(types.SET_IMAGE_DETAILS, data);
dispatch('requestTagsList');
})
.catch(() => {
createFlash(FETCH_IMAGE_DETAILS_ERROR_MESSAGE);
commit(types.SET_MAIN_LOADING, false);
});
};
export const requestDeleteTag = ({ commit, dispatch, state }, { tag, params }) => {
commit(types.SET_MAIN_LOADING, true);
return axios
......
......@@ -7,3 +7,4 @@ export const SET_MAIN_LOADING = 'SET_MAIN_LOADING';
export const SET_TAGS_PAGINATION = 'SET_TAGS_PAGINATION';
export const SET_TAGS_LIST_SUCCESS = 'SET_TAGS_LIST_SUCCESS';
export const SET_SHOW_GARBAGE_COLLECTION_TIP = 'SET_SHOW_GARBAGE_COLLECTION_TIP';
export const SET_IMAGE_DETAILS = 'SET_IMAGE_DETAILS';
......@@ -47,4 +47,8 @@ export default {
const normalizedHeaders = normalizeHeaders(headers);
state.tagsPagination = parseIntPagination(normalizedHeaders);
},
[types.SET_IMAGE_DETAILS](state, details) {
state.imageDetails = details;
},
};
......@@ -3,6 +3,7 @@ export default () => ({
showGarbageCollectionTip: false,
config: {},
images: [],
imageDetails: {},
tags: [],
pagination: {},
tagsPagination: {},
......
export const decodeAndParse = param => JSON.parse(window.atob(param));
// eslint-disable-next-line @gitlab/require-i18n-strings
export const pathGenerator = (imageDetails, ending = 'tags?format=json') => {
// this method is a temporary workaround, to be removed with graphql implementation
// https://gitlab.com/gitlab-org/gitlab/-/issues/276432
const basePath = imageDetails.path.replace(`/${imageDetails.name}`, '');
return `/${basePath}/registry/repository/${imageDetails.id}/${ending}`;
};
......@@ -26,7 +26,7 @@ export default {
required: false,
default: null,
},
openPath: {
openedPath: {
type: String,
required: false,
default: '',
......@@ -43,7 +43,7 @@ export default {
},
},
computed: {
open() {
opened() {
return this.total - (this.closed + (this.merged || 0));
},
showMerged() {
......@@ -63,8 +63,8 @@ export default {
<span class="gl-white-space-pre-wrap" data-testid="open-stat">
<gl-sprintf :message="__('Open: %{open}')">
<template #open>
<gl-link v-if="openPath" :href="openPath">{{ open }}</gl-link>
<template v-else>{{ open }}</template>
<gl-link v-if="openedPath" :href="openedPath">{{ opened }}</gl-link>
<template v-else>{{ opened }}</template>
</template>
</gl-sprintf>
</span>
......
......@@ -87,9 +87,14 @@ export default {
<release-block-header :release="release" />
<div class="card-body">
<div v-if="shouldRenderMilestoneInfo">
<!-- TODO: Switch open* links to opened* once fields have been updated in GraphQL -->
<release-block-milestone-info
:milestones="milestones"
:open-issues-path="release._links.issuesUrl"
:opened-issues-path="release._links.openedIssuesUrl"
:closed-issues-path="release._links.closedIssuesUrl"
:opened-merge-requests-path="release._links.openedMergeRequestsUrl"
:merged-merge-requests-path="release._links.mergedMergeRequestsUrl"
:closed-merge-requests-path="release._links.closedMergeRequestsUrl"
/>
<hr class="mb-3 mt-0" />
</div>
......
......@@ -20,7 +20,7 @@ export default {
type: Array,
required: true,
},
openIssuesPath: {
openedIssuesPath: {
type: String,
required: false,
default: '',
......@@ -30,7 +30,7 @@ export default {
required: false,
default: '',
},
openMergeRequestsPath: {
openedMergeRequestsPath: {
type: String,
required: false,
default: '',
......@@ -173,7 +173,7 @@ export default {
:label="__('Issues')"
:total="issueCounts.total"
:closed="issueCounts.closed"
:open-path="openIssuesPath"
:opened-path="openedIssuesPath"
:closed-path="closedIssuesPath"
data-testid="issue-stats"
/>
......@@ -183,7 +183,7 @@ export default {
:total="mergeRequestCounts.total"
:merged="mergeRequestCounts.merged"
:closed="mergeRequestCounts.closed"
:open-path="openMergeRequestsPath"
:opened-path="openedMergeRequestsPath"
:merged-path="mergedMergeRequestsPath"
:closed-path="closedMergeRequestsPath"
data-testid="merge-request-stats"
......
......@@ -33,9 +33,12 @@ fragment Release on Release {
}
links {
editUrl
issuesUrl
mergeRequestsUrl
selfUrl
openedIssuesUrl
closedIssuesUrl
openedMergeRequestsUrl
mergedMergeRequestsUrl
closedMergeRequestsUrl
}
commit {
sha
......
......@@ -137,8 +137,8 @@ export default {
:href="commit.author.webPath"
class="commit-author-link js-user-link"
>
{{ commit.author.name }}
</gl-link>
{{ commit.author.name }}</gl-link
>
<template v-else>
{{ commit.authorName }}
</template>
......
......@@ -65,7 +65,7 @@ export default {
.then(({ data }) => {
this.milestones = data;
})
.catch(() => createFlash(__('There was a problem fetching milestones.')))
.catch(() => createFlash({ message: __('There was a problem fetching milestones.') }))
.finally(() => {
this.loading = false;
});
......
......@@ -75,10 +75,6 @@ $t-gray-a-16-design-pin: rgba($black, 0.16);
left: 0;
}
.design-scaler {
z-index: 1;
}
.design-scaler-wrapper {
bottom: 0;
left: 50%;
......
......@@ -486,23 +486,3 @@
.progress-bar.bg-primary {
background-color: $blue-500 !important;
}
.pipeline-stage-pill {
width: 10rem;
}
.pipeline-job-pill {
width: 8rem;
}
.stage-rounded {
border-radius: 2rem;
}
.stage-left-rounded {
border-radius: 2rem 0 0 2rem;
}
.stage-right-rounded {
border-radius: 0 2rem 2rem 0;
}
......@@ -188,6 +188,12 @@ ul.related-merge-requests > li {
border-width: 1px;
line-height: $line-height-base;
width: auto;
&.disabled {
background-color: $gray-light;
border-color: $gray-100;
color: $gl-text-color-disabled;
}
}
}
......
......@@ -132,13 +132,23 @@ class GroupsController < Groups::ApplicationController
def update
if Groups::UpdateService.new(@group, current_user, group_params).execute
redirect_to edit_group_path(@group, anchor: params[:update_section]), notice: "Group '#{@group.name}' was successfully updated."
notice = "Group '#{@group.name}' was successfully updated."
redirect_to edit_group_origin_location, notice: notice
else
@group.reset
render action: "edit"
end
end
def edit_group_origin_location
if params.dig(:group, :redirect_target) == 'repository_settings'
group_settings_repository_path(@group, anchor: 'js-default-branch-name')
else
edit_group_path(@group, anchor: params[:update_section])
end
end
def destroy
Groups::DestroyService.new(@group, current_user).async_execute
......
......@@ -48,18 +48,14 @@ class Import::BaseController < ApplicationController
private
def filter_attribute
:name
end
def sanitized_filter_param
@filter ||= sanitize(params[:filter])
@filter ||= sanitize(params[:filter])&.downcase
end
def filtered(collection)
return collection unless sanitized_filter_param
collection.select { |item| item[filter_attribute].include?(sanitized_filter_param) }
collection.select { |item| item[:name].to_s.downcase.include?(sanitized_filter_param) }
end
def serialized_provider_repos
......
......@@ -132,8 +132,4 @@ class Import::BitbucketController < Import::BaseController
refresh_token: session[:bitbucket_refresh_token]
}
end
def sanitized_filter_param
@filter ||= sanitize(params[:filter])
end
end
......@@ -170,10 +170,6 @@ class Import::BitbucketServerController < Import::BaseController
BitbucketServer::Paginator::PAGE_LENGTH
end
def sanitized_filter_param
sanitize(params[:filter])
end
def bitbucket_connection_error(error)
flash[:alert] = _("Unable to connect to server: %{error}") % { error: error }
clear_session_data
......
......@@ -245,14 +245,6 @@ class Import::GithubController < Import::BaseController
def extra_import_params
{}
end
def sanitized_filter_param
@filter ||= sanitize(params[:filter])
end
def filter_attribute
:name
end
end
Import::GithubController.prepend_if_ee('EE::Import::GithubController')
......@@ -3,7 +3,7 @@
class Projects::Ci::LintsController < Projects::ApplicationController
before_action :authorize_create_pipeline!
before_action do
push_frontend_feature_flag(:ci_lint_vue, project)
push_frontend_feature_flag(:ci_lint_vue, project, default_enabled: true)
end
feature_category :pipeline_authoring
......
......@@ -52,7 +52,7 @@ class Projects::RunnersController < Projects::ApplicationController
end
def toggle_shared_runners
if Feature.enabled?(:disable_shared_runners_on_group, default_enabled: true) && !project.shared_runners_enabled && project.group && project.group.shared_runners_setting == 'disabled_and_unoverridable'
if !project.shared_runners_enabled && project.group && project.group.shared_runners_setting == 'disabled_and_unoverridable'
return redirect_to project_runners_path(@project), alert: _("Cannot enable shared runners because parent group does not allow it")
end
......
# frozen_string_literal: true
module Types
class GroupInvitationType < BaseObject
expose_permissions Types::PermissionTypes::Group
authorize :read_group
implements InvitationInterface
graphql_name 'GroupInvitation'
description 'Represents a Group Invitation'
field :group, Types::GroupType, null: true,
description: 'Group that a User is invited to',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, obj.source_id).find }
end
end
# frozen_string_literal: true
module Types
module InvitationInterface
include BaseInterface
field :email, GraphQL::STRING_TYPE, null: false,
description: 'Email of the member to invite'
field :access_level, Types::AccessLevelType, null: true,
description: 'GitLab::Access level'
field :created_by, Types::UserType, null: true,
description: 'User that authorized membership'
field :created_at, Types::TimeType, null: true,
description: 'Date and time the membership was created'
field :updated_at, Types::TimeType, null: true,
description: 'Date and time the membership was last updated'
field :expires_at, Types::TimeType, null: true,
description: 'Date and time the membership expires'
field :user, Types::UserType, null: true,
description: 'User that is associated with the member object'
definition_methods do
def resolve_type(object, context)
case object
when GroupMember
Types::GroupInvitationType
when ProjectMember
Types::ProjectInvitationType
else
raise ::Gitlab::Graphql::Errors::BaseError, "Unknown member type #{object.class.name}"
end
end
end
end
end
......@@ -62,6 +62,8 @@ module Types
description: 'Number of downvotes the issue has received'
field :user_notes_count, GraphQL::INT_TYPE, null: false,
description: 'Number of user notes of the issue'
field :user_discussions_count, GraphQL::INT_TYPE, null: false,
description: 'Number of user discussions in the issue'
field :web_path, GraphQL::STRING_TYPE, null: false, method: :issue_path,
description: 'Web path of the issue'
field :web_url, GraphQL::STRING_TYPE, null: false,
......@@ -113,6 +115,26 @@ module Types
field :severity, Types::IssuableSeverityEnum, null: true,
description: 'Severity level of the incident'
def user_notes_count
BatchLoader::GraphQL.for(object.id).batch(key: :issue_user_notes_count) do |ids, loader, args|
counts = Note.count_for_collection(ids, 'Issue').index_by(&:noteable_id)
ids.each do |id|
loader.call(id, counts[id]&.count || 0)
end
end
end
def user_discussions_count
BatchLoader::GraphQL.for(object.id).batch(key: :issue_user_discussions_count) do |ids, loader, args|
counts = Note.count_for_collection(ids, 'Issue', 'COUNT(DISTINCT discussion_id) as count').index_by(&:noteable_id)
ids.each do |id|
loader.call(id, counts[id]&.count || 0)
end
end
end
def author
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find
end
......
......@@ -68,6 +68,8 @@ module Types
description: 'SHA of the merge request commit (set once merged)'
field :user_notes_count, GraphQL::INT_TYPE, null: true,
description: 'User notes count of the merge request'
field :user_discussions_count, GraphQL::INT_TYPE, null: true,
description: 'Number of user discussions in the merge request'
field :should_remove_source_branch, GraphQL::BOOLEAN_TYPE, method: :should_remove_source_branch?, null: true,
description: 'Indicates if the source branch of the merge request will be deleted after merge'
field :force_remove_source_branch, GraphQL::BOOLEAN_TYPE, method: :force_remove_source_branch?, null: true,
......@@ -158,17 +160,25 @@ module Types
object.approved_by_users
end
# rubocop: disable CodeReuse/ActiveRecord
def user_notes_count
BatchLoader::GraphQL.for(object.id).batch(key: :merge_request_user_notes_count) do |ids, loader, args|
counts = Note.where(noteable_type: 'MergeRequest', noteable_id: ids).user.group(:noteable_id).count
counts = Note.count_for_collection(ids, 'MergeRequest').index_by(&:noteable_id)
ids.each do |id|
loader.call(id, counts[id] || 0)
loader.call(id, counts[id]&.count || 0)
end
end
end
def user_discussions_count
BatchLoader::GraphQL.for(object.id).batch(key: :merge_request_user_discussions_count) do |ids, loader, args|
counts = Note.count_for_collection(ids, 'MergeRequest', 'COUNT(DISTINCT discussion_id) as count').index_by(&:noteable_id)
ids.each do |id|
loader.call(id, counts[id]&.count || 0)
end
end
end
# rubocop: enable CodeReuse/ActiveRecord
def diff_stats(path: nil)
stats = Array.wrap(object.diff_stats&.to_a)
......
# frozen_string_literal: true
module Types
class ProjectInvitationType < BaseObject
graphql_name 'ProjectInvitation'
description 'Represents a Project Membership Invitation'
expose_permissions Types::PermissionTypes::Project
implements InvitationInterface
authorize :read_project
field :project, Types::ProjectType, null: true,
description: 'Project ID for the project of the invitation'
def project
Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.source_id).find
end
end
end
......@@ -25,12 +25,5 @@ module Types
description: 'HTTP URL of the issues page, filtered by this release and `state=open`'
field :closed_issues_url, GraphQL::STRING_TYPE, null: true,
description: 'HTTP URL of the issues page, filtered by this release and `state=closed`'
field :merge_requests_url, GraphQL::STRING_TYPE, null: true, method: :opened_merge_requests_url,
description: 'HTTP URL of the merge request page filtered by this release',
deprecated: { reason: 'Use `openedMergeRequestsUrl`', milestone: '13.6' }
field :issues_url, GraphQL::STRING_TYPE, null: true, method: :opened_issues_url,
description: 'HTTP URL of the issues page filtered by this release',
deprecated: { reason: 'Use `openedIssuesUrl`', milestone: '13.6' }
end
end
......@@ -160,7 +160,7 @@ module IssuesHelper
can_report_spam: issue.submittable_as_spam_by?(current_user).to_s,
can_update_issue: can?(current_user, :update_issue, issue).to_s,
iid: issue.iid,
is_issue_author: issue.author == current_user,
is_issue_author: (issue.author == current_user).to_s,
new_issue_path: new_project_issue_path(project),
project_path: project.full_path,
report_abuse_path: new_abuse_report_path(user_id: issue.author.id, ref_url: issue_url(issue)),
......
......@@ -30,7 +30,7 @@ module OperationsHelper
'alerts_setup_url' => help_page_path('operations/incident_management/alert_integrations.md', anchor: 'generic-http-endpoint'),
'alerts_usage_url' => project_alert_management_index_path(@project),
'disabled' => disabled.to_s,
'project_path' => project_path(@project)
'project_path' => @project.full_path
}
end
......
......@@ -48,6 +48,8 @@ class ApplicationRecord < ActiveRecord::Base
def self.safe_find_or_create_by!(*args, &block)
safe_find_or_create_by(*args, &block).tap do |record|
raise ActiveRecord::RecordNotFound unless record.present?
record.validate! unless record.persisted?
end
end
......
......@@ -30,7 +30,7 @@ module Clusters
end
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(
helm_command_module::InstallCommand.new(
name: 'certmanager',
repository: repository,
version: VERSION,
......@@ -43,7 +43,7 @@ module Clusters
end
def uninstall_command
Gitlab::Kubernetes::Helm::DeleteCommand.new(
helm_command_module::DeleteCommand.new(
name: 'certmanager',
rbac: cluster.platform_kubernetes_rbac?,
files: files,
......
......@@ -29,7 +29,7 @@ module Clusters
end
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(
helm_command_module::InstallCommand.new(
name: 'crossplane',
repository: repository,
version: VERSION,
......
......@@ -26,7 +26,7 @@ module Clusters
end
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(
helm_command_module::InstallCommand.new(
name: 'elastic-stack',
version: VERSION,
rbac: cluster.platform_kubernetes_rbac?,
......@@ -39,7 +39,7 @@ module Clusters
end
def uninstall_command
Gitlab::Kubernetes::Helm::DeleteCommand.new(
helm_command_module::DeleteCommand.new(
name: 'elastic-stack',
rbac: cluster.platform_kubernetes_rbac?,
files: files,
......@@ -96,7 +96,7 @@ module Clusters
def post_install_script
[
"timeout -t60 sh /data/helm/elastic-stack/config/wait-for-elasticsearch.sh http://elastic-stack-elasticsearch-master:9200"
"timeout 60 sh /data/helm/elastic-stack/config/wait-for-elasticsearch.sh http://elastic-stack-elasticsearch-master:9200"
]
end
......@@ -116,7 +116,7 @@ module Clusters
# Chart version 3.0.0 moves to our own chart at https://gitlab.com/gitlab-org/charts/elastic-stack
# and is not compatible with pre-existing resources. We first remove them.
[
Gitlab::Kubernetes::Helm::DeleteCommand.new(
helm_command_module::DeleteCommand.new(
name: 'elastic-stack',
rbac: cluster.platform_kubernetes_rbac?,
files: files
......
......@@ -30,7 +30,7 @@ module Clusters
end
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(
helm_command_module::InstallCommand.new(
name: 'fluentd',
repository: repository,
version: VERSION,
......
......@@ -4,6 +4,8 @@ require 'openssl'
module Clusters
module Applications
# DEPRECATED: This model represents the Helm 2 Tiller server, and is no longer being actively used.
# It is being kept around for a potential cleanup of the unused Tiller server.
class Helm < ApplicationRecord
self.table_name = 'clusters_applications_helm'
......@@ -49,7 +51,7 @@ module Clusters
end
def install_command
Gitlab::Kubernetes::Helm::InitCommand.new(
Gitlab::Kubernetes::Helm::V2::InitCommand.new(
name: name,
files: files,
rbac: cluster.platform_kubernetes_rbac?
......@@ -57,7 +59,7 @@ module Clusters
end
def uninstall_command
Gitlab::Kubernetes::Helm::ResetCommand.new(
Gitlab::Kubernetes::Helm::V2::ResetCommand.new(
name: name,
files: files,
rbac: cluster.platform_kubernetes_rbac?
......@@ -86,19 +88,19 @@ module Clusters
end
def create_keys_and_certs
ca_cert = Gitlab::Kubernetes::Helm::Certificate.generate_root
ca_cert = Gitlab::Kubernetes::Helm::V2::Certificate.generate_root
self.ca_key = ca_cert.key_string
self.ca_cert = ca_cert.cert_string
end
def tiller_cert
@tiller_cert ||= ca_cert_obj.issue(expires_in: Gitlab::Kubernetes::Helm::Certificate::INFINITE_EXPIRY)
@tiller_cert ||= ca_cert_obj.issue(expires_in: Gitlab::Kubernetes::Helm::V2::Certificate::INFINITE_EXPIRY)
end
def ca_cert_obj
return unless has_ssl?
Gitlab::Kubernetes::Helm::Certificate
Gitlab::Kubernetes::Helm::V2::Certificate
.from_strings(ca_key, ca_cert)
end
end
......
......@@ -62,7 +62,7 @@ module Clusters
end
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(
helm_command_module::InstallCommand.new(
name: name,
repository: repository,
version: VERSION,
......
......@@ -39,7 +39,7 @@ module Clusters
end
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(
helm_command_module::InstallCommand.new(
name: name,
version: VERSION,
rbac: cluster.platform_kubernetes_rbac?,
......
......@@ -70,7 +70,7 @@ module Clusters
end
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(
helm_command_module::InstallCommand.new(
name: name,
version: VERSION,
rbac: cluster.platform_kubernetes_rbac?,
......@@ -94,7 +94,7 @@ module Clusters
end
def uninstall_command
Gitlab::Kubernetes::Helm::DeleteCommand.new(
helm_command_module::DeleteCommand.new(
name: name,
rbac: cluster.platform_kubernetes_rbac?,
files: files,
......
......@@ -67,7 +67,7 @@ module Clusters
end
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(
helm_command_module::InstallCommand.new(
name: name,
repository: repository,
version: VERSION,
......@@ -79,7 +79,7 @@ module Clusters
end
def patch_command(values)
::Gitlab::Kubernetes::Helm::PatchCommand.new(
helm_command_module::PatchCommand.new(
name: name,
repository: repository,
version: version,
......@@ -90,7 +90,7 @@ module Clusters
end
def uninstall_command
Gitlab::Kubernetes::Helm::DeleteCommand.new(
helm_command_module::DeleteCommand.new(
name: name,
rbac: cluster.platform_kubernetes_rbac?,
files: files,
......
......@@ -30,7 +30,7 @@ module Clusters
end
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(
helm_command_module::InstallCommand.new(
name: name,
version: VERSION,
rbac: cluster.platform_kubernetes_rbac?,
......
......@@ -79,6 +79,9 @@ module Clusters
validates :cluster_type, presence: true
validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true }
validates :namespace_per_environment, inclusion: { in: [true, false] }
validates :helm_major_version, inclusion: { in: [2, 3] }
default_value_for :helm_major_version, 3
validate :restrict_modification, on: :update
validate :no_groups, unless: :group_type?
......
......@@ -12,6 +12,17 @@ module Clusters
after_initialize :set_initial_status
def helm_command_module
case cluster.helm_major_version
when 3
Gitlab::Kubernetes::Helm::V3
when 2
Gitlab::Kubernetes::Helm::V2
else
raise "Invalid Helm major version"
end
end
def set_initial_status
return unless not_installable?
......
......@@ -4,7 +4,7 @@ module Clusters
module Concerns
module ApplicationData
def uninstall_command
Gitlab::Kubernetes::Helm::DeleteCommand.new(
helm_command_module::DeleteCommand.new(
name: name,
rbac: cluster.platform_kubernetes_rbac?,
files: files
......
......@@ -37,27 +37,6 @@ module FromUnion
# rubocop: disable Gitlab/Union
extend FromSetOperator
define_set_operator Gitlab::SQL::Union
alias_method :from_union_set_operator, :from_union
def from_union(members, remove_duplicates: true, alias_as: table_name)
if Feature.enabled?(:sql_set_operators)
from_union_set_operator(members, remove_duplicates: remove_duplicates, alias_as: alias_as)
else
# The original from_union method.
standard_from_union(members, remove_duplicates: remove_duplicates, alias_as: alias_as)
end
end
private
def standard_from_union(members, remove_duplicates: true, alias_as: table_name)
union = Gitlab::SQL::Union
.new(members, remove_duplicates: remove_duplicates)
.to_sql
from(Arel.sql("(#{union}) #{alias_as}"))
end
# rubocop: enable Gitlab/Union
end
end
......@@ -79,8 +79,6 @@ class Deployment < ApplicationRecord
after_transition any => :running do |deployment|
deployment.run_after_commit do
next unless Feature.enabled?(:ci_send_deployment_hook_when_start, deployment.project)
Deployments::ExecuteHooksWorker.perform_async(id)
end
end
......
......@@ -16,6 +16,7 @@ class Discussion
:commit_id,
:confidential?,
:for_commit?,
:for_design?,
:for_merge_request?,
:noteable_ability_name,
:to_ability_name,
......
......@@ -109,6 +109,8 @@ class Group < Namespace
.where("project_authorizations.user_id IN (?)", user_ids)
end
delegate :default_branch_name, to: :namespace_settings
class << self
def sort_by_attribute(method)
if method == 'storage_size_desc'
......@@ -587,7 +589,7 @@ class Group < Namespace
def update_two_factor_requirement
return unless saved_change_to_require_two_factor_authentication? || saved_change_to_two_factor_grace_period?
members_with_descendants.find_each(&:update_two_factor_requirement)
direct_and_indirect_members.find_each(&:update_two_factor_requirement)
end
def path_changed_hook
......
......@@ -96,6 +96,8 @@ class Member < ApplicationRecord
scope :owners, -> { active.where(access_level: OWNER) }
scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) }
scope :with_user, -> (user) { where(user: user) }
scope :with_user_by_email, -> (email) { left_join_users.where(users: { email: email } ) }
scope :preload_user_and_notification_settings, -> { preload(user: :notification_settings) }
scope :with_source_id, ->(source_id) { where(source_id: source_id) }
......
......@@ -393,7 +393,6 @@ class Namespace < ApplicationRecord
end
def changing_shared_runners_enabled_is_allowed
return unless Feature.enabled?(:disable_shared_runners_on_group, default_enabled: true)
return unless new_record? || changes.has_key?(:shared_runners_enabled)
if shared_runners_enabled && has_parent? && parent.shared_runners_setting == 'disabled_and_unoverridable'
......@@ -402,7 +401,6 @@ class Namespace < ApplicationRecord
end
def changing_allow_descendants_override_disabled_shared_runners_is_allowed
return unless Feature.enabled?(:disable_shared_runners_on_group, default_enabled: true)
return unless new_record? || changes.has_key?(:allow_descendants_override_disabled_shared_runners)
if shared_runners_enabled && !new_record?
......
......@@ -6,10 +6,18 @@ class NamespaceSetting < ApplicationRecord
validate :default_branch_name_content
validate :allow_mfa_for_group
before_validation :normalize_default_branch_name
NAMESPACE_SETTINGS_PARAMS = [:default_branch_name].freeze
self.primary_key = :namespace_id
private
def normalize_default_branch_name
self.default_branch_name = nil if default_branch_name.blank?
end
def default_branch_name_content
return if default_branch_name.nil?
......
......@@ -197,8 +197,8 @@ class Note < ApplicationRecord
.map(&:position)
end
def count_for_collection(ids, type)
user.select('noteable_id', 'COUNT(*) as count')
def count_for_collection(ids, type, count_column = 'COUNT(*) as count')
user.select(:noteable_id, count_column)
.group(:noteable_id)
.where(noteable_type: type, noteable_id: ids)
end
......
......@@ -286,7 +286,7 @@ class PagesDomain < ApplicationRecord
return unless domain
if domain.downcase.ends_with?(Settings.pages.host.downcase)
self.errors.add(:domain, "*.#{Settings.pages.host} is restricted")
self.errors.add(:domain, "*.#{Settings.pages.host} is restricted. Please compare our documentation at https://docs.gitlab.com/ee/administration/pages/#advanced-configuration against your configuration.")
end
end
......
......@@ -1195,7 +1195,6 @@ class Project < ApplicationRecord
end
def changing_shared_runners_enabled_is_allowed
return unless Feature.enabled?(:disable_shared_runners_on_group, default_enabled: true)
return unless new_record? || changes.has_key?(:shared_runners_enabled)
if shared_runners_enabled && group && group.shared_runners_setting == 'disabled_and_unoverridable'
......
......@@ -7,13 +7,15 @@ class NotePolicy < BasePolicy
delegate { @subject.noteable if DeclarativePolicy.has_policy?(@subject.noteable) }
condition(:is_author) { @user && @subject.author == @user }
condition(:is_noteable_author) { @user && @subject.noteable.author_id == @user.id }
condition(:is_noteable_author) { @user && @subject.noteable.try(:author_id) == @user.id }
condition(:editable, scope: :subject) { @subject.editable? }
condition(:can_read_noteable) { can?(:"read_#{@subject.noteable_ability_name}") }
condition(:commit_is_deleted) { @subject.for_commit? && @subject.noteable.blank? }
condition(:for_design) { @subject.for_design? }
condition(:is_visible) { @subject.system_note_with_references_visible_for?(@user) }
condition(:confidential, scope: :subject) { @subject.confidential? }
......@@ -28,6 +30,7 @@ class NotePolicy < BasePolicy
rule { ~can_read_noteable }.policy do
prevent :admin_note
prevent :resolve_note
prevent :reposition_note
prevent :award_emoji
end
......@@ -46,6 +49,7 @@ class NotePolicy < BasePolicy
prevent :read_note
prevent :admin_note
prevent :resolve_note
prevent :reposition_note
prevent :award_emoji
end
......@@ -57,9 +61,14 @@ class NotePolicy < BasePolicy
prevent :read_note
prevent :admin_note
prevent :resolve_note
prevent :reposition_note
prevent :award_emoji
end
rule { can?(:admin_note) | (for_design & can?(:create_note)) }.policy do
enable :reposition_note
end
def parent_namespace
strong_memoize(:parent_namespace) do
next if @subject.is_a?(PersonalSnippet)
......
# frozen_string_literal: true
class InvitationPresenter < Gitlab::View::Presenter::Delegated
presents :invitation
end
......@@ -163,16 +163,18 @@ module Ci
end
def ensure_pending_state
Ci::BuildPendingState.create_or_find_by!(
build_state = Ci::BuildPendingState.safe_find_or_create_by(
build_id: build.id,
state: params.fetch(:state),
trace_checksum: params.fetch(:checksum),
failure_reason: params.dig(:failure_reason)
)
rescue ActiveRecord::RecordNotFound
metrics.increment_trace_operation(operation: :conflict)
build.pending_state
unless build_state.present?
metrics.increment_trace_operation(operation: :conflict)
end
build_state || build.pending_state
end
##
......
......@@ -7,7 +7,7 @@ module Clusters
GITLAB_ADMIN_TOKEN_NAME = 'gitlab-token'
GITLAB_CLUSTER_ROLE_BINDING_NAME = 'gitlab-admin'
GITLAB_CLUSTER_ROLE_NAME = 'cluster-admin'
PROJECT_CLUSTER_ROLE_NAME = 'edit'
PROJECT_CLUSTER_ROLE_NAME = 'admin'
GITLAB_KNATIVE_SERVING_ROLE_NAME = 'gitlab-knative-serving-role'
GITLAB_KNATIVE_SERVING_ROLE_BINDING_NAME = 'gitlab-knative-serving-rolebinding'
GITLAB_CROSSPLANE_DATABASE_ROLE_NAME = 'gitlab-crossplane-database-role'
......
......@@ -123,11 +123,9 @@ module Clusters
end
def role_binding_resource
role_name = Feature.enabled?(:kubernetes_cluster_namespace_role_admin) ? 'admin' : Clusters::Kubernetes::PROJECT_CLUSTER_ROLE_NAME
Gitlab::Kubernetes::RoleBinding.new(
name: role_binding_name,
role_name: role_name,
role_name: Clusters::Kubernetes::PROJECT_CLUSTER_ROLE_NAME,
role_kind: :ClusterRole,
namespace: service_account_namespace,
service_account_name: service_account_name
......
# frozen_string_literal: true
module Members
class InviteService < Members::BaseService
DEFAULT_LIMIT = 100
attr_reader :errors
def initialize(current_user, params)
@current_user, @params = current_user, params.dup
@errors = {}
end
def execute(source)
return error(s_('Email cannot be blank')) if params[:email].blank?
emails = params[:email].split(',').uniq.flatten
return error(s_("Too many users specified (limit is %{user_limit})") % { user_limit: user_limit }) if
user_limit && emails.size > user_limit
emails.each do |email|
next if existing_member?(source, email)
next if existing_invite?(source, email)
if existing_user?(email)
add_existing_user_as_member(current_user, source, params, email)
next
end
invite_new_member_and_user(current_user, source, params, email)
end
return success unless errors.any?
error(errors)
end
private
def invite_new_member_and_user(current_user, source, params, email)
new_member = (source.class.name + 'Member').constantize.create(source_id: source.id,
user_id: nil,
access_level: params[:access_level],
invite_email: email,
created_by_id: current_user.id,
expires_at: params[:expires_at],
requested_at: Time.current.utc)
unless new_member.valid? && new_member.persisted?
errors[params[:email]] = new_member.errors.full_messages.to_sentence
end
end
def add_existing_user_as_member(current_user, source, params, email)
new_member = create_member(current_user, existing_user(email), source, params.merge({ invite_email: email }))
unless new_member.valid? && new_member.persisted?
errors[email] = new_member.errors.full_messages.to_sentence
end
end
def create_member(current_user, user, source, params)
source.add_user(user, params[:access_level], current_user: current_user, expires_at: params[:expires_at])
end
def user_limit
limit = params.fetch(:limit, DEFAULT_LIMIT)
limit && limit < 0 ? nil : limit
end
def existing_member?(source, email)
existing_member = source.members.with_user_by_email(email).exists?
if existing_member
errors[email] = "Already a member of #{source.name}"
return true
end
false
end
def existing_invite?(source, email)
existing_invite = source.members.search_invite_email(email).exists?
if existing_invite
errors[email] = "Member already invited to #{source.name}"
return true
end
false
end
def existing_user(email)
User.find_by_email(email)
end
def existing_user?(email)
existing_user(email).present?
end
end
end
......@@ -9,7 +9,7 @@ module Packages
def execute
if @tag_name.present?
@tag_name.match(Gitlab::Regex.composer_package_version_regex).captures[0]
@tag_name.delete_prefix('v')
elsif @branch_name.present?
branch_sufix_or_prefix(@branch_name.match(Gitlab::Regex.composer_package_version_regex))
end
......
......@@ -16,6 +16,7 @@ module Search
Gitlab::SearchResults.new(current_user,
params[:search],
projects,
order_by: params[:order_by],
sort: params[:sort],
filters: { state: params[:state], confidential: params[:confidential] })
end
......
......@@ -16,6 +16,7 @@ module Search
params[:search],
projects,
group: group,
order_by: params[:order_by],
sort: params[:sort],
filters: { state: params[:state], confidential: params[:confidential] }
)
......
......@@ -17,6 +17,7 @@ module Search
params[:search],
project: project,
repository_ref: params[:repository_ref],
order_by: params[:order_by],
sort: params[:sort],
filters: { confidential: params[:confidential], state: params[:state] }
)
......
- breadcrumb_title _("Dashboard")
- page_title _("Dashboard")
- billable_users_url = help_page_path('subscriptions/self_managed/index', anchor: 'billable-users')
- billable_users_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer nofollow">'.html_safe % { url: billable_users_url }
- if @notices
- @notices.each do |notice|
......@@ -8,7 +10,9 @@
= notice[:message].html_safe
- if @license.present? && show_license_breakdown?
= render_if_exists 'admin/licenses/breakdown'
.license-panel.gl-mt-5
= render_if_exists 'admin/licenses/summary'
= render_if_exists 'admin/licenses/breakdown'
.admin-dashboard.gl-mt-3
.row
......@@ -22,10 +26,20 @@
= link_to(s_('AdminArea|New project'), new_project_path, class: "btn gl-button btn-success gl-w-full")
.col-sm-4
.info-well.dark-well
.well-segment.well-centered
.well-segment.well-centered.gl-text-center
= link_to admin_users_path do
%h3.text-center
%h3.gl-display-inline-block.gl-mb-0
= s_('AdminArea|Users: %{number_of_users}') % { number_of_users: approximate_count_with_delimiters(@counts, User) }
%span.gl-outline-0.gl-ml-2{ href: "#", tabindex: "0", data: { container: "body",
toggle: "popover",
placement: "top",
html: "true",
trigger: "focus",
content: s_("AdminArea|All users created in the instance, including users who are not %{billable_users_link_start}billable users%{billable_users_link_end}.").html_safe % { billable_users_link_start: billable_users_link_start, billable_users_link_end: '</a>'.html_safe },
} }
= sprite_icon('question', size: 16, css_class: 'gl-text-gray-700 gl-mb-1')
%hr
.btn-group.d-flex{ role: 'group' }
= link_to s_('AdminArea|New user'), new_admin_user_path, class: "btn gl-button btn-success gl-w-full"
......
......@@ -4,17 +4,7 @@
.container
.gl-mt-3
- if Feature.enabled?(:devops_adoption)
%h2
= _('DevOps Report')
%ul.nav-links.nav-tabs.nav.js-devops-tabs{ role: 'tablist' }
= render 'tab', active: true, title: _('DevOps Score'), target: '#devops_score_pane'
= render 'tab', active: false, title: _('Adoption'), target: '#devops_adoption_pane'
.tab-content
.tab-pane.active#devops_score_pane
= render 'report'
.tab-pane#devops_adoption_pane
.js-devops-adoption{ data: { empty_state_svg_path: image_path('illustrations/monitoring/getting_started.svg') } }
= render_if_exists 'admin/dev_ops_report/devops_tabs'
- else
= render 'report'
%section.settings.as-default-branch-name.no-animate#js-default-branch-name{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
= _('Default initial branch name')
%button.gl-button.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Set the default name of the initial branch when creating new repositories through the user interface.')
.settings-content
= form_for @group, url: group_path(@group, anchor: 'js-default-branch-name'), html: { class: 'fieldset-form' } do |f|
= form_errors(@group)
- fallback_branch_name = '<code>master</code>'
%fieldset
.form-group
= f.label :default_branch_name, _('Default initial branch name'), class: 'label-light'
= f.text_field :default_branch_name, value: group.namespace_settings&.default_branch_name, placeholder: 'master', class: 'form-control'
%span.form-text.text-muted
= (_("Changes affect new repositories only. If not specified, either the configured application-wide default or Git's default name %{branch_name_default} will be used.") % { branch_name_default: fallback_branch_name }).html_safe
= f.hidden_field :redirect_target, value: "repository_settings"
= f.submit _('Save changes'), class: 'gl-button btn-success'
......@@ -4,3 +4,4 @@
- deploy_token_description = s_('DeployTokens|Group deploy tokens allow access to the packages, repositories, and registry images within the group.')
= render "shared/deploy_tokens/index", group_or_project: @group, description: deploy_token_description
= render "initial_branch_name", group: @group
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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