Commit 17bc419a authored by Kerri Miller's avatar Kerri Miller

Merge branch '241194-feature-flag-rollout-of-ee-feature-specs' into 'master'

Update EE specs related to group members view

See merge request gitlab-org/gitlab!46557
parents eafbbab3 59840ab1
...@@ -16,7 +16,7 @@ inherit_mode: ...@@ -16,7 +16,7 @@ inherit_mode:
- Include - Include
AllCops: AllCops:
TargetRubyVersion: 2.6 TargetRubyVersion: 2.7
TargetRailsVersion: 6.0 TargetRailsVersion: 6.0
Exclude: Exclude:
- 'vendor/**/*' - 'vendor/**/*'
......
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
## 13.5.3 (2020-11-03)
- No changes.
## 13.5.2 (2020-11-02) ## 13.5.2 (2020-11-02)
### Security (4 changes) ### Security (4 changes)
...@@ -230,6 +234,13 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -230,6 +234,13 @@ Please view this file on the master branch, on stable branches it's out of date.
- Remove bootstrap class in licensed user count. !45443 - Remove bootstrap class in licensed user count. !45443
## 13.4.6 (2020-11-03)
### Fixed (1 change)
- Handle 500 error for GraphQL mutation. !43936
## 13.4.5 (2020-11-02) ## 13.4.5 (2020-11-02)
### Security (4 changes) ### Security (4 changes)
......
...@@ -2,6 +2,19 @@ ...@@ -2,6 +2,19 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 13.5.3 (2020-11-03)
### Fixed (3 changes)
- Fix IDE issues with special characters. !46398
- Ensure that copy to clipboard button is visible. !46466
- Auto Deploy: fixes issues for fetching other charts from stable repo. !46531
### Added (1 change)
- Add environment variables to override backup/restore DB settings. !45855
## 13.5.2 (2020-11-02) ## 13.5.2 (2020-11-02)
### Security (9 changes) ### Security (9 changes)
...@@ -598,6 +611,17 @@ entry. ...@@ -598,6 +611,17 @@ entry.
- Bump cluster applications CI template. !45472 - Bump cluster applications CI template. !45472
## 13.4.6 (2020-11-03)
### Fixed (1 change)
- Auto Deploy: fixes issues for fetching other charts from stable repo. !46531
### Other (1 change)
- GitLab-managed apps: Use GitLab's repo as replacement for the Helm stable repo. !44875
## 13.4.5 (2020-11-02) ## 13.4.5 (2020-11-02)
### Security (9 changes) ### Security (9 changes)
......
940a45ca938b20031820a4976f936a5b6173de92 fa974a4ab21aa6acc4c3a00456265248a4d70703
import Vue from 'vue'; // EE-specific feature. Find the implementation in the `ee/`-folder
import DevopsAdoptionApp from './components/devops_adoption_app.vue'; export default () => {};
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);
},
});
};
<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 { ...@@ -81,7 +81,6 @@ export default {
<div class="incident-management-list"> <div class="incident-management-list">
<h5 class="gl-font-lg">{{ $options.i18n.title }}</h5> <h5 class="gl-font-lg">{{ $options.i18n.title }}</h5>
<gl-table <gl-table
:empty-text="$options.i18n.emptyState"
:items="integrations" :items="integrations"
:fields="$options.fields" :fields="$options.fields"
:busy="loading" :busy="loading"
...@@ -115,6 +114,14 @@ export default { ...@@ -115,6 +114,14 @@ export default {
<template #table-busy> <template #table-busy>
<gl-loading-icon size="lg" color="dark" class="mt-3" /> <gl-loading-icon size="lg" color="dark" class="mt-3" />
</template> </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> </gl-table>
</div> </div>
</template> </template>
...@@ -56,7 +56,7 @@ export default { ...@@ -56,7 +56,7 @@ export default {
data() { data() {
return { return {
loading: false, loading: false,
selectedIntegration: integrationTypes[1].value, selectedIntegration: integrationTypes[0].value,
options: integrationTypes, options: integrationTypes,
active: false, active: false,
authKey: '', authKey: '',
...@@ -88,34 +88,34 @@ export default { ...@@ -88,34 +88,34 @@ export default {
]; ];
}, },
isPrometheus() { isPrometheus() {
return this.selectedIntegration === 'prometheus'; return this.selectedIntegration === 'PROMETHEUS';
}, },
isOpsgenie() { isOpsgenie() {
return this.selectedIntegration === 'opsgenie'; return this.selectedIntegration === 'OPSGENIE';
}, },
selectedIntegrationType() { selectedIntegrationType() {
switch (this.selectedIntegration) { switch (this.selectedIntegration) {
case 'generic': { case 'HTTP': {
return { return {
url: this.generic.url, url: this.generic.url,
authKey: this.generic.authorizationKey, authKey: this.generic.authKey,
activated: this.generic.activated, active: this.generic.active,
resetKey: this.resetKey.bind(this), resetKey: this.resetKey.bind(this),
}; };
} }
case 'prometheus': { case 'PROMETHEUS': {
return { return {
url: this.prometheus.prometheusUrl, url: this.prometheus.url,
authKey: this.prometheus.authorizationKey, authKey: this.prometheus.authKey,
activated: this.prometheus.activated, active: this.prometheus.active,
resetKey: this.resetKey.bind(this, 'prometheus'), resetKey: this.resetKey.bind(this, 'PROMETHEUS'),
targetUrl: this.prometheus.prometheusApiUrl, targetUrl: this.prometheus.prometheusApiUrl,
}; };
} }
case 'opsgenie': { case 'OPSGENIE': {
return { return {
targetUrl: this.opsgenie.opsgenieMvcTargetUrl, targetUrl: this.opsgenie.opsgenieMvcTargetUrl,
activated: this.opsgenie.activated, active: this.opsgenie.active,
}; };
} }
default: { default: {
...@@ -161,16 +161,12 @@ export default { ...@@ -161,16 +161,12 @@ export default {
}, },
}, },
mounted() { mounted() {
if ( if (this.prometheus.active || this.generic.active || !this.opsgenie.opsgenieMvcIsAvailable) {
this.prometheus.activated ||
this.generic.activated ||
!this.opsgenie.opsgenieMvcIsAvailable
) {
this.removeOpsGenieOption(); this.removeOpsGenieOption();
} else if (this.opsgenie.activated) { } else if (this.opsgenie.active) {
this.setOpsgenieAsDefault(); this.setOpsgenieAsDefault();
} }
this.active = this.selectedIntegrationType.activated; this.active = this.selectedIntegrationType.active;
this.authKey = this.selectedIntegrationType.authKey ?? ''; this.authKey = this.selectedIntegrationType.authKey ?? '';
}, },
methods: { methods: {
...@@ -183,19 +179,19 @@ export default { ...@@ -183,19 +179,19 @@ export default {
}, },
setOpsgenieAsDefault() { setOpsgenieAsDefault() {
this.options = this.options.map(el => { this.options = this.options.map(el => {
if (el.value !== 'opsgenie') { if (el.value !== 'OPSGENIE') {
return { ...el, disabled: true }; return { ...el, disabled: true };
} }
return { ...el, disabled: false }; 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) { if (this.targetUrl === null) {
this.targetUrl = this.selectedIntegrationType.targetUrl; this.targetUrl = this.selectedIntegrationType.targetUrl;
} }
}, },
removeOpsGenieOption() { removeOpsGenieOption() {
this.options = this.options.map(el => { this.options = this.options.map(el => {
if (el.value !== 'opsgenie') { if (el.value !== 'OPSGENIE') {
return { ...el, disabled: false }; return { ...el, disabled: false };
} }
return { ...el, disabled: true }; return { ...el, disabled: true };
...@@ -204,7 +200,7 @@ export default { ...@@ -204,7 +200,7 @@ export default {
resetFormValues() { resetFormValues() {
this.testAlert.json = null; this.testAlert.json = null;
this.targetUrl = this.selectedIntegrationType.targetUrl; this.targetUrl = this.selectedIntegrationType.targetUrl;
this.active = this.selectedIntegrationType.activated; this.active = this.selectedIntegrationType.active;
}, },
dismissFeedback() { dismissFeedback() {
this.serverError = null; this.serverError = null;
...@@ -212,7 +208,7 @@ export default { ...@@ -212,7 +208,7 @@ export default {
this.isFeedbackDismissed = false; this.isFeedbackDismissed = false;
}, },
resetKey(key) { resetKey(key) {
const fn = key === 'prometheus' ? this.resetPrometheusKey() : this.resetGenericKey(); const fn = key === 'PROMETHEUS' ? this.resetPrometheusKey() : this.resetGenericKey();
return fn return fn
.then(({ data: { token } }) => { .then(({ data: { token } }) => {
...@@ -242,9 +238,10 @@ export default { ...@@ -242,9 +238,10 @@ export default {
}, },
toggleActivated(value) { toggleActivated(value) {
this.loading = true; this.loading = true;
const path = this.isOpsgenie ? this.opsgenie.formPath : this.generic.formPath;
return service return service
.updateGenericActive({ .updateGenericActive({
endpoint: this[this.selectedIntegration].formPath, endpoint: path,
params: this.isOpsgenie params: this.isOpsgenie
? { service: { opsgenie_mvc_target_url: this.targetUrl, opsgenie_mvc_enabled: value } } ? { service: { opsgenie_mvc_target_url: this.targetUrl, opsgenie_mvc_enabled: value } }
: { service: { active: value } }, : { service: { active: value } },
...@@ -345,7 +342,7 @@ export default { ...@@ -345,7 +342,7 @@ export default {
if (this.canSaveForm) { if (this.canSaveForm) {
this.canSaveForm = false; this.canSaveForm = false;
this.active = this.selectedIntegrationType.activated; this.active = this.selectedIntegrationType.active;
} }
}, },
}, },
...@@ -402,9 +399,9 @@ export default { ...@@ -402,9 +399,9 @@ export default {
</gl-sprintf> </gl-sprintf>
</span> </span>
</gl-form-group> </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 <toggle-button
id="activated" id="active"
:disabled-input="loading" :disabled-input="loading"
:is-loading="loading" :is-loading="loading"
:value="active" :value="active"
......
<script> <script>
import produce from 'immer';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { fetchPolicies } from '~/lib/graphql'; import { fetchPolicies } from '~/lib/graphql';
import createFlash, { FLASH_TYPES } from '~/flash';
import getIntegrationsQuery from '../graphql/queries/get_integrations.query.graphql'; 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 IntegrationsList from './alerts_integrations_list.vue';
import SettingsFormOld from './alerts_settings_form_old.vue'; import SettingsFormOld from './alerts_settings_form_old.vue';
import SettingsFormNew from './alerts_settings_form_new.vue'; import SettingsFormNew from './alerts_settings_form_new.vue';
import { typeSet } from '../constants';
export default { 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: { components: {
IntegrationsList, IntegrationsList,
SettingsFormOld, SettingsFormOld,
...@@ -49,6 +60,7 @@ export default { ...@@ -49,6 +60,7 @@ export default {
data() { data() {
return { return {
errored: false, errored: false,
isUpdating: false,
integrations: {}, integrations: {},
}; };
}, },
...@@ -61,16 +73,85 @@ export default { ...@@ -61,16 +73,85 @@ export default {
{ {
name: s__('AlertSettings|HTTP endpoint'), name: s__('AlertSettings|HTTP endpoint'),
type: s__('AlertsIntegrations|HTTP endpoint'), type: s__('AlertsIntegrations|HTTP endpoint'),
active: this.generic.activated, active: this.generic.active,
}, },
{ {
name: s__('AlertSettings|External Prometheus'), name: s__('AlertSettings|External Prometheus'),
type: s__('AlertsIntegrations|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> </script>
...@@ -80,7 +161,11 @@ export default { ...@@ -80,7 +161,11 @@ export default {
:integrations="glFeatures.httpIntegrationsList ? integrations.list : intergrationsOptionsOld" :integrations="glFeatures.httpIntegrationsList ? integrations.list : intergrationsOptionsOld"
:loading="loading" :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 /> <settings-form-old v-else />
</div> </div>
</template> </template>
import { s__ } from '~/locale'; import { s__ } from '~/locale';
// TODO: Remove this as part of the form old removal
export const i18n = { export const i18n = {
usageSection: s__( 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.', '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 = { ...@@ -39,13 +40,23 @@ export const i18n = {
integration: s__('AlertSettings|Integration'), integration: s__('AlertSettings|Integration'),
}; };
// TODO: Delete as part of old form removal in 13.6
export const integrationTypes = [ 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: '', text: s__('AlertSettings|Select integration type') },
{ value: 'generic', text: s__('AlertSettings|HTTP Endpoint') }, ...integrationTypes,
{ value: 'prometheus', text: s__('AlertSettings|External Prometheus') },
{ value: 'opsgenie', text: s__('AlertSettings|Opsgenie') },
]; ];
export const typeSet = {
http: 'HTTP',
prometheus: 'PROMETHEUS',
};
export const JSON_VALIDATE_DELAY = 250; export const JSON_VALIDATE_DELAY = 250;
export const targetPrometheusUrlPlaceholder = 'http://prometheus.example.com/'; 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 => { ...@@ -48,9 +48,9 @@ export default el => {
el, el,
provide: { provide: {
prometheus: { prometheus: {
activated: parseBoolean(prometheusActivated), active: parseBoolean(prometheusActivated),
prometheusUrl, url: prometheusUrl,
authorizationKey: prometheusAuthorizationKey, authKey: prometheusAuthorizationKey,
prometheusFormPath, prometheusFormPath,
prometheusResetKeyPath, prometheusResetKeyPath,
prometheusApiUrl, prometheusApiUrl,
...@@ -58,14 +58,14 @@ export default el => { ...@@ -58,14 +58,14 @@ export default el => {
generic: { generic: {
alertsSetupUrl, alertsSetupUrl,
alertsUsageUrl, alertsUsageUrl,
activated: parseBoolean(activatedStr), active: parseBoolean(activatedStr),
formPath, formPath,
authorizationKey, authKey: authorizationKey,
url, url,
}, },
opsgenie: { opsgenie: {
formPath: opsgenieMvcFormPath, formPath: opsgenieMvcFormPath,
activated: parseBoolean(opsgenieMvcEnabled), active: parseBoolean(opsgenieMvcEnabled),
opsgenieMvcTargetUrl, opsgenieMvcTargetUrl,
opsgenieMvcIsAvailable: parseBoolean(opsgenieMvcAvailable), opsgenieMvcIsAvailable: parseBoolean(opsgenieMvcAvailable),
}, },
......
...@@ -70,6 +70,7 @@ const Api = { ...@@ -70,6 +70,7 @@ const Api = {
featureFlagUserLists: '/api/:version/projects/:id/feature_flags_user_lists', featureFlagUserLists: '/api/:version/projects/:id/feature_flags_user_lists',
featureFlagUserList: '/api/:version/projects/:id/feature_flags_user_lists/:list_iid', featureFlagUserList: '/api/:version/projects/:id/feature_flags_user_lists/:list_iid',
billableGroupMembersPath: '/api/:version/groups/:id/billable_members', billableGroupMembersPath: '/api/:version/groups/:id/billable_members',
containerRegistryDetailsPath: '/api/:version/registry/repositories/:id/',
group(groupId, callback = () => {}) { group(groupId, callback = () => {}) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId); const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
...@@ -106,6 +107,11 @@ const Api = { ...@@ -106,6 +107,11 @@ const Api = {
return axios.delete(url); return axios.delete(url);
}, },
containerRegistryDetails(registryId, options = {}) {
const url = Api.buildUrl(this.containerRegistryDetailsPath).replace(':id', registryId);
return axios.get(url, options);
},
groupMembers(id, options) { groupMembers(id, options) {
const url = Api.buildUrl(this.groupMembersPath).replace(':id', encodeURIComponent(id)); const url = Api.buildUrl(this.groupMembersPath).replace(':id', encodeURIComponent(id));
......
...@@ -12,11 +12,19 @@ import { getLocationHash } from '../lib/utils/url_utility'; ...@@ -12,11 +12,19 @@ import { getLocationHash } from '../lib/utils/url_utility';
$(() => { $(() => {
function toggleContainer(container, toggleState) { function toggleContainer(container, toggleState) {
const $container = $(container); const $container = $(container);
const isExpanded = $container.data('is-expanded');
$container const $collapseIcon = $container.find('.js-sidebar-collapse');
.find('.js-toggle-button .fa-chevron-up, .js-toggle-button .fa-chevron-down') const $expandIcon = $container.find('.js-sidebar-expand');
.toggleClass('fa-chevron-up', toggleState)
.toggleClass('fa-chevron-down', toggleState !== undefined ? !toggleState : undefined); if (isExpanded && !toggleState) {
$container.data('is-expanded', false);
$collapseIcon.addClass('hidden');
$expandIcon.removeClass('hidden');
} else {
$container.data('is-expanded', true);
$expandIcon.addClass('hidden');
$collapseIcon.removeClass('hidden');
}
$container.find('.js-toggle-content').toggle(toggleState); $container.find('.js-toggle-content').toggle(toggleState);
} }
......
...@@ -168,9 +168,6 @@ export default class CreateMergeRequestDropdown { ...@@ -168,9 +168,6 @@ export default class CreateMergeRequestDropdown {
disable() { disable() {
this.disableCreateAction(); this.disableCreateAction();
this.dropdownToggle.classList.add('disabled');
this.dropdownToggle.setAttribute('disabled', 'disabled');
} }
disableCreateAction() { disableCreateAction() {
...@@ -189,9 +186,6 @@ export default class CreateMergeRequestDropdown { ...@@ -189,9 +186,6 @@ export default class CreateMergeRequestDropdown {
this.createTargetButton.classList.remove('disabled'); this.createTargetButton.classList.remove('disabled');
this.createTargetButton.removeAttribute('disabled'); this.createTargetButton.removeAttribute('disabled');
this.dropdownToggle.classList.remove('disabled');
this.dropdownToggle.removeAttribute('disabled');
} }
static findByValue(objects, ref, returnFirstMatch = false) { static findByValue(objects, ref, returnFirstMatch = false) {
......
#import "../fragments/design.fragment.graphql" #import "../fragments/design.fragment.graphql"
#import "~/graphql_shared/fragments/author.fragment.graphql" #import "~/graphql_shared/fragments/author.fragment.graphql"
query getDesign($fullPath: ID!, $iid: String!, $atVersion: ID, $filenames: [String!]) { query getDesign(
$fullPath: ID!
$iid: String!
$atVersion: DesignManagementVersionID
$filenames: [String!]
) {
project(fullPath: $fullPath) { project(fullPath: $fullPath) {
id id
issue(iid: $iid) { issue(iid: $iid) {
......
...@@ -626,7 +626,7 @@ export function switchToFullDiffFromRenamedFile({ commit, dispatch, state }, { d ...@@ -626,7 +626,7 @@ export function switchToFullDiffFromRenamedFile({ commit, dispatch, state }, { d
.then(({ data }) => { .then(({ data }) => {
const lines = data.map((line, index) => const lines = data.map((line, index) =>
prepareLineForRenamedFile({ prepareLineForRenamedFile({
diffViewType: state.diffViewType, diffViewType: window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType,
line, line,
diffFile, diffFile,
index, index,
...@@ -638,6 +638,7 @@ export function switchToFullDiffFromRenamedFile({ commit, dispatch, state }, { d ...@@ -638,6 +638,7 @@ export function switchToFullDiffFromRenamedFile({ commit, dispatch, state }, { d
viewer: { viewer: {
...diffFile.alternate_viewer, ...diffFile.alternate_viewer,
automaticallyCollapsed: false, automaticallyCollapsed: false,
manuallyCollapsed: false,
}, },
}); });
commit(types.SET_CURRENT_VIEW_DIFF_FILE_LINES, { filePath: diffFile.file_path, lines }); commit(types.SET_CURRENT_VIEW_DIFF_FILE_LINES, { filePath: diffFile.file_path, lines });
......
...@@ -378,8 +378,13 @@ export default { ...@@ -378,8 +378,13 @@ export default {
}, },
[types.SET_CURRENT_VIEW_DIFF_FILE_LINES](state, { filePath, lines }) { [types.SET_CURRENT_VIEW_DIFF_FILE_LINES](state, { filePath, lines }) {
const file = state.diffFiles.find(f => f.file_path === filePath); const file = state.diffFiles.find(f => f.file_path === filePath);
const currentDiffLinesKey = let currentDiffLinesKey;
state.diffViewType === 'inline' ? 'highlighted_diff_lines' : 'parallel_diff_lines';
if (window.gon?.features?.unifiedDiffLines || state.diffViewType === 'inline') {
currentDiffLinesKey = 'highlighted_diff_lines';
} else {
currentDiffLinesKey = 'parallel_diff_lines';
}
file[currentDiffLinesKey] = lines; file[currentDiffLinesKey] = lines;
}, },
......
...@@ -8,9 +8,9 @@ import { ...@@ -8,9 +8,9 @@ import {
GlBadge, GlBadge,
GlAlert, GlAlert,
GlSprintf, GlSprintf,
GlDeprecatedDropdown, GlDropdown,
GlDeprecatedDropdownItem, GlDropdownItem,
GlDeprecatedDropdownDivider, GlDropdownDivider,
GlIcon, GlIcon,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
...@@ -43,9 +43,9 @@ export default { ...@@ -43,9 +43,9 @@ export default {
GlBadge, GlBadge,
GlAlert, GlAlert,
GlSprintf, GlSprintf,
GlDeprecatedDropdown, GlDropdown,
GlDeprecatedDropdownItem, GlDropdownItem,
GlDeprecatedDropdownDivider, GlDropdownDivider,
TimeAgoTooltip, TimeAgoTooltip,
}, },
directives: { directives: {
...@@ -331,38 +331,38 @@ export default { ...@@ -331,38 +331,38 @@ export default {
</gl-button> </gl-button>
</form> </form>
</div> </div>
<gl-deprecated-dropdown <gl-dropdown
text="Options" text="Options"
class="error-details-options d-md-none" class="error-details-options d-md-none"
right right
:disabled="issueUpdateInProgress" :disabled="issueUpdateInProgress"
> >
<gl-deprecated-dropdown-item <gl-dropdown-item
data-qa-selector="update_ignore_status_button" data-qa-selector="update_ignore_status_button"
@click="onIgnoreStatusUpdate" @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" data-qa-selector="update_resolve_status_button"
@click="onResolveStatusUpdate" @click="onResolveStatusUpdate"
>{{ resolveBtnLabel }}</gl-deprecated-dropdown-item >{{ resolveBtnLabel }}</gl-dropdown-item
> >
<gl-deprecated-dropdown-divider /> <gl-dropdown-divider />
<gl-deprecated-dropdown-item <gl-dropdown-item
v-if="error.gitlabIssuePath" v-if="error.gitlabIssuePath"
data-qa-selector="view_issue_button" data-qa-selector="view_issue_button"
:href="error.gitlabIssuePath" :href="error.gitlabIssuePath"
variant="success" variant="success"
>{{ __('View issue') }}</gl-deprecated-dropdown-item >{{ __('View issue') }}</gl-dropdown-item
> >
<gl-deprecated-dropdown-item <gl-dropdown-item
v-if="!error.gitlabIssuePath" v-if="!error.gitlabIssuePath"
:loading="issueCreationInProgress" :loading="issueCreationInProgress"
data-qa-selector="create_issue_button" data-qa-selector="create_issue_button"
@click="createIssue" @click="createIssue"
>{{ __('Create issue') }}</gl-deprecated-dropdown-item >{{ __('Create issue') }}</gl-dropdown-item
> >
</gl-deprecated-dropdown> </gl-dropdown>
</div> </div>
</div> </div>
<div> <div>
......
...@@ -8,9 +8,9 @@ import { ...@@ -8,9 +8,9 @@ import {
GlLoadingIcon, GlLoadingIcon,
GlTable, GlTable,
GlFormInput, GlFormInput,
GlDeprecatedDropdown, GlDropdown,
GlDeprecatedDropdownItem, GlDropdownItem,
GlDeprecatedDropdownDivider, GlDropdownDivider,
GlTooltipDirective, GlTooltipDirective,
GlPagination, GlPagination,
} from '@gitlab/ui'; } from '@gitlab/ui';
...@@ -72,9 +72,9 @@ export default { ...@@ -72,9 +72,9 @@ export default {
components: { components: {
GlEmptyState, GlEmptyState,
GlButton, GlButton,
GlDeprecatedDropdown, GlDropdown,
GlDeprecatedDropdownItem, GlDropdownItem,
GlDeprecatedDropdownDivider, GlDropdownDivider,
GlIcon, GlIcon,
GlLink, GlLink,
GlLoadingIcon, GlLoadingIcon,
...@@ -233,30 +233,30 @@ export default { ...@@ -233,30 +233,30 @@ export default {
> >
<div class="search-box flex-fill mb-1 mb-md-0"> <div class="search-box flex-fill mb-1 mb-md-0">
<div class="filtered-search-box mb-0"> <div class="filtered-search-box mb-0">
<gl-deprecated-dropdown <gl-dropdown
:text="__('Recent searches')" :text="__('Recent searches')"
class="filtered-search-history-dropdown-wrapper" 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" :disabled="loading"
> >
<div v-if="!$options.hasLocalStorage" class="px-3"> <div v-if="!$options.hasLocalStorage" class="px-3">
{{ __('This feature requires local storage to be enabled') }} {{ __('This feature requires local storage to be enabled') }}
</div> </div>
<template v-else-if="recentSearches.length > 0"> <template v-else-if="recentSearches.length > 0">
<gl-deprecated-dropdown-item <gl-dropdown-item
v-for="searchQuery in recentSearches" v-for="searchQuery in recentSearches"
:key="searchQuery" :key="searchQuery"
@click="setSearchText(searchQuery)" @click="setSearchText(searchQuery)"
>{{ searchQuery }} >{{ searchQuery }}
</gl-deprecated-dropdown-item> </gl-dropdown-item>
<gl-deprecated-dropdown-divider /> <gl-dropdown-divider />
<gl-deprecated-dropdown-item ref="clearRecentSearches" @click="clearRecentSearches" <gl-dropdown-item ref="clearRecentSearches" @click="clearRecentSearches"
>{{ __('Clear recent searches') }} >{{ __('Clear recent searches') }}
</gl-deprecated-dropdown-item> </gl-dropdown-item>
</template> </template>
<div v-else class="px-3">{{ __("You don't have any recent searches") }}</div> <div v-else class="px-3">{{ __("You don't have any recent searches") }}</div>
</gl-deprecated-dropdown> </gl-dropdown>
<div class="filtered-search-input-container flex-fill"> <div class="filtered-search-input-container gl-flex-fill-1">
<gl-form-input <gl-form-input
v-model="errorSearchQuery" v-model="errorSearchQuery"
class="pl-2 filtered-search" class="pl-2 filtered-search"
...@@ -280,49 +280,44 @@ export default { ...@@ -280,49 +280,44 @@ export default {
</div> </div>
</div> </div>
<gl-deprecated-dropdown <gl-dropdown
:text="$options.statusFilters[statusFilter]" :text="$options.statusFilters[statusFilter]"
class="status-dropdown mx-md-1 mb-1 mb-md-0" class="status-dropdown mx-md-1 mb-1 mb-md-0"
menu-class="dropdown"
:disabled="loading" :disabled="loading"
right
> >
<gl-deprecated-dropdown-item <gl-dropdown-item
v-for="(label, status) in $options.statusFilters" v-for="(label, status) in $options.statusFilters"
:key="status" :key="status"
@click="filterErrors(status, label)" @click="filterErrors(status, label)"
> >
<span class="d-flex"> <span class="d-flex">
<gl-icon <gl-icon
class="flex-shrink-0 append-right-4" class="gl-new-dropdown-item-check-icon"
:class="{ invisible: !isCurrentStatusFilter(status) }" :class="{ invisible: !isCurrentStatusFilter(status) }"
name="mobile-issue-close" name="mobile-issue-close"
/> />
{{ label }} {{ label }}
</span> </span>
</gl-deprecated-dropdown-item> </gl-dropdown-item>
</gl-deprecated-dropdown> </gl-dropdown>
<gl-deprecated-dropdown <gl-dropdown :text="$options.sortFields[sortField]" right :disabled="loading">
:text="$options.sortFields[sortField]" <gl-dropdown-item
left
:disabled="loading"
menu-class="dropdown"
>
<gl-deprecated-dropdown-item
v-for="(label, field) in $options.sortFields" v-for="(label, field) in $options.sortFields"
:key="field" :key="field"
@click="sortByField(field)" @click="sortByField(field)"
> >
<span class="d-flex"> <span class="d-flex">
<gl-icon <gl-icon
class="flex-shrink-0 append-right-4" class="gl-new-dropdown-item-check-icon"
:class="{ invisible: !isCurrentSortField(field) }" :class="{ invisible: !isCurrentSortField(field) }"
name="mobile-issue-close" name="mobile-issue-close"
/> />
{{ label }} {{ label }}
</span> </span>
</gl-deprecated-dropdown-item> </gl-dropdown-item>
</gl-deprecated-dropdown> </gl-dropdown>
</div> </div>
<div v-if="loading" class="py-3"> <div v-if="loading" class="py-3">
......
<script> <script>
import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { getDisplayName } from '../utils'; import { getDisplayName } from '../utils';
export default { export default {
components: { components: {
GlDeprecatedDropdown, GlDropdown,
GlDeprecatedDropdownItem, GlDropdownItem,
}, },
props: { props: {
dropdownLabel: { dropdownLabel: {
...@@ -52,22 +52,22 @@ export default { ...@@ -52,22 +52,22 @@ export default {
<div :class="{ 'gl-show-field-errors': isProjectInvalid }"> <div :class="{ 'gl-show-field-errors': isProjectInvalid }">
<label class="label-bold" for="project-dropdown">{{ __('Project') }}</label> <label class="label-bold" for="project-dropdown">{{ __('Project') }}</label>
<div class="row"> <div class="row">
<gl-deprecated-dropdown <gl-dropdown
id="project-dropdown" id="project-dropdown"
class="col-8 col-md-9 gl-pr-0" class="col-8 col-md-9 gl-pr-0"
:disabled="!hasProjects" :disabled="!hasProjects"
menu-class="w-100 mw-100" 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" :text="dropdownLabel"
> >
<gl-deprecated-dropdown-item <gl-dropdown-item
v-for="project in projects" v-for="project in projects"
:key="`${project.organizationSlug}.${project.slug}`" :key="`${project.organizationSlug}.${project.slug}`"
class="w-100" class="w-100"
@click="$emit('select-project', project)" @click="$emit('select-project', project)"
>{{ getDisplayName(project) }}</gl-deprecated-dropdown-item >{{ getDisplayName(project) }}</gl-dropdown-item
> >
</gl-deprecated-dropdown> </gl-dropdown>
</div> </div>
<p v-if="isProjectInvalid" class="js-project-dropdown-error gl-field-error"> <p v-if="isProjectInvalid" class="js-project-dropdown-error gl-field-error">
{{ invalidProjectLabel }} {{ invalidProjectLabel }}
......
...@@ -27,7 +27,7 @@ export default { ...@@ -27,7 +27,7 @@ export default {
rolloutUserListLabel: s__('FeatureFlag|User List'), rolloutUserListLabel: s__('FeatureFlag|User List'),
rolloutUserListDescription: s__('FeatureFlag|Select a user list'), rolloutUserListDescription: s__('FeatureFlag|Select a user list'),
rolloutUserListNoListError: s__('FeatureFlag|There are no configured user lists'), rolloutUserListNoListError: s__('FeatureFlag|There are no configured user lists'),
defaultDropdownText: s__('FeatureFlags|Select a user list'), defaultDropdownText: s__('FeatureFlags|No user list selected'),
}, },
computed: { computed: {
...mapGetters(['hasUserLists', 'isLoading', 'hasError', 'userListOptions']), ...mapGetters(['hasUserLists', 'isLoading', 'hasError', 'userListOptions']),
...@@ -36,7 +36,7 @@ export default { ...@@ -36,7 +36,7 @@ export default {
return this.strategy?.userList?.id ?? ''; return this.strategy?.userList?.id ?? '';
}, },
dropdownText() { dropdownText() {
return this.strategy?.userList?.name ?? this.$options.defaultDropdownText; return this.strategy?.userList?.name ?? this.$options.translations.defaultDropdownText;
}, },
}, },
mounted() { mounted() {
......
...@@ -37,8 +37,6 @@ const restartJobsPolling = () => { ...@@ -37,8 +37,6 @@ const restartJobsPolling = () => {
if (eTagPoll) eTagPoll.restart(); if (eTagPoll) eTagPoll.restart();
}; };
const setFilter = ({ commit }, filter) => commit(types.SET_FILTER, filter);
const setImportTarget = ({ commit }, { repoId, importTarget }) => const setImportTarget = ({ commit }, { repoId, importTarget }) =>
commit(types.SET_IMPORT_TARGET, { repoId, importTarget }); commit(types.SET_IMPORT_TARGET, { repoId, importTarget });
...@@ -172,12 +170,9 @@ const fetchNamespacesFactory = (namespacesPath = isRequired()) => ({ commit }) = ...@@ -172,12 +170,9 @@ const fetchNamespacesFactory = (namespacesPath = isRequired()) => ({ commit }) =
}); });
}; };
const setPage = ({ state, commit, dispatch }, page) => { const setFilter = ({ commit, dispatch }, filter) => {
if (page === state.pageInfo.page) { commit(types.SET_FILTER, filter);
return null;
}
commit(types.SET_PAGE, page);
return dispatch('fetchRepos'); return dispatch('fetchRepos');
}; };
...@@ -188,7 +183,6 @@ export default ({ endpoints = isRequired() }) => ({ ...@@ -188,7 +183,6 @@ export default ({ endpoints = isRequired() }) => ({
setFilter, setFilter,
setImportTarget, setImportTarget,
importAll, importAll,
setPage,
fetchRepos: fetchReposFactory({ reposPath: endpoints.reposPath }), fetchRepos: fetchReposFactory({ reposPath: endpoints.reposPath }),
fetchImport: fetchImportFactory(endpoints.importPath), fetchImport: fetchImportFactory(endpoints.importPath),
fetchJobs: fetchJobsFactory(endpoints.jobsPath), fetchJobs: fetchJobsFactory(endpoints.jobsPath),
......
...@@ -116,7 +116,7 @@ export default { ...@@ -116,7 +116,7 @@ export default {
<gl-dropdown <gl-dropdown
v-if="displayFilters" v-if="displayFilters"
id="discussion-filter-dropdown" id="discussion-filter-dropdown"
class="gl-mr-3 full-width-mobile discussion-filter-container js-discussion-filter-container qa-discussion-filter" class="gl-mr-3 full-width-mobile discussion-filter-container js-discussion-filter-container"
data-qa-selector="discussion_filter_dropdown" data-qa-selector="discussion_filter_dropdown"
:text="currentFilter.title" :text="currentFilter.title"
> >
......
...@@ -65,8 +65,8 @@ export default { ...@@ -65,8 +65,8 @@ export default {
}; };
}, },
computed: { computed: {
toggleChevronClass() { toggleChevronIconName() {
return this.expanded ? 'fa-chevron-up' : 'fa-chevron-down'; return this.expanded ? 'chevron-up' : 'chevron-down';
}, },
noteTimestampLink() { noteTimestampLink() {
return this.noteId ? `#note_${this.noteId}` : undefined; return this.noteId ? `#note_${this.noteId}` : undefined;
...@@ -133,7 +133,7 @@ export default { ...@@ -133,7 +133,7 @@ export default {
type="button" type="button"
@click="handleToggle" @click="handleToggle"
> >
<i ref="chevronIcon" :class="toggleChevronClass" class="fa" aria-hidden="true"></i> <gl-icon ref="chevronIcon" :name="toggleChevronIconName" aria-hidden="true" />
{{ __('Toggle thread') }} {{ __('Toggle thread') }}
</button> </button>
</div> </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 initDevOpsScoreEmptyState from '~/admin/dev_ops_report/devops_score_empty_state';
import initDevopAdoption from '~/admin/dev_ops_report/devops_adoption';
initDevOpsScoreEmptyState(); initDevOpsScoreEmptyState();
initDevopAdoption(); initDevopAdoption();
...@@ -21,35 +21,32 @@ function mountRemoveMemberModal() { ...@@ -21,35 +21,32 @@ function mountRemoveMemberModal() {
}); });
} }
document.addEventListener('DOMContentLoaded', () => { const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
groupsSelect(); initGroupMembersApp(
memberExpirationDate(); document.querySelector('.js-group-members-list'),
memberExpirationDate('.js-access-expiration-date-groups'); SHARED_FIELDS.concat(['source', 'granted']),
mountRemoveMemberModal(); 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( new Members(); // eslint-disable-line no-new
document.querySelector('.js-group-members-list'), new UsersSelect(); // eslint-disable-line no-new
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
});
...@@ -57,7 +57,7 @@ export default { ...@@ -57,7 +57,7 @@ export default {
<tooltip-on-truncate :title="jobName" truncate-target="child" placement="top"> <tooltip-on-truncate :title="jobName" truncate-target="child" placement="top">
<div <div
:id="jobId" :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" :class="jobPillClasses"
@mouseover="onMouseEnter" @mouseover="onMouseEnter"
@mouseleave="onMouseLeave" @mouseleave="onMouseLeave"
......
...@@ -97,15 +97,20 @@ export default { ...@@ -97,15 +97,20 @@ export default {
this.reportFailure(DRAW_FAILURE); this.reportFailure(DRAW_FAILURE);
} }
}, },
getStageBackgroundClass(index) { getStageBackgroundClasses(index) {
const { length } = this.pipelineData.stages; 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) { if (length === 1) {
return 'stage-rounded'; return 'gl-rounded-bottom-left-6 gl-rounded-top-left-6 gl-rounded-bottom-right-6 gl-rounded-top-right-6';
} else if (index === 0) { }
return 'stage-left-rounded';
} else if (index === length - 1) { if (index === 0) {
return 'stage-right-rounded'; 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 ''; return '';
...@@ -190,7 +195,8 @@ export default { ...@@ -190,7 +195,8 @@ export default {
> >
<div <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="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" /> <stage-pill :stage-name="stage.name" :is-empty="stage.groups.length === 0" />
</div> </div>
......
...@@ -26,7 +26,7 @@ export default { ...@@ -26,7 +26,7 @@ export default {
<template> <template>
<tooltip-on-truncate :title="stageName" truncate-target="child" placement="top"> <tooltip-on-truncate :title="stageName" truncate-target="child" placement="top">
<div <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" :class="emptyClass"
> >
{{ stageName }} {{ stageName }}
......
...@@ -15,6 +15,10 @@ export const DELETE_TAGS_SUCCESS_MESSAGE = s__( ...@@ -15,6 +15,10 @@ export const DELETE_TAGS_SUCCESS_MESSAGE = s__(
'ContainerRegistry|Tags successfully marked for deletion.', '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 TAGS_LIST_TITLE = s__('ContainerRegistry|Image tags');
export const DIGEST_LABEL = s__('ContainerRegistry|Digest: %{imageId}'); export const DIGEST_LABEL = s__('ContainerRegistry|Digest: %{imageId}');
export const CREATED_AT_LABEL = s__('ContainerRegistry|Published %{timeInfo}'); export const CREATED_AT_LABEL = s__('ContainerRegistry|Published %{timeInfo}');
......
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import Api from '~/api';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { import {
FETCH_IMAGES_LIST_ERROR_MESSAGE, FETCH_IMAGES_LIST_ERROR_MESSAGE,
DEFAULT_PAGE, DEFAULT_PAGE,
DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE,
FETCH_TAGS_LIST_ERROR_MESSAGE, FETCH_TAGS_LIST_ERROR_MESSAGE,
FETCH_IMAGE_DETAILS_ERROR_MESSAGE,
} from '../constants/index'; } from '../constants/index';
import { decodeAndParse } from '../utils'; import { decodeAndParse } from '../utils';
...@@ -61,6 +63,19 @@ export const requestTagsList = ({ commit, dispatch }, { pagination = {}, params ...@@ -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 }) => { export const requestDeleteTag = ({ commit, dispatch, state }, { tag, params }) => {
commit(types.SET_MAIN_LOADING, true); commit(types.SET_MAIN_LOADING, true);
return axios return axios
......
...@@ -7,3 +7,4 @@ export const SET_MAIN_LOADING = 'SET_MAIN_LOADING'; ...@@ -7,3 +7,4 @@ export const SET_MAIN_LOADING = 'SET_MAIN_LOADING';
export const SET_TAGS_PAGINATION = 'SET_TAGS_PAGINATION'; export const SET_TAGS_PAGINATION = 'SET_TAGS_PAGINATION';
export const SET_TAGS_LIST_SUCCESS = 'SET_TAGS_LIST_SUCCESS'; 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_SHOW_GARBAGE_COLLECTION_TIP = 'SET_SHOW_GARBAGE_COLLECTION_TIP';
export const SET_IMAGE_DETAILS = 'SET_IMAGE_DETAILS';
...@@ -47,4 +47,8 @@ export default { ...@@ -47,4 +47,8 @@ export default {
const normalizedHeaders = normalizeHeaders(headers); const normalizedHeaders = normalizeHeaders(headers);
state.tagsPagination = parseIntPagination(normalizedHeaders); state.tagsPagination = parseIntPagination(normalizedHeaders);
}, },
[types.SET_IMAGE_DETAILS](state, details) {
state.imageDetails = details;
},
}; };
...@@ -3,6 +3,7 @@ export default () => ({ ...@@ -3,6 +3,7 @@ export default () => ({
showGarbageCollectionTip: false, showGarbageCollectionTip: false,
config: {}, config: {},
images: [], images: [],
imageDetails: {},
tags: [], tags: [],
pagination: {}, pagination: {},
tagsPagination: {}, tagsPagination: {},
......
export const decodeAndParse = param => JSON.parse(window.atob(param)); 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 { ...@@ -26,7 +26,7 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
openPath: { openedPath: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
...@@ -43,7 +43,7 @@ export default { ...@@ -43,7 +43,7 @@ export default {
}, },
}, },
computed: { computed: {
open() { opened() {
return this.total - (this.closed + (this.merged || 0)); return this.total - (this.closed + (this.merged || 0));
}, },
showMerged() { showMerged() {
...@@ -63,8 +63,8 @@ export default { ...@@ -63,8 +63,8 @@ export default {
<span class="gl-white-space-pre-wrap" data-testid="open-stat"> <span class="gl-white-space-pre-wrap" data-testid="open-stat">
<gl-sprintf :message="__('Open: %{open}')"> <gl-sprintf :message="__('Open: %{open}')">
<template #open> <template #open>
<gl-link v-if="openPath" :href="openPath">{{ open }}</gl-link> <gl-link v-if="openedPath" :href="openedPath">{{ opened }}</gl-link>
<template v-else>{{ open }}</template> <template v-else>{{ opened }}</template>
</template> </template>
</gl-sprintf> </gl-sprintf>
</span> </span>
......
...@@ -87,9 +87,14 @@ export default { ...@@ -87,9 +87,14 @@ export default {
<release-block-header :release="release" /> <release-block-header :release="release" />
<div class="card-body"> <div class="card-body">
<div v-if="shouldRenderMilestoneInfo"> <div v-if="shouldRenderMilestoneInfo">
<!-- TODO: Switch open* links to opened* once fields have been updated in GraphQL -->
<release-block-milestone-info <release-block-milestone-info
:milestones="milestones" :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" /> <hr class="mb-3 mt-0" />
</div> </div>
......
...@@ -20,7 +20,7 @@ export default { ...@@ -20,7 +20,7 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
openIssuesPath: { openedIssuesPath: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
...@@ -30,7 +30,7 @@ export default { ...@@ -30,7 +30,7 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
openMergeRequestsPath: { openedMergeRequestsPath: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
...@@ -173,7 +173,7 @@ export default { ...@@ -173,7 +173,7 @@ export default {
:label="__('Issues')" :label="__('Issues')"
:total="issueCounts.total" :total="issueCounts.total"
:closed="issueCounts.closed" :closed="issueCounts.closed"
:open-path="openIssuesPath" :opened-path="openedIssuesPath"
:closed-path="closedIssuesPath" :closed-path="closedIssuesPath"
data-testid="issue-stats" data-testid="issue-stats"
/> />
...@@ -183,7 +183,7 @@ export default { ...@@ -183,7 +183,7 @@ export default {
:total="mergeRequestCounts.total" :total="mergeRequestCounts.total"
:merged="mergeRequestCounts.merged" :merged="mergeRequestCounts.merged"
:closed="mergeRequestCounts.closed" :closed="mergeRequestCounts.closed"
:open-path="openMergeRequestsPath" :opened-path="openedMergeRequestsPath"
:merged-path="mergedMergeRequestsPath" :merged-path="mergedMergeRequestsPath"
:closed-path="closedMergeRequestsPath" :closed-path="closedMergeRequestsPath"
data-testid="merge-request-stats" data-testid="merge-request-stats"
......
...@@ -33,9 +33,12 @@ fragment Release on Release { ...@@ -33,9 +33,12 @@ fragment Release on Release {
} }
links { links {
editUrl editUrl
issuesUrl
mergeRequestsUrl
selfUrl selfUrl
openedIssuesUrl
closedIssuesUrl
openedMergeRequestsUrl
mergedMergeRequestsUrl
closedMergeRequestsUrl
} }
commit { commit {
sha sha
......
...@@ -137,8 +137,8 @@ export default { ...@@ -137,8 +137,8 @@ export default {
:href="commit.author.webPath" :href="commit.author.webPath"
class="commit-author-link js-user-link" class="commit-author-link js-user-link"
> >
{{ commit.author.name }} {{ commit.author.name }}</gl-link
</gl-link> >
<template v-else> <template v-else>
{{ commit.authorName }} {{ commit.authorName }}
</template> </template>
......
...@@ -109,10 +109,6 @@ ...@@ -109,10 +109,6 @@
content: '\f0da'; content: '\f0da';
} }
.fa-chevron-up::before {
content: '\f077';
}
.fa-exclamation-circle::before { .fa-exclamation-circle::before {
content: '\f06a'; content: '\f06a';
} }
......
...@@ -486,23 +486,3 @@ ...@@ -486,23 +486,3 @@
.progress-bar.bg-primary { .progress-bar.bg-primary {
background-color: $blue-500 !important; 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 { ...@@ -188,6 +188,12 @@ ul.related-merge-requests > li {
border-width: 1px; border-width: 1px;
line-height: $line-height-base; line-height: $line-height-base;
width: auto; width: auto;
&.disabled {
background-color: $gray-light;
border-color: $gray-100;
color: $gl-text-color-disabled;
}
} }
} }
......
...@@ -132,13 +132,23 @@ class GroupsController < Groups::ApplicationController ...@@ -132,13 +132,23 @@ class GroupsController < Groups::ApplicationController
def update def update
if Groups::UpdateService.new(@group, current_user, group_params).execute 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 else
@group.reset @group.reset
render action: "edit" render action: "edit"
end end
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 def destroy
Groups::DestroyService.new(@group, current_user).async_execute Groups::DestroyService.new(@group, current_user).async_execute
......
...@@ -48,18 +48,14 @@ class Import::BaseController < ApplicationController ...@@ -48,18 +48,14 @@ class Import::BaseController < ApplicationController
private private
def filter_attribute
:name
end
def sanitized_filter_param def sanitized_filter_param
@filter ||= sanitize(params[:filter]) @filter ||= sanitize(params[:filter])&.downcase
end end
def filtered(collection) def filtered(collection)
return collection unless sanitized_filter_param 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 end
def serialized_provider_repos def serialized_provider_repos
......
...@@ -132,8 +132,4 @@ class Import::BitbucketController < Import::BaseController ...@@ -132,8 +132,4 @@ class Import::BitbucketController < Import::BaseController
refresh_token: session[:bitbucket_refresh_token] refresh_token: session[:bitbucket_refresh_token]
} }
end end
def sanitized_filter_param
@filter ||= sanitize(params[:filter])
end
end end
...@@ -170,10 +170,6 @@ class Import::BitbucketServerController < Import::BaseController ...@@ -170,10 +170,6 @@ class Import::BitbucketServerController < Import::BaseController
BitbucketServer::Paginator::PAGE_LENGTH BitbucketServer::Paginator::PAGE_LENGTH
end end
def sanitized_filter_param
sanitize(params[:filter])
end
def bitbucket_connection_error(error) def bitbucket_connection_error(error)
flash[:alert] = _("Unable to connect to server: %{error}") % { error: error } flash[:alert] = _("Unable to connect to server: %{error}") % { error: error }
clear_session_data clear_session_data
......
...@@ -245,14 +245,6 @@ class Import::GithubController < Import::BaseController ...@@ -245,14 +245,6 @@ class Import::GithubController < Import::BaseController
def extra_import_params def extra_import_params
{} {}
end end
def sanitized_filter_param
@filter ||= sanitize(params[:filter])
end
def filter_attribute
:name
end
end end
Import::GithubController.prepend_if_ee('EE::Import::GithubController') Import::GithubController.prepend_if_ee('EE::Import::GithubController')
...@@ -51,7 +51,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -51,7 +51,7 @@ class Projects::IssuesController < Projects::ApplicationController
real_time_feature_flag = :real_time_issue_sidebar real_time_feature_flag = :real_time_issue_sidebar
real_time_enabled = Gitlab::ActionCable::Config.in_app? || Feature.enabled?(real_time_feature_flag, @project) real_time_enabled = Gitlab::ActionCable::Config.in_app? || Feature.enabled?(real_time_feature_flag, @project)
gon.push({ features: { real_time_feature_flag.to_s.camelize(:lower) => real_time_enabled } }, true) push_to_gon_features(real_time_feature_flag, real_time_enabled)
record_experiment_user(:invite_members_version_a) record_experiment_user(:invite_members_version_a)
record_experiment_user(:invite_members_version_b) record_experiment_user(:invite_members_version_b)
......
...@@ -32,7 +32,7 @@ module FinderWithCrossProjectAccess ...@@ -32,7 +32,7 @@ module FinderWithCrossProjectAccess
end end
override :execute override :execute
def execute(*args) def execute(*args, **kwargs)
check = Gitlab::CrossProjectAccess.find_check(self) check = Gitlab::CrossProjectAccess.find_check(self)
original = -> { super } original = -> { super }
......
...@@ -9,7 +9,7 @@ module Resolvers ...@@ -9,7 +9,7 @@ module Resolvers
authorize :read_design authorize :read_design
argument :id, GraphQL::ID_TYPE, argument :id, ::Types::GlobalIDType[::DesignManagement::DesignAtVersion],
required: true, required: true,
description: 'The Global ID of the design at this version' description: 'The Global ID of the design at this version'
...@@ -18,7 +18,10 @@ module Resolvers ...@@ -18,7 +18,10 @@ module Resolvers
end end
def find_object(id:) def find_object(id:)
dav = GitlabSchema.object_from_id(id, expected_type: ::DesignManagement::DesignAtVersion) # TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ::Types::GlobalIDType[::DesignManagement::DesignAtVersion].coerce_isolated_input(id)
dav = GitlabSchema.find_by_gid(id)
return unless consistent?(dav) return unless consistent?(dav)
dav dav
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module Resolvers module Resolvers
module DesignManagement module DesignManagement
class DesignResolver < BaseResolver class DesignResolver < BaseResolver
argument :id, GraphQL::ID_TYPE, argument :id, ::Types::GlobalIDType[::DesignManagement::Design],
required: false, required: false,
description: 'Find a design by its ID' description: 'Find a design by its ID'
...@@ -50,7 +50,11 @@ module Resolvers ...@@ -50,7 +50,11 @@ module Resolvers
end end
def parse_gid(gid) def parse_gid(gid)
GitlabSchema.parse_gid(gid, expected_type: ::DesignManagement::Design).model_id # TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
gid = ::Types::GlobalIDType[::DesignManagement::Design].coerce_isolated_input(gid)
gid.model_id
end end
end end
end end
......
...@@ -3,16 +3,16 @@ ...@@ -3,16 +3,16 @@
module Resolvers module Resolvers
module DesignManagement module DesignManagement
class DesignsResolver < BaseResolver class DesignsResolver < BaseResolver
argument :ids, DesignID = ::Types::GlobalIDType[::DesignManagement::Design]
[GraphQL::ID_TYPE], VersionID = ::Types::GlobalIDType[::DesignManagement::Version]
argument :ids, [DesignID],
required: false, required: false,
description: 'Filters designs by their ID' description: 'Filters designs by their ID'
argument :filenames, argument :filenames, [GraphQL::STRING_TYPE],
[GraphQL::STRING_TYPE],
required: false, required: false,
description: 'Filters designs by their filename' description: 'Filters designs by their filename'
argument :at_version, argument :at_version, VersionID,
GraphQL::ID_TYPE,
required: false, required: false,
description: 'Filters designs to only those that existed at the version. ' \ description: 'Filters designs to only those that existed at the version. ' \
'If argument is omitted or nil then all designs will reflect the latest version' 'If argument is omitted or nil then all designs will reflect the latest version'
...@@ -36,11 +36,20 @@ module Resolvers ...@@ -36,11 +36,20 @@ module Resolvers
def version(at_version) def version(at_version)
return unless at_version return unless at_version
GitlabSchema.object_from_id(at_version, expected_type: ::DesignManagement::Version)&.sync # TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
at_version = VersionID.coerce_isolated_input(at_version)
# TODO: when we get promises use this to make resolve lazy
Gitlab::Graphql::Lazy.force(GitlabSchema.find_by_gid(at_version))
end end
def design_ids(ids) def design_ids(gids)
ids&.map { |id| GlobalID.parse(id, expected_type: ::DesignManagement::Design).model_id } return if gids.nil?
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
gids = gids.map { |id| DesignID.coerce_isolated_input(id) }
gids.map(&:model_id)
end end
def issue def issue
......
...@@ -5,17 +5,20 @@ module Resolvers ...@@ -5,17 +5,20 @@ module Resolvers
module Version module Version
# Resolver for a DesignAtVersion object given an implicit version context # Resolver for a DesignAtVersion object given an implicit version context
class DesignAtVersionResolver < BaseResolver class DesignAtVersionResolver < BaseResolver
DesignAtVersionID = ::Types::GlobalIDType[::DesignManagement::DesignAtVersion]
DesignID = ::Types::GlobalIDType[::DesignManagement::Design]
include Gitlab::Graphql::Authorize::AuthorizeResource include Gitlab::Graphql::Authorize::AuthorizeResource
type Types::DesignManagement::DesignAtVersionType, null: true type Types::DesignManagement::DesignAtVersionType, null: true
authorize :read_design authorize :read_design
argument :id, GraphQL::ID_TYPE, argument :id, DesignAtVersionID,
required: false, required: false,
as: :design_at_version_id, as: :design_at_version_id,
description: 'The ID of the DesignAtVersion' description: 'The ID of the DesignAtVersion'
argument :design_id, GraphQL::ID_TYPE, argument :design_id, DesignID,
required: false, required: false,
description: 'The ID of a specific design' description: 'The ID of a specific design'
argument :filename, GraphQL::STRING_TYPE, argument :filename, GraphQL::STRING_TYPE,
...@@ -29,6 +32,11 @@ module Resolvers ...@@ -29,6 +32,11 @@ module Resolvers
def resolve(design_id: nil, filename: nil, design_at_version_id: nil) def resolve(design_id: nil, filename: nil, design_at_version_id: nil)
validate_arguments(design_id, filename, design_at_version_id) validate_arguments(design_id, filename, design_at_version_id)
# TODO: remove this when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
design_id &&= DesignID.coerce_isolated_input(design_id)
design_at_version_id &&= DesignAtVersionID.coerce_isolated_input(design_at_version_id)
return unless Ability.allowed?(current_user, :read_design, issue) return unless Ability.allowed?(current_user, :read_design, issue)
return specific_design_at_version(design_at_version_id) if design_at_version_id return specific_design_at_version(design_at_version_id) if design_at_version_id
...@@ -49,7 +57,7 @@ module Resolvers ...@@ -49,7 +57,7 @@ module Resolvers
end end
def specific_design_at_version(id) def specific_design_at_version(id)
dav = GitlabSchema.object_from_id(id, expected_type: ::DesignManagement::DesignAtVersion) dav = GitlabSchema.find_by_gid(id)
return unless consistent?(dav) return unless consistent?(dav)
dav dav
...@@ -65,8 +73,8 @@ module Resolvers ...@@ -65,8 +73,8 @@ module Resolvers
dav.design.visible_in?(version) dav.design.visible_in?(version)
end end
def find(id, filename) def find(gid, filename)
ids = [parse_design_id(id).model_id] if id ids = [gid.model_id] if gid
filenames = [filename] if filename filenames = [filename] if filename
::DesignManagement::DesignsFinder ::DesignManagement::DesignsFinder
...@@ -74,10 +82,6 @@ module Resolvers ...@@ -74,10 +82,6 @@ module Resolvers
.execute .execute
end end
def parse_design_id(id)
GitlabSchema.parse_gid(id, expected_type: ::DesignManagement::Design)
end
def issue def issue
version.issue version.issue
end end
......
...@@ -11,8 +11,9 @@ module Resolvers ...@@ -11,8 +11,9 @@ module Resolvers
authorize :read_design authorize :read_design
argument :ids, DesignID = ::Types::GlobalIDType[::DesignManagement::Design]
[GraphQL::ID_TYPE],
argument :ids, [DesignID],
required: false, required: false,
description: 'Filters designs by their ID' description: 'Filters designs by their ID'
argument :filenames, argument :filenames,
...@@ -31,16 +32,19 @@ module Resolvers ...@@ -31,16 +32,19 @@ module Resolvers
private private
def find(ids, filenames) def find(ids, filenames)
ids = ids&.map { |id| parse_design_id(id).model_id }
::DesignManagement::DesignsFinder.new(issue, current_user, ::DesignManagement::DesignsFinder.new(issue, current_user,
ids: ids, ids: design_ids(ids),
filenames: filenames, filenames: filenames,
visible_at_version: version) visible_at_version: version)
end end
def parse_design_id(id) def design_ids(gids)
GitlabSchema.parse_gid(id, expected_type: ::DesignManagement::Design) return if gids.nil?
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
gids = gids.map { |id| DesignID.coerce_isolated_input(id) }
gids.map(&:model_id)
end end
def issue def issue
......
...@@ -11,20 +11,25 @@ module Resolvers ...@@ -11,20 +11,25 @@ module Resolvers
alias_method :collection, :object alias_method :collection, :object
VersionID = ::Types::GlobalIDType[::DesignManagement::Version]
argument :sha, GraphQL::STRING_TYPE, argument :sha, GraphQL::STRING_TYPE,
required: false, required: false,
description: "The SHA256 of a specific version" description: "The SHA256 of a specific version"
argument :id, GraphQL::ID_TYPE, argument :id, VersionID,
as: :version_id,
required: false, required: false,
description: 'The Global ID of the version' description: 'The Global ID of the version'
def resolve(id: nil, sha: nil) def resolve(version_id: nil, sha: nil)
check_args(id, sha) # TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
version_id &&= VersionID.coerce_isolated_input(version_id)
gid = GitlabSchema.parse_gid(id, expected_type: ::DesignManagement::Version) if id check_args(version_id, sha)
::DesignManagement::VersionsFinder ::DesignManagement::VersionsFinder
.new(collection, current_user, sha: sha, version_id: gid&.model_id) .new(collection, current_user, sha: sha, version_id: version_id&.model_id)
.execute .execute
.first .first
end end
......
...@@ -9,7 +9,7 @@ module Resolvers ...@@ -9,7 +9,7 @@ module Resolvers
authorize :read_design authorize :read_design
argument :id, GraphQL::ID_TYPE, argument :id, ::Types::GlobalIDType[::DesignManagement::Version],
required: true, required: true,
description: 'The Global ID of the version' description: 'The Global ID of the version'
...@@ -18,7 +18,11 @@ module Resolvers ...@@ -18,7 +18,11 @@ module Resolvers
end end
def find_object(id:) def find_object(id:)
GitlabSchema.object_from_id(id, expected_type: ::DesignManagement::Version) # TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ::Types::GlobalIDType[::DesignManagement::Version].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end end
end end
end end
......
...@@ -7,12 +7,14 @@ module Resolvers ...@@ -7,12 +7,14 @@ module Resolvers
alias_method :design_or_collection, :object alias_method :design_or_collection, :object
VersionID = ::Types::GlobalIDType[::DesignManagement::Version]
argument :earlier_or_equal_to_sha, GraphQL::STRING_TYPE, argument :earlier_or_equal_to_sha, GraphQL::STRING_TYPE,
as: :sha, as: :sha,
required: false, required: false,
description: 'The SHA256 of the most recent acceptable version' description: 'The SHA256 of the most recent acceptable version'
argument :earlier_or_equal_to_id, GraphQL::ID_TYPE, argument :earlier_or_equal_to_id, VersionID,
as: :id, as: :id,
required: false, required: false,
description: 'The Global ID of the most recent acceptable version' description: 'The Global ID of the most recent acceptable version'
...@@ -23,6 +25,9 @@ module Resolvers ...@@ -23,6 +25,9 @@ module Resolvers
end end
def resolve(parent: nil, id: nil, sha: nil) def resolve(parent: nil, id: nil, sha: nil)
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id &&= VersionID.coerce_isolated_input(id)
version = cutoff(parent, id, sha) version = cutoff(parent, id, sha)
raise ::Gitlab::Graphql::Errors::ResourceNotAvailable, 'cutoff not found' unless version.present? raise ::Gitlab::Graphql::Errors::ResourceNotAvailable, 'cutoff not found' unless version.present?
...@@ -47,8 +52,7 @@ module Resolvers ...@@ -47,8 +52,7 @@ module Resolvers
end end
end end
def specific_version(id, sha) def specific_version(gid, sha)
gid = GitlabSchema.parse_gid(id, expected_type: ::DesignManagement::Version) if id
find(sha: sha, version_id: gid&.model_id).first find(sha: sha, version_id: gid&.model_id).first
end end
...@@ -58,8 +62,8 @@ module Resolvers ...@@ -58,8 +62,8 @@ module Resolvers
.execute .execute
end end
def by_id(id) def by_id(gid)
GitlabSchema.object_from_id(id, expected_type: ::DesignManagement::Version).sync ::Gitlab::Graphql::Lazy.force(GitlabSchema.find_by_gid(gid))
end end
# Find an `at_version` argument passed to a parent node. # Find an `at_version` argument passed to a parent node.
...@@ -69,7 +73,11 @@ module Resolvers ...@@ -69,7 +73,11 @@ module Resolvers
# for consistency we should only present versions up to the given # for consistency we should only present versions up to the given
# version here. # version here.
def at_version_arg(parent) def at_version_arg(parent)
::Gitlab::Graphql::FindArgumentInParent.find(parent, :at_version, limit_depth: 4) # TODO: remove coercion when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
version_id = ::Gitlab::Graphql::FindArgumentInParent.find(parent, :at_version, limit_depth: 4)
version_id &&= VersionID.coerce_isolated_input(version_id)
version_id
end end
end end
end end
......
...@@ -62,6 +62,8 @@ module Types ...@@ -62,6 +62,8 @@ module Types
description: 'Number of downvotes the issue has received' description: 'Number of downvotes the issue has received'
field :user_notes_count, GraphQL::INT_TYPE, null: false, field :user_notes_count, GraphQL::INT_TYPE, null: false,
description: 'Number of user notes of the issue' 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, field :web_path, GraphQL::STRING_TYPE, null: false, method: :issue_path,
description: 'Web path of the issue' description: 'Web path of the issue'
field :web_url, GraphQL::STRING_TYPE, null: false, field :web_url, GraphQL::STRING_TYPE, null: false,
...@@ -113,6 +115,26 @@ module Types ...@@ -113,6 +115,26 @@ module Types
field :severity, Types::IssuableSeverityEnum, null: true, field :severity, Types::IssuableSeverityEnum, null: true,
description: 'Severity level of the incident' 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 def author
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find
end end
......
...@@ -68,6 +68,8 @@ module Types ...@@ -68,6 +68,8 @@ module Types
description: 'SHA of the merge request commit (set once merged)' description: 'SHA of the merge request commit (set once merged)'
field :user_notes_count, GraphQL::INT_TYPE, null: true, field :user_notes_count, GraphQL::INT_TYPE, null: true,
description: 'User notes count of the merge request' 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, 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' 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, field :force_remove_source_branch, GraphQL::BOOLEAN_TYPE, method: :force_remove_source_branch?, null: true,
...@@ -158,17 +160,25 @@ module Types ...@@ -158,17 +160,25 @@ module Types
object.approved_by_users object.approved_by_users
end end
# rubocop: disable CodeReuse/ActiveRecord
def user_notes_count def user_notes_count
BatchLoader::GraphQL.for(object.id).batch(key: :merge_request_user_notes_count) do |ids, loader, args| 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| 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 end
end end
# rubocop: enable CodeReuse/ActiveRecord
def diff_stats(path: nil) def diff_stats(path: nil)
stats = Array.wrap(object.diff_stats&.to_a) stats = Array.wrap(object.diff_stats&.to_a)
......
...@@ -30,7 +30,7 @@ module OperationsHelper ...@@ -30,7 +30,7 @@ module OperationsHelper
'alerts_setup_url' => help_page_path('operations/incident_management/alert_integrations.md', anchor: 'generic-http-endpoint'), '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), 'alerts_usage_url' => project_alert_management_index_path(@project),
'disabled' => disabled.to_s, 'disabled' => disabled.to_s,
'project_path' => project_path(@project) 'project_path' => @project.full_path
} }
end end
......
...@@ -48,6 +48,8 @@ class ApplicationRecord < ActiveRecord::Base ...@@ -48,6 +48,8 @@ class ApplicationRecord < ActiveRecord::Base
def self.safe_find_or_create_by!(*args, &block) def self.safe_find_or_create_by!(*args, &block)
safe_find_or_create_by(*args, &block).tap do |record| safe_find_or_create_by(*args, &block).tap do |record|
raise ActiveRecord::RecordNotFound unless record.present?
record.validate! unless record.persisted? record.validate! unless record.persisted?
end end
end end
......
...@@ -109,6 +109,8 @@ class Group < Namespace ...@@ -109,6 +109,8 @@ class Group < Namespace
.where("project_authorizations.user_id IN (?)", user_ids) .where("project_authorizations.user_id IN (?)", user_ids)
end end
delegate :default_branch_name, to: :namespace_settings
class << self class << self
def sort_by_attribute(method) def sort_by_attribute(method)
if method == 'storage_size_desc' if method == 'storage_size_desc'
...@@ -587,7 +589,7 @@ class Group < Namespace ...@@ -587,7 +589,7 @@ class Group < Namespace
def update_two_factor_requirement def update_two_factor_requirement
return unless saved_change_to_require_two_factor_authentication? || saved_change_to_two_factor_grace_period? 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 end
def path_changed_hook def path_changed_hook
......
...@@ -6,10 +6,18 @@ class NamespaceSetting < ApplicationRecord ...@@ -6,10 +6,18 @@ class NamespaceSetting < ApplicationRecord
validate :default_branch_name_content validate :default_branch_name_content
validate :allow_mfa_for_group validate :allow_mfa_for_group
before_validation :normalize_default_branch_name
NAMESPACE_SETTINGS_PARAMS = [:default_branch_name].freeze NAMESPACE_SETTINGS_PARAMS = [:default_branch_name].freeze
self.primary_key = :namespace_id 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 def default_branch_name_content
return if default_branch_name.nil? return if default_branch_name.nil?
......
...@@ -197,8 +197,8 @@ class Note < ApplicationRecord ...@@ -197,8 +197,8 @@ class Note < ApplicationRecord
.map(&:position) .map(&:position)
end end
def count_for_collection(ids, type) def count_for_collection(ids, type, count_column = 'COUNT(*) as count')
user.select('noteable_id', 'COUNT(*) as count') user.select(:noteable_id, count_column)
.group(:noteable_id) .group(:noteable_id)
.where(noteable_type: type, noteable_id: ids) .where(noteable_type: type, noteable_id: ids)
end end
......
...@@ -163,16 +163,18 @@ module Ci ...@@ -163,16 +163,18 @@ module Ci
end end
def ensure_pending_state def ensure_pending_state
Ci::BuildPendingState.create_or_find_by!( build_state = Ci::BuildPendingState.safe_find_or_create_by(
build_id: build.id, build_id: build.id,
state: params.fetch(:state), state: params.fetch(:state),
trace_checksum: params.fetch(:checksum), trace_checksum: params.fetch(:checksum),
failure_reason: params.dig(:failure_reason) 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 end
## ##
......
...@@ -7,7 +7,7 @@ module Clusters ...@@ -7,7 +7,7 @@ module Clusters
GITLAB_ADMIN_TOKEN_NAME = 'gitlab-token' GITLAB_ADMIN_TOKEN_NAME = 'gitlab-token'
GITLAB_CLUSTER_ROLE_BINDING_NAME = 'gitlab-admin' GITLAB_CLUSTER_ROLE_BINDING_NAME = 'gitlab-admin'
GITLAB_CLUSTER_ROLE_NAME = 'cluster-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_NAME = 'gitlab-knative-serving-role'
GITLAB_KNATIVE_SERVING_ROLE_BINDING_NAME = 'gitlab-knative-serving-rolebinding' GITLAB_KNATIVE_SERVING_ROLE_BINDING_NAME = 'gitlab-knative-serving-rolebinding'
GITLAB_CROSSPLANE_DATABASE_ROLE_NAME = 'gitlab-crossplane-database-role' GITLAB_CROSSPLANE_DATABASE_ROLE_NAME = 'gitlab-crossplane-database-role'
......
...@@ -123,11 +123,9 @@ module Clusters ...@@ -123,11 +123,9 @@ module Clusters
end end
def role_binding_resource def role_binding_resource
role_name = Feature.enabled?(:kubernetes_cluster_namespace_role_admin) ? 'admin' : Clusters::Kubernetes::PROJECT_CLUSTER_ROLE_NAME
Gitlab::Kubernetes::RoleBinding.new( Gitlab::Kubernetes::RoleBinding.new(
name: role_binding_name, name: role_binding_name,
role_name: role_name, role_name: Clusters::Kubernetes::PROJECT_CLUSTER_ROLE_NAME,
role_kind: :ClusterRole, role_kind: :ClusterRole,
namespace: service_account_namespace, namespace: service_account_namespace,
service_account_name: service_account_name service_account_name: service_account_name
......
...@@ -9,7 +9,7 @@ module Packages ...@@ -9,7 +9,7 @@ module Packages
def execute def execute
if @tag_name.present? if @tag_name.present?
@tag_name.match(Gitlab::Regex.composer_package_version_regex).captures[0] @tag_name.delete_prefix('v')
elsif @branch_name.present? elsif @branch_name.present?
branch_sufix_or_prefix(@branch_name.match(Gitlab::Regex.composer_package_version_regex)) branch_sufix_or_prefix(@branch_name.match(Gitlab::Regex.composer_package_version_regex))
end end
......
- breadcrumb_title _("Dashboard") - breadcrumb_title _("Dashboard")
- page_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 - if @notices
- @notices.each do |notice| - @notices.each do |notice|
...@@ -8,7 +10,9 @@ ...@@ -8,7 +10,9 @@
= notice[:message].html_safe = notice[:message].html_safe
- if @license.present? && show_license_breakdown? - 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 .admin-dashboard.gl-mt-3
.row .row
...@@ -22,10 +26,20 @@ ...@@ -22,10 +26,20 @@
= link_to(s_('AdminArea|New project'), new_project_path, class: "btn gl-button btn-success gl-w-full") = link_to(s_('AdminArea|New project'), new_project_path, class: "btn gl-button btn-success gl-w-full")
.col-sm-4 .col-sm-4
.info-well.dark-well .info-well.dark-well
.well-segment.well-centered .well-segment.well-centered.gl-text-center
= link_to admin_users_path do = 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) } = 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 %hr
.btn-group.d-flex{ role: 'group' } .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" = link_to s_('AdminArea|New user'), new_admin_user_path, class: "btn gl-button btn-success gl-w-full"
......
...@@ -4,17 +4,7 @@ ...@@ -4,17 +4,7 @@
.container .container
.gl-mt-3 .gl-mt-3
- if Feature.enabled?(:devops_adoption) - if Feature.enabled?(:devops_adoption)
%h2 = render_if_exists 'admin/dev_ops_report/devops_tabs'
= _('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') } }
- else - else
= render 'report' = render 'report'
...@@ -2,17 +2,15 @@ ...@@ -2,17 +2,15 @@
%li.note.note-discussion.timeline-entry.unstyled-comments %li.note.note-discussion.timeline-entry.unstyled-comments
.timeline-entry-inner .timeline-entry-inner
.timeline-content .timeline-content
.discussion.js-toggle-container{ data: { discussion_id: discussion.id } } .discussion.js-toggle-container{ data: { discussion_id: discussion.id, is_expanded: expanded.to_s } }
.discussion-header .discussion-header
.timeline-icon .timeline-icon
= link_to user_path(discussion.author) do = link_to user_path(discussion.author) do
= image_tag avatar_icon_for_user(discussion.author), class: "avatar s40" = image_tag avatar_icon_for_user(discussion.author), class: "avatar s40"
.discussion-actions .discussion-actions
%button.note-action-button.discussion-toggle-button.js-toggle-button{ type: "button", class: ("js-toggle-lazy-diff" unless expanded) } %button.note-action-button.discussion-toggle-button.js-toggle-button{ type: "button", class: ("js-toggle-lazy-diff" unless expanded) }
- if expanded = sprite_icon('chevron-up', css_class: "js-sidebar-collapse #{'hidden' unless expanded}")
= icon("chevron-up") = sprite_icon('chevron-down', css_class: "js-sidebar-expand #{'hidden' if expanded}")
- else
= icon("chevron-down")
= _('Toggle thread') = _('Toggle thread')
= link_to_member(@project, discussion.author, avatar: false) = link_to_member(@project, discussion.author, avatar: false)
......
%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 @@ ...@@ -4,3 +4,4 @@
- deploy_token_description = s_('DeployTokens|Group deploy tokens allow access to the packages, repositories, and registry images within the group.') - 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 "shared/deploy_tokens/index", group_or_project: @group, description: deploy_token_description
= render "initial_branch_name", group: @group
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
%a.btn.gl-button.btn-default.float-right.gl-display-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" } %a.btn.gl-button.btn-default.float-right.gl-display-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
= sprite_icon('chevron-double-lg-left') = sprite_icon('chevron-double-lg-left')
- if Feature.enabled?(:vue_issue_header, @project) - if Feature.enabled?(:vue_issue_header, @project) && display_issuable_type == 'issue'
.js-issue-header-actions{ data: issue_header_actions_data(@project, @issue, current_user) } .js-issue-header-actions{ data: issue_header_actions_data(@project, @issue, current_user) }
- else - else
.detail-page-header-actions.js-issuable-actions.js-issuable-buttons{ data: { "action": "close-reopen" } } .detail-page-header-actions.js-issuable-actions.js-issuable-buttons{ data: { "action": "close-reopen" } }
......
- milestone_url = @milestone.project_milestone? ? project_milestone_path(@project, @milestone) : group_milestone_path(@group, @milestone) - milestone_url = @milestone.project_milestone? ? project_milestone_path(@project, @milestone) : group_milestone_path(@group, @milestone)
%button.js-delete-milestone-button.btn.btn-grouped.btn-danger{ data: { milestone_id: @milestone.id, %button.js-delete-milestone-button.btn.gl-button.btn-grouped.btn-danger{ data: { milestone_id: @milestone.id,
milestone_title: markdown_field(@milestone, :title), milestone_title: markdown_field(@milestone, :title),
milestone_url: milestone_url, milestone_url: milestone_url,
milestone_issue_count: @milestone.issues.count, milestone_issue_count: @milestone.issues.count,
......
...@@ -11,10 +11,10 @@ ...@@ -11,10 +11,10 @@
.milestone-buttons .milestone-buttons
- if can?(current_user, :admin_milestone, @group || @project) - if can?(current_user, :admin_milestone, @group || @project)
= link_to _('Edit'), edit_milestone_path(milestone), class: 'btn btn-grouped' = link_to _('Edit'), edit_milestone_path(milestone), class: 'btn gl-button btn-grouped'
- if milestone.project_milestone? && milestone.project.group - if milestone.project_milestone? && milestone.project.group
%button.js-promote-project-milestone-button.btn.btn-grouped{ data: { toggle: 'modal', %button.js-promote-project-milestone-button.btn.gl-button.btn-grouped{ data: { toggle: 'modal',
target: '#promote-milestone-modal', target: '#promote-milestone-modal',
milestone_title: milestone.title, milestone_title: milestone.title,
group_name: milestone.project.group.name, group_name: milestone.project.group.name,
...@@ -26,11 +26,11 @@ ...@@ -26,11 +26,11 @@
#promote-milestone-modal #promote-milestone-modal
- if milestone.active? - if milestone.active?
= link_to _('Close milestone'), update_milestone_path(milestone, { state_event: :close }), method: :put, class: 'btn btn-grouped btn-close' = link_to _('Close milestone'), update_milestone_path(milestone, { state_event: :close }), method: :put, class: 'btn gl-button btn-grouped btn-close'
- else - else
= link_to _('Reopen milestone'), update_milestone_path(milestone, { state_event: :activate }), method: :put, class: 'btn btn-grouped btn-reopen' = link_to _('Reopen milestone'), update_milestone_path(milestone, { state_event: :activate }), method: :put, class: 'btn gl-button btn-grouped btn-reopen'
= render 'shared/milestones/delete_button' = render 'shared/milestones/delete_button'
%button.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ type: 'button' } %button.btn.gl-button.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ type: 'button' }
= sprite_icon('chevron-double-lg-left') = sprite_icon('chevron-double-lg-left')
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
= markdown_field(label, :description) = markdown_field(label, :description)
.float-right.d-none.d-lg-block.d-xl-block .float-right.d-none.d-lg-block.d-xl-block
= link_to milestones_issues_path(options.merge(state: 'opened')), class: 'btn btn-transparent btn-action' do = link_to milestones_issues_path(options.merge(state: 'opened')), class: 'btn gl-button btn-default-tertiary btn-action' do
- pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), _('open issue') - pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), _('open issue')
= link_to milestones_issues_path(options.merge(state: 'closed')), class: 'btn btn-transparent btn-action' do = link_to milestones_issues_path(options.merge(state: 'closed')), class: 'btn gl-button btn-default-tertiary btn-action' do
- pluralize milestone_issues_by_label_count(@milestone, label, state: :closed), _('closed issue') - pluralize milestone_issues_by_label_count(@milestone, label, state: :closed), _('closed issue')
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
.milestone-actions.d-flex.justify-content-sm-start.justify-content-md-end .milestone-actions.d-flex.justify-content-sm-start.justify-content-md-end
- if @project # if in milestones list on project level - if @project # if in milestones list on project level
- if can_admin_group_milestones? - if can_admin_group_milestones?
%button.js-promote-project-milestone-button.btn.btn-blank.btn-sm.btn-grouped.has-tooltip{ title: s_('Milestones|Promote to Group Milestone'), %button.js-promote-project-milestone-button.btn.gl-button.btn-default-tertiary.btn-sm.btn-grouped.has-tooltip{ title: s_('Milestones|Promote to Group Milestone'),
disabled: true, disabled: true,
type: 'button', type: 'button',
data: { url: promote_project_milestone_path(milestone.project, milestone), data: { url: promote_project_milestone_path(milestone.project, milestone),
...@@ -59,6 +59,6 @@ ...@@ -59,6 +59,6 @@
- if can?(current_user, :admin_milestone, milestone) - if can?(current_user, :admin_milestone, milestone)
- if milestone.closed? - if milestone.closed?
= link_to s_('Milestones|Reopen Milestone'), milestone_path(milestone, milestone: { state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen" = link_to s_('Milestones|Reopen Milestone'), milestone_path(milestone, milestone: { state_event: :activate }), method: :put, class: "btn gl-button btn-sm btn-grouped btn-reopen"
- else - else
= link_to s_('Milestones|Close Milestone'), milestone_path(milestone, milestone: { state_event: :close }), method: :put, class: "btn btn-sm btn-grouped btn-close" = link_to s_('Milestones|Close Milestone'), milestone_path(milestone, milestone: { state_event: :close }), method: :put, class: "btn gl-button btn-warning-secondary btn-sm btn-grouped btn-close"
---
title: Add EC2 to AutoDevOps template
merge_request: 45651
author:
type: changed
---
title: Resolve User stuck in 2FA setup page even if group disable 2FA enforce
merge_request: 46432
author:
type: fixed
---
title: Replace fa-chevron-up with GitLab SVG icon
merge_request: 46118
author:
type: changed
---
title: Track usage of CI Secrets Management (Vault secrets)
merge_request: 46515
author:
type: added
---
title: Remove all records from `security_findings` table
merge_request: 44312
author:
type: fixed
---
title: Allow semver versions in composer packages
merge_request: 46301
author:
type: fixed
---
title: Remove feedback alert from on-demand scans form
merge_request: 45217
author:
type: changed
---
title: Populate missing `dismissed_at` and `dismissed_by_id` attributes of vulnerabilities
merge_request: 46370
author:
type: fixed
---
title: Fix IDE issues with special characters
merge_request: 46398
author:
type: fixed
---
title: Replace-GlDeprecatedDropdown-with-GlDropdown-in-app/assets/javascripts/error_tracking
merge_request: 41420
author: nuwe1
type: other
---
title: Add userDiscussionsCount to issues and merge requests GraphQL
merge_request: 46311
author:
type: added
---
title: 'Auto Deploy: fixes issues for fetching other charts from stable repo'
merge_request: 46531
author:
type: fixed
---
title: Fix logging handling for API integer params
merge_request: 46551
author:
type: fixed
---
title: Assign new incoming diff lines for renamed files to the correct view type
merge_request: 46823
author:
type: fixed
---
title: Ensure that copy to clipboard button is visible
merge_request: 46466
author:
type: fixed
---
title: Fix linebreak issue in last commit anchor
merge_request: 46643
author:
type: fixed
---
title: Add Default Initial Branch Name for Repositories Group Setting
merge_request: 43290
author:
type: added
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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