Commit 59306a42 authored by Imre Farkas's avatar Imre Farkas

Merge branch '220433-user-stuck-in-2fa-setup-page-even-if-group-disable-2fa-enforce' into 'master'

Resolve User stuck in 2FA setup page even if group disable 2FA enforce

See merge request gitlab-org/gitlab!46432
parents 959e4fab 10849667
......@@ -676,10 +676,14 @@
##################
.releases:rules:canonical-dot-com-gitlab-stable-branch-only:
rules:
- if: '$CI_COMMIT_MESSAGE =~ /\[merge-train skip\]/'
when: never
- if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_PATH == "gitlab-org/gitlab" && $CI_COMMIT_REF_NAME =~ /^[\d-]+-stable-ee$/'
.releases:rules:canonical-dot-com-security-gitlab-stable-branch-only:
rules:
- if: '$CI_COMMIT_MESSAGE =~ /\[merge-train skip\]/'
when: never
- if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_PATH == "gitlab-org/security/gitlab" && $CI_COMMIT_REF_NAME =~ /^[\d-]+-stable-ee$/'
#################
......
......@@ -16,7 +16,7 @@ inherit_mode:
- Include
AllCops:
TargetRubyVersion: 2.6
TargetRubyVersion: 2.7
TargetRailsVersion: 6.0
Exclude:
- 'vendor/**/*'
......
......@@ -183,19 +183,6 @@ RSpec/ContextWording:
RSpec/ExpectChange:
Enabled: false
# Offense count: 47
RSpec/ExpectGitlabTracking:
Exclude:
- 'spec/controllers/projects/registry/repositories_controller_spec.rb'
- 'spec/controllers/projects/registry/tags_controller_spec.rb'
- 'spec/controllers/projects/settings/operations_controller_spec.rb'
- 'spec/lib/api/helpers_spec.rb'
- 'spec/requests/api/project_container_repositories_spec.rb'
- 'spec/support/shared_examples/controllers/trackable_shared_examples.rb'
- 'spec/support/shared_examples/requests/api/discussions_shared_examples.rb'
- 'spec/support/shared_examples/requests/api/packages_shared_examples.rb'
- 'spec/support/shared_examples/requests/api/tracking_shared_examples.rb'
# Offense count: 751
RSpec/ExpectInHook:
Enabled: false
......
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)
### Security (4 changes)
......@@ -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
## 13.4.6 (2020-11-03)
### Fixed (1 change)
- Handle 500 error for GraphQL mutation. !43936
## 13.4.5 (2020-11-02)
### Security (4 changes)
......
......@@ -2,6 +2,19 @@
documentation](doc/development/changelog.md) for instructions on adding your own
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)
### Security (9 changes)
......@@ -598,6 +611,17 @@ entry.
- 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)
### Security (9 changes)
......
2b90359f63697b1e052f1385c79daffb1768a311
020b5f709d58277c360ba409b8f8a9e81cee2781
import Vue from 'vue';
import DevopsAdoptionApp from './components/devops_adoption_app.vue';
export default () => {
const el = document.querySelector('.js-devops-adoption');
if (!el) return false;
const { emptyStateSvgPath } = el.dataset;
return new Vue({
el,
provide: {
emptyStateSvgPath,
},
render(h) {
return h(DevopsAdoptionApp);
},
});
};
// EE-specific feature. Find the implementation in the `ee/`-folder
export default () => {};
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
export default {
components: {
GlLink,
GlSprintf,
},
props: {
message: {
type: String,
required: true,
},
link: {
type: String,
required: true,
},
},
};
</script>
<template>
<span class="gl-text-gray-500">
<gl-sprintf :message="message">
<template #link="{ content }">
<gl-link class="gl-display-inline-block" :href="link" target="_blank">{{
content
}}</gl-link>
</template>
</gl-sprintf>
</span>
</template>
......@@ -81,7 +81,6 @@ export default {
<div class="incident-management-list">
<h5 class="gl-font-lg">{{ $options.i18n.title }}</h5>
<gl-table
:empty-text="$options.i18n.emptyState"
:items="integrations"
:fields="$options.fields"
:busy="loading"
......@@ -115,6 +114,14 @@ export default {
<template #table-busy>
<gl-loading-icon size="lg" color="dark" class="mt-3" />
</template>
<template #empty>
<div
class="gl-border-t-solid gl-border-b-solid gl-border-1 gl-border gl-border-gray-100 mt-n3"
>
<p class="gl-text-gray-400 gl-py-3 gl-my-3">{{ $options.i18n.emptyState }}</p>
</div>
</template>
</gl-table>
</div>
</template>
......@@ -56,7 +56,7 @@ export default {
data() {
return {
loading: false,
selectedIntegration: integrationTypes[1].value,
selectedIntegration: integrationTypes[0].value,
options: integrationTypes,
active: false,
authKey: '',
......@@ -88,34 +88,34 @@ export default {
];
},
isPrometheus() {
return this.selectedIntegration === 'prometheus';
return this.selectedIntegration === 'PROMETHEUS';
},
isOpsgenie() {
return this.selectedIntegration === 'opsgenie';
return this.selectedIntegration === 'OPSGENIE';
},
selectedIntegrationType() {
switch (this.selectedIntegration) {
case 'generic': {
case 'HTTP': {
return {
url: this.generic.url,
authKey: this.generic.authorizationKey,
activated: this.generic.activated,
authKey: this.generic.authKey,
active: this.generic.active,
resetKey: this.resetKey.bind(this),
};
}
case 'prometheus': {
case 'PROMETHEUS': {
return {
url: this.prometheus.prometheusUrl,
authKey: this.prometheus.authorizationKey,
activated: this.prometheus.activated,
resetKey: this.resetKey.bind(this, 'prometheus'),
url: this.prometheus.url,
authKey: this.prometheus.authKey,
active: this.prometheus.active,
resetKey: this.resetKey.bind(this, 'PROMETHEUS'),
targetUrl: this.prometheus.prometheusApiUrl,
};
}
case 'opsgenie': {
case 'OPSGENIE': {
return {
targetUrl: this.opsgenie.opsgenieMvcTargetUrl,
activated: this.opsgenie.activated,
active: this.opsgenie.active,
};
}
default: {
......@@ -161,16 +161,12 @@ export default {
},
},
mounted() {
if (
this.prometheus.activated ||
this.generic.activated ||
!this.opsgenie.opsgenieMvcIsAvailable
) {
if (this.prometheus.active || this.generic.active || !this.opsgenie.opsgenieMvcIsAvailable) {
this.removeOpsGenieOption();
} else if (this.opsgenie.activated) {
} else if (this.opsgenie.active) {
this.setOpsgenieAsDefault();
}
this.active = this.selectedIntegrationType.activated;
this.active = this.selectedIntegrationType.active;
this.authKey = this.selectedIntegrationType.authKey ?? '';
},
methods: {
......@@ -183,19 +179,19 @@ export default {
},
setOpsgenieAsDefault() {
this.options = this.options.map(el => {
if (el.value !== 'opsgenie') {
if (el.value !== 'OPSGENIE') {
return { ...el, disabled: true };
}
return { ...el, disabled: false };
});
this.selectedIntegration = this.options.find(({ value }) => value === 'opsgenie').value;
this.selectedIntegration = this.options.find(({ value }) => value === 'OPSGENIE').value;
if (this.targetUrl === null) {
this.targetUrl = this.selectedIntegrationType.targetUrl;
}
},
removeOpsGenieOption() {
this.options = this.options.map(el => {
if (el.value !== 'opsgenie') {
if (el.value !== 'OPSGENIE') {
return { ...el, disabled: false };
}
return { ...el, disabled: true };
......@@ -204,7 +200,7 @@ export default {
resetFormValues() {
this.testAlert.json = null;
this.targetUrl = this.selectedIntegrationType.targetUrl;
this.active = this.selectedIntegrationType.activated;
this.active = this.selectedIntegrationType.active;
},
dismissFeedback() {
this.serverError = null;
......@@ -212,7 +208,7 @@ export default {
this.isFeedbackDismissed = false;
},
resetKey(key) {
const fn = key === 'prometheus' ? this.resetPrometheusKey() : this.resetGenericKey();
const fn = key === 'PROMETHEUS' ? this.resetPrometheusKey() : this.resetGenericKey();
return fn
.then(({ data: { token } }) => {
......@@ -242,9 +238,10 @@ export default {
},
toggleActivated(value) {
this.loading = true;
const path = this.isOpsgenie ? this.opsgenie.formPath : this.generic.formPath;
return service
.updateGenericActive({
endpoint: this[this.selectedIntegration].formPath,
endpoint: path,
params: this.isOpsgenie
? { service: { opsgenie_mvc_target_url: this.targetUrl, opsgenie_mvc_enabled: value } }
: { service: { active: value } },
......@@ -345,7 +342,7 @@ export default {
if (this.canSaveForm) {
this.canSaveForm = false;
this.active = this.selectedIntegrationType.activated;
this.active = this.selectedIntegrationType.active;
}
},
},
......@@ -402,9 +399,9 @@ export default {
</gl-sprintf>
</span>
</gl-form-group>
<gl-form-group :label="$options.i18n.activeLabel" label-for="activated">
<gl-form-group :label="$options.i18n.activeLabel" label-for="active">
<toggle-button
id="activated"
id="active"
:disabled-input="loading"
:is-loading="loading"
:value="active"
......
<script>
import produce from 'immer';
import { s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { fetchPolicies } from '~/lib/graphql';
import createFlash, { FLASH_TYPES } from '~/flash';
import getIntegrationsQuery from '../graphql/queries/get_integrations.query.graphql';
import createHttpIntegrationMutation from '../graphql/mutations/create_http_integration.mutation.graphql';
import createPrometheusIntegrationMutation from '../graphql/mutations/create_prometheus_integration.mutation.graphql';
import IntegrationsList from './alerts_integrations_list.vue';
import SettingsFormOld from './alerts_settings_form_old.vue';
import SettingsFormNew from './alerts_settings_form_new.vue';
import { typeSet } from '../constants';
export default {
typeSet,
i18n: {
changesSaved: s__(
'AlertsIntegrations|The integration has been successfully saved. Alerts from this new integration should now appear on your alerts list.',
),
},
components: {
IntegrationsList,
SettingsFormOld,
......@@ -49,6 +60,7 @@ export default {
data() {
return {
errored: false,
isUpdating: false,
integrations: {},
};
},
......@@ -61,16 +73,85 @@ export default {
{
name: s__('AlertSettings|HTTP endpoint'),
type: s__('AlertsIntegrations|HTTP endpoint'),
active: this.generic.activated,
active: this.generic.active,
},
{
name: s__('AlertSettings|External Prometheus'),
type: s__('AlertsIntegrations|Prometheus'),
active: this.prometheus.activated,
active: this.prometheus.active,
},
];
},
},
methods: {
onCreateNewIntegration({ type, variables }) {
this.isUpdating = true;
this.$apollo
.mutate({
mutation:
type === this.$options.typeSet.http
? createHttpIntegrationMutation
: createPrometheusIntegrationMutation,
variables: {
...variables,
projectPath: this.projectPath,
},
update: this.updateIntegrations,
})
.then(({ data: { httpIntegrationCreate, prometheusIntegrationCreate } = {} } = {}) => {
const error = httpIntegrationCreate?.errors[0] || prometheusIntegrationCreate?.errors[0];
if (error) {
return createFlash({ message: error });
}
return createFlash({
message: this.$options.i18n.changesSaved,
type: FLASH_TYPES.SUCCESS,
});
})
.catch(err => {
this.errored = true;
createFlash({ message: err });
})
.finally(() => {
this.isUpdating = false;
});
},
updateIntegrations(
store,
{
data: { httpIntegrationCreate, prometheusIntegrationCreate },
},
) {
const integration =
httpIntegrationCreate?.integration || prometheusIntegrationCreate?.integration;
if (!integration) {
return;
}
const sourceData = store.readQuery({
query: getIntegrationsQuery,
variables: {
projectPath: this.projectPath,
},
});
const data = produce(sourceData, draftData => {
// eslint-disable-next-line no-param-reassign
draftData.project.alertManagementIntegrations.nodes = [
integration,
...draftData.project.alertManagementIntegrations.nodes,
];
});
store.writeQuery({
query: getIntegrationsQuery,
variables: {
projectPath: this.projectPath,
},
data,
});
},
},
};
</script>
......@@ -80,7 +161,11 @@ export default {
:integrations="glFeatures.httpIntegrationsList ? integrations.list : intergrationsOptionsOld"
:loading="loading"
/>
<settings-form-new v-if="glFeatures.httpIntegrationsList" />
<settings-form-new
v-if="glFeatures.httpIntegrationsList"
:loading="loading"
@on-create-new-integration="onCreateNewIntegration"
/>
<settings-form-old v-else />
</div>
</template>
import { s__ } from '~/locale';
// TODO: Remove this as part of the form old removal
export const i18n = {
usageSection: s__(
'AlertSettings|You must provide this URL and authorization key to authorize an external service to send alerts to GitLab. You can provide this URL and key to multiple services. After configuring an external service, alerts from your service will display on the GitLab %{linkStart}Alerts%{linkEnd} page.',
......@@ -39,13 +40,23 @@ export const i18n = {
integration: s__('AlertSettings|Integration'),
};
// TODO: Delete as part of old form removal in 13.6
export const integrationTypes = [
{ value: 'HTTP', text: s__('AlertSettings|HTTP Endpoint') },
{ value: 'PROMETHEUS', text: s__('AlertSettings|External Prometheus') },
{ value: 'OPSGENIE', text: s__('AlertSettings|Opsgenie') },
];
export const integrationTypesNew = [
{ value: '', text: s__('AlertSettings|Select integration type') },
{ value: 'generic', text: s__('AlertSettings|HTTP Endpoint') },
{ value: 'prometheus', text: s__('AlertSettings|External Prometheus') },
{ value: 'opsgenie', text: s__('AlertSettings|Opsgenie') },
...integrationTypes,
];
export const typeSet = {
http: 'HTTP',
prometheus: 'PROMETHEUS',
};
export const JSON_VALIDATE_DELAY = 250;
export const targetPrometheusUrlPlaceholder = 'http://prometheus.example.com/';
......
#import "../fragments/integration_item.fragment.graphql"
mutation createHttpIntegration($projectPath: ID!, $name: String!, $active: Boolean!) {
httpIntegrationCreate(input: { projectPath: $projectPath, name: $name, active: $active }) {
errors
integration {
...IntegrationItem
}
}
}
#import "../fragments/integration_item.fragment.graphql"
mutation createPrometheusIntegration($projectPath: ID!, $apiUrl: String!, $active: Boolean!) {
prometheusIntegrationCreate(
input: { projectPath: $projectPath, apiUrl: $apiUrl, active: $active }
) {
errors
integration {
...IntegrationItem
}
}
}
......@@ -48,9 +48,9 @@ export default el => {
el,
provide: {
prometheus: {
activated: parseBoolean(prometheusActivated),
prometheusUrl,
authorizationKey: prometheusAuthorizationKey,
active: parseBoolean(prometheusActivated),
url: prometheusUrl,
authKey: prometheusAuthorizationKey,
prometheusFormPath,
prometheusResetKeyPath,
prometheusApiUrl,
......@@ -58,14 +58,14 @@ export default el => {
generic: {
alertsSetupUrl,
alertsUsageUrl,
activated: parseBoolean(activatedStr),
active: parseBoolean(activatedStr),
formPath,
authorizationKey,
authKey: authorizationKey,
url,
},
opsgenie: {
formPath: opsgenieMvcFormPath,
activated: parseBoolean(opsgenieMvcEnabled),
active: parseBoolean(opsgenieMvcEnabled),
opsgenieMvcTargetUrl,
opsgenieMvcIsAvailable: parseBoolean(opsgenieMvcAvailable),
},
......
......@@ -17,10 +17,13 @@ export default {
},
},
computed: {
seriesData() {
return {
full: this.formattedData.keys.map((val, idx) => [val, this.formattedData.values[idx]]),
};
barSeriesData() {
return [
{
name: 'full',
data: this.formattedData.keys.map((val, idx) => [val, this.formattedData.values[idx]]),
},
];
},
},
};
......@@ -30,7 +33,7 @@ export default {
<div class="gl-xs-w-full">
<gl-column-chart
v-if="formattedData.keys"
:data="seriesData"
:bars="barSeriesData"
:x-axis-title="__('Value')"
:y-axis-title="__('Number of events')"
:x-axis-type="'category'"
......
......@@ -70,6 +70,7 @@ const Api = {
featureFlagUserLists: '/api/:version/projects/:id/feature_flags_user_lists',
featureFlagUserList: '/api/:version/projects/:id/feature_flags_user_lists/:list_iid',
billableGroupMembersPath: '/api/:version/groups/:id/billable_members',
containerRegistryDetailsPath: '/api/:version/registry/repositories/:id/',
group(groupId, callback = () => {}) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
......@@ -106,6 +107,11 @@ const Api = {
return axios.delete(url);
},
containerRegistryDetails(registryId, options = {}) {
const url = Api.buildUrl(this.containerRegistryDetailsPath).replace(':id', registryId);
return axios.get(url, options);
},
groupMembers(id, options) {
const url = Api.buildUrl(this.groupMembersPath).replace(':id', encodeURIComponent(id));
......
......@@ -12,11 +12,19 @@ import { getLocationHash } from '../lib/utils/url_utility';
$(() => {
function toggleContainer(container, toggleState) {
const $container = $(container);
$container
.find('.js-toggle-button .fa-chevron-up, .js-toggle-button .fa-chevron-down')
.toggleClass('fa-chevron-up', toggleState)
.toggleClass('fa-chevron-down', toggleState !== undefined ? !toggleState : undefined);
const isExpanded = $container.data('is-expanded');
const $collapseIcon = $container.find('.js-sidebar-collapse');
const $expandIcon = $container.find('.js-sidebar-expand');
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);
}
......
<script>
import { ApolloMutation } from 'vue-apollo';
import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql';
import destroyDesignMutation from '../graphql/mutations/destroy_design.mutation.graphql';
import { updateStoreAfterDesignsDelete } from '../utils/cache_update';
......
<script>
import { GlButton, GlIcon } from '@gitlab/ui';
import permissionsQuery from 'shared_queries/design_management/design_permissions.query.graphql';
import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import DesignNavigation from './design_navigation.vue';
import DeleteButton from '../delete_button.vue';
import permissionsQuery from '../../graphql/queries/design_permissions.query.graphql';
import { DESIGNS_ROUTE_NAME } from '../../router/constants';
export default {
......
#import "../fragments/design.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) {
id
issue(iid: $iid) {
......
import { propertyOf } from 'lodash';
import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql';
import createFlash, { FLASH_TYPES } from '~/flash';
import { s__ } from '~/locale';
import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
import allVersionsMixin from './all_versions';
import { DESIGNS_ROUTE_NAME } from '../router/constants';
......
import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql';
import { findVersionId } from '../utils/design_management_utils';
export default {
......
<script>
import { GlLoadingIcon, GlButton, GlAlert } from '@gitlab/ui';
import VueDraggable from 'vuedraggable';
import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql';
import permissionsQuery from 'shared_queries/design_management/design_permissions.query.graphql';
import createFlash, { FLASH_TYPES } from '~/flash';
import { s__, sprintf } from '~/locale';
import { getFilename } from '~/lib/utils/file_upload';
......@@ -12,8 +14,6 @@ import DesignVersionDropdown from '../components/upload/design_version_dropdown.
import DesignDropzone from '../components/upload/design_dropzone.vue';
import uploadDesignMutation from '../graphql/mutations/upload_design.mutation.graphql';
import moveDesignMutation from '../graphql/mutations/move_design.mutation.graphql';
import permissionsQuery from '../graphql/queries/design_permissions.query.graphql';
import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
import allDesignsMixin from '../mixins/all_designs';
import {
UPLOAD_DESIGN_ERROR,
......
......@@ -626,7 +626,7 @@ export function switchToFullDiffFromRenamedFile({ commit, dispatch, state }, { d
.then(({ data }) => {
const lines = data.map((line, index) =>
prepareLineForRenamedFile({
diffViewType: state.diffViewType,
diffViewType: window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType,
line,
diffFile,
index,
......@@ -638,6 +638,7 @@ export function switchToFullDiffFromRenamedFile({ commit, dispatch, state }, { d
viewer: {
...diffFile.alternate_viewer,
automaticallyCollapsed: false,
manuallyCollapsed: false,
},
});
commit(types.SET_CURRENT_VIEW_DIFF_FILE_LINES, { filePath: diffFile.file_path, lines });
......
......@@ -378,8 +378,13 @@ export default {
},
[types.SET_CURRENT_VIEW_DIFF_FILE_LINES](state, { filePath, lines }) {
const file = state.diffFiles.find(f => f.file_path === filePath);
const currentDiffLinesKey =
state.diffViewType === 'inline' ? 'highlighted_diff_lines' : 'parallel_diff_lines';
let currentDiffLinesKey;
if (window.gon?.features?.unifiedDiffLines || state.diffViewType === 'inline') {
currentDiffLinesKey = 'highlighted_diff_lines';
} else {
currentDiffLinesKey = 'parallel_diff_lines';
}
file[currentDiffLinesKey] = lines;
},
......
......@@ -27,7 +27,7 @@ export default {
rolloutUserListLabel: s__('FeatureFlag|User List'),
rolloutUserListDescription: s__('FeatureFlag|Select a user list'),
rolloutUserListNoListError: s__('FeatureFlag|There are no configured user lists'),
defaultDropdownText: s__('FeatureFlags|Select a user list'),
defaultDropdownText: s__('FeatureFlags|No user list selected'),
},
computed: {
...mapGetters(['hasUserLists', 'isLoading', 'hasError', 'userListOptions']),
......@@ -36,7 +36,7 @@ export default {
return this.strategy?.userList?.id ?? '';
},
dropdownText() {
return this.strategy?.userList?.name ?? this.$options.defaultDropdownText;
return this.strategy?.userList?.name ?? this.$options.translations.defaultDropdownText;
},
},
mounted() {
......
......@@ -3,9 +3,8 @@
import $ from 'jquery';
import 'vendor/jquery.scrollTo';
import { GlLoadingIcon } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { GlLoadingIcon, GlModal } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import { HIDDEN_CLASS } from '~/lib/utils/constants';
import { getParameterByName } from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
......@@ -16,8 +15,8 @@ import groupsComponent from './groups.vue';
export default {
components: {
DeprecatedModal,
groupsComponent,
GlModal,
GlLoadingIcon,
},
props: {
......@@ -49,13 +48,30 @@ export default {
isLoading: true,
isSearchEmpty: false,
searchEmptyMessage: '',
showModal: false,
groupLeaveConfirmationMessage: '',
targetGroup: null,
targetParentGroup: null,
};
},
computed: {
primaryProps() {
return {
text: __('Leave group'),
attributes: [{ variant: 'warning' }, { category: 'primary' }],
};
},
cancelProps() {
return {
text: __('Cancel'),
};
},
groupLeaveConfirmationMessage() {
if (!this.targetGroup) {
return '';
}
return sprintf(s__('GroupsTree|Are you sure you want to leave the "%{fullName}" group?'), {
fullName: this.targetGroup.fullName,
});
},
groups() {
return this.store.getGroups();
},
......@@ -171,27 +187,17 @@ export default {
}
},
showLeaveGroupModal(group, parentGroup) {
const { fullName } = group;
this.targetGroup = group;
this.targetParentGroup = parentGroup;
this.showModal = true;
this.groupLeaveConfirmationMessage = sprintf(
s__('GroupsTree|Are you sure you want to leave the "%{fullName}" group?'),
{ fullName },
);
},
hideLeaveGroupModal() {
this.showModal = false;
},
leaveGroup() {
this.showModal = false;
this.targetGroup.isBeingRemoved = true;
this.service
.leaveGroup(this.targetGroup.leavePath)
.then(res => {
$.scrollTo(0);
this.store.removeGroup(this.targetGroup, this.targetParentGroup);
Flash(res.data.notice, 'notice');
this.$toast.show(res.data.notice);
})
.catch(err => {
let message = COMMON_STR.FAILURE;
......@@ -245,21 +251,21 @@ export default {
class="loading-animation prepend-top-20"
/>
<groups-component
v-if="!isLoading"
v-else
:groups="groups"
:search-empty="isSearchEmpty"
:search-empty-message="searchEmptyMessage"
:page-info="pageInfo"
:action="action"
/>
<deprecated-modal
v-show="showModal"
:primary-button-label="__('Leave')"
<gl-modal
modal-id="leave-group-modal"
:title="__('Are you sure?')"
:text="groupLeaveConfirmationMessage"
kind="warning"
@cancel="hideLeaveGroupModal"
@submit="leaveGroup"
/>
:action-primary="primaryProps"
:action-cancel="cancelProps"
@primary="leaveGroup"
>
{{ groupLeaveConfirmationMessage }}
</gl-modal>
</div>
</template>
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { GlTooltipDirective, GlButton, GlModalDirective } from '@gitlab/ui';
import eventHub from '../event_hub';
import { COMMON_STR } from '../constants';
export default {
components: {
GlIcon,
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
GlModal: GlModalDirective,
},
props: {
parentGroup: {
......@@ -44,28 +45,28 @@ export default {
<template>
<div class="controls d-flex justify-content-end">
<a
<gl-button
v-if="group.canLeave"
v-gl-tooltip.top
:href="group.leavePath"
v-gl-modal.leave-group-modal
:title="leaveBtnTitle"
:aria-label="leaveBtnTitle"
data-testid="leave-group-btn"
class="leave-group btn btn-xs no-expand gl-text-gray-500 gl-ml-5"
@click.prevent="onLeaveGroup"
>
<gl-icon name="leave" class="position-top-0" />
</a>
<a
size="small"
icon="leave"
class="leave-group gl-ml-3"
@click.stop="onLeaveGroup"
/>
<gl-button
v-if="group.canEdit"
v-gl-tooltip.top
:href="group.editPath"
:title="editBtnTitle"
:aria-label="editBtnTitle"
data-testid="edit-group-btn"
class="edit-group btn btn-xs no-expand gl-text-gray-500 gl-ml-5"
>
<gl-icon name="settings" class="position-top-0 align-middle" />
</a>
size="small"
icon="pencil"
class="edit-group gl-ml-3"
/>
</div>
</template>
import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
import { parseBoolean } from '~/lib/utils/common_utils';
import Translate from '../vue_shared/translate';
import GroupFilterableList from './groups_filterable_list';
......@@ -31,6 +32,8 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
Vue.component('group-folder', groupFolderComponent);
Vue.component('group-item', groupItemComponent);
Vue.use(GlToast);
// eslint-disable-next-line no-new
new Vue({
el,
......
......@@ -35,18 +35,14 @@ export default {
};
},
computed: {
chartData() {
const queryData = this.graphData.metrics.reduce((acc, query) => {
barChartData() {
return this.graphData.metrics.reduce((acc, query) => {
const series = makeDataSeries(query.result || [], {
name: this.formatLegendLabel(query),
});
return acc.concat(series);
}, []);
return {
values: queryData[0].data,
};
},
chartOptions() {
const xAxis = getTimeAxisOptions({ timezone: this.timezone });
......@@ -109,7 +105,7 @@ export default {
<gl-column-chart
ref="columnChart"
v-bind="$attrs"
:data="chartData"
:bars="barChartData"
:option="chartOptions"
:width="width"
:height="height"
......
......@@ -61,14 +61,16 @@ export default {
},
computed: {
chartData() {
return this.graphData.metrics.map(({ result }) => {
// This needs a fix. Not only metrics[0] should be shown.
// See https://gitlab.com/gitlab-org/gitlab/-/issues/220492
if (!result || result.length === 0) {
return [];
}
return result[0].values.map(val => val[1]);
});
return this.graphData.metrics
.map(({ label: name, result }) => {
// This needs a fix. Not only metrics[0] should be shown.
// See https://gitlab.com/gitlab-org/gitlab/-/issues/220492
if (!result || result.length === 0) {
return [];
}
return { name, data: result[0].values.map(val => val[1]) };
})
.slice(0, 1);
},
xAxisTitle() {
return this.graphData.x_label !== undefined ? this.graphData.x_label : '';
......@@ -136,7 +138,7 @@ export default {
<gl-stacked-column-chart
ref="chart"
v-bind="$attrs"
:data="chartData"
:bars="chartData"
:option="chartOptions"
:x-axis-title="xAxisTitle"
:y-axis-title="yAxisTitle"
......@@ -144,7 +146,6 @@ export default {
:group-by="groupBy"
:width="width"
:height="height"
:series-names="seriesNames"
:legend-layout="legendLayout"
:legend-average-text="legendAverageText"
:legend-current-text="legendCurrentText"
......
......@@ -116,7 +116,7 @@ export default {
<gl-dropdown
v-if="displayFilters"
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"
:text="currentFilter.title"
>
......
......@@ -65,8 +65,8 @@ export default {
};
},
computed: {
toggleChevronClass() {
return this.expanded ? 'fa-chevron-up' : 'fa-chevron-down';
toggleChevronIconName() {
return this.expanded ? 'chevron-up' : 'chevron-down';
},
noteTimestampLink() {
return this.noteId ? `#note_${this.noteId}` : undefined;
......@@ -133,7 +133,7 @@ export default {
type="button"
@click="handleToggle"
>
<i ref="chevronIcon" :class="toggleChevronClass" class="fa" aria-hidden="true"></i>
<gl-icon ref="chevronIcon" :name="toggleChevronIconName" aria-hidden="true" />
{{ __('Toggle thread') }}
</button>
</div>
......
import initDevopAdoption from 'ee_else_ce/admin/dev_ops_report/devops_adoption';
import initDevOpsScoreEmptyState from '~/admin/dev_ops_report/devops_score_empty_state';
import initDevopAdoption from '~/admin/dev_ops_report/devops_adoption';
initDevOpsScoreEmptyState();
initDevopAdoption();
......@@ -5,6 +5,8 @@ import { __ } from '~/locale';
import CodeCoverage from '../components/code_coverage.vue';
import SeriesDataMixin from './series_data_mixin';
const seriesDataToBarData = raw => Object.entries(raw).map(([name, data]) => ({ name, data }));
document.addEventListener('DOMContentLoaded', () => {
waitForCSSLoaded(() => {
const languagesContainer = document.getElementById('js-languages-chart');
......@@ -41,13 +43,13 @@ document.addEventListener('DOMContentLoaded', () => {
},
computed: {
seriesData() {
return { full: this.chartData.map(d => [d.label, d.value]) };
return [{ name: 'full', data: this.chartData.map(d => [d.label, d.value]) }];
},
},
render(h) {
return h(GlColumnChart, {
props: {
data: this.seriesData,
bars: this.seriesData,
xAxisTitle: __('Used programming language'),
yAxisTitle: __('Percentage'),
xAxisType: 'category',
......@@ -86,7 +88,7 @@ document.addEventListener('DOMContentLoaded', () => {
render(h) {
return h(GlColumnChart, {
props: {
data: this.seriesData,
bars: seriesDataToBarData(this.seriesData),
xAxisTitle: __('Day of month'),
yAxisTitle: __('No. of commits'),
xAxisType: 'category',
......@@ -113,13 +115,13 @@ document.addEventListener('DOMContentLoaded', () => {
acc.push([key, weekDays[key]]);
return acc;
}, []);
return { full: data };
return [{ name: 'full', data }];
},
},
render(h) {
return h(GlColumnChart, {
props: {
data: this.seriesData,
bars: this.seriesData,
xAxisTitle: __('Weekday'),
yAxisTitle: __('No. of commits'),
xAxisType: 'category',
......@@ -143,7 +145,7 @@ document.addEventListener('DOMContentLoaded', () => {
render(h) {
return h(GlColumnChart, {
props: {
data: this.seriesData,
bars: seriesDataToBarData(this.seriesData),
xAxisTitle: __('Hour (UTC)'),
yAxisTitle: __('No. of commits'),
xAxisType: 'category',
......
......@@ -16,6 +16,11 @@ export default {
</script>
<template>
<div class="gl-border-solid gl-border-gray-100 gl-border-1">
<editor-lite v-model="value" file-name="*.yml" :editor-options="{ readOnly: true }" />
<editor-lite
v-model="value"
file-name="*.yml"
:editor-options="{ readOnly: true }"
@editor-ready="$emit('editor-ready')"
/>
</div>
</template>
<script>
import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import { GlLoadingIcon, GlAlert, GlTabs, GlTab } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import TextEditor from './components/text_editor.vue';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import getBlobContent from './graphql/queries/blob_content.graphql';
......@@ -10,7 +11,10 @@ export default {
components: {
GlLoadingIcon,
GlAlert,
GlTabs,
GlTab,
TextEditor,
PipelineGraph,
},
props: {
projectPath: {
......@@ -31,6 +35,7 @@ export default {
return {
error: null,
content: '',
editorIsReady: false,
};
},
apollo: {
......@@ -66,10 +71,16 @@ export default {
const reason = networkReason ?? generalReason ?? this.$options.i18n.unknownError;
return sprintf(this.$options.i18n.errorMessageWithReason, { reason });
},
pipelineData() {
// Note data will loaded as part of https://gitlab.com/gitlab-org/gitlab/-/issues/263141
return {};
},
},
i18n: {
unknownError: __('Unknown Error'),
errorMessageWithReason: s__('Pipelines|CI file could not be loaded: %{reason}'),
tabEdit: s__('Pipelines|Write pipeline configuration'),
tabGraph: s__('Pipelines|Visualize'),
},
};
</script>
......@@ -79,7 +90,19 @@ export default {
<gl-alert v-if="error" :dismissible="false" variant="danger">{{ errorMessage }}</gl-alert>
<div class="gl-mt-4">
<gl-loading-icon v-if="loading" size="lg" />
<text-editor v-else v-model="content" />
<div v-else class="file-editor">
<gl-tabs>
<!-- editor should be mounted when its tab is visible, so the container has a size -->
<gl-tab :title="$options.i18n.tabEdit" :lazy="!editorIsReady">
<!-- editor should be mounted only once, when the tab is displayed -->
<text-editor v-model="content" @editor-ready="editorIsReady = true" />
</gl-tab>
<gl-tab :title="$options.i18n.tabGraph">
<pipeline-graph :pipeline-data="pipelineData" />
</gl-tab>
</gl-tabs>
</div>
</div>
</div>
</template>
......@@ -45,9 +45,12 @@ export default {
},
data() {
return {
timesChartTransformedData: {
full: this.mergeLabelsAndValues(this.timesChartData.labels, this.timesChartData.values),
},
timesChartTransformedData: [
{
name: 'full',
data: this.mergeLabelsAndValues(this.timesChartData.labels, this.timesChartData.values),
},
],
};
},
computed: {
......@@ -128,7 +131,7 @@ export default {
<gl-column-chart
:height="$options.chartContainerHeight"
:option="$options.timesChartOptions"
:data="timesChartTransformedData"
:bars="timesChartTransformedData"
:y-axis-title="__('Minutes')"
:x-axis-title="__('Commit')"
x-axis-type="category"
......
......@@ -15,6 +15,10 @@ export const DELETE_TAGS_SUCCESS_MESSAGE = s__(
'ContainerRegistry|Tags successfully marked for deletion.',
);
export const FETCH_IMAGE_DETAILS_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while fetching the image details.',
);
export const TAGS_LIST_TITLE = s__('ContainerRegistry|Image tags');
export const DIGEST_LABEL = s__('ContainerRegistry|Digest: %{imageId}');
export const CREATED_AT_LABEL = s__('ContainerRegistry|Published %{timeInfo}');
......
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import Api from '~/api';
import * as types from './mutation_types';
import {
FETCH_IMAGES_LIST_ERROR_MESSAGE,
DEFAULT_PAGE,
DEFAULT_PAGE_SIZE,
FETCH_TAGS_LIST_ERROR_MESSAGE,
FETCH_IMAGE_DETAILS_ERROR_MESSAGE,
} from '../constants/index';
import { decodeAndParse } from '../utils';
......@@ -61,6 +63,19 @@ export const requestTagsList = ({ commit, dispatch }, { pagination = {}, params
});
};
export const requestImageDetailsAndTagsList = ({ dispatch, commit }, id) => {
commit(types.SET_MAIN_LOADING, true);
return Api.containerRegistryDetails(id)
.then(({ data }) => {
commit(types.SET_IMAGE_DETAILS, data);
dispatch('requestTagsList');
})
.catch(() => {
createFlash(FETCH_IMAGE_DETAILS_ERROR_MESSAGE);
commit(types.SET_MAIN_LOADING, false);
});
};
export const requestDeleteTag = ({ commit, dispatch, state }, { tag, params }) => {
commit(types.SET_MAIN_LOADING, true);
return axios
......
......@@ -7,3 +7,4 @@ export const SET_MAIN_LOADING = 'SET_MAIN_LOADING';
export const SET_TAGS_PAGINATION = 'SET_TAGS_PAGINATION';
export const SET_TAGS_LIST_SUCCESS = 'SET_TAGS_LIST_SUCCESS';
export const SET_SHOW_GARBAGE_COLLECTION_TIP = 'SET_SHOW_GARBAGE_COLLECTION_TIP';
export const SET_IMAGE_DETAILS = 'SET_IMAGE_DETAILS';
......@@ -47,4 +47,8 @@ export default {
const normalizedHeaders = normalizeHeaders(headers);
state.tagsPagination = parseIntPagination(normalizedHeaders);
},
[types.SET_IMAGE_DETAILS](state, details) {
state.imageDetails = details;
},
};
......@@ -3,6 +3,7 @@ export default () => ({
showGarbageCollectionTip: false,
config: {},
images: [],
imageDetails: {},
tags: [],
pagination: {},
tagsPagination: {},
......
export const decodeAndParse = param => JSON.parse(window.atob(param));
// eslint-disable-next-line @gitlab/require-i18n-strings
export const pathGenerator = (imageDetails, ending = 'tags?format=json') => {
// this method is a temporary workaround, to be removed with graphql implementation
// https://gitlab.com/gitlab-org/gitlab/-/issues/276432
const basePath = imageDetails.path.replace(`/${imageDetails.name}`, '');
return `/${basePath}/registry/repository/${imageDetails.id}/${ending}`;
};
......@@ -137,8 +137,8 @@ export default {
:href="commit.author.webPath"
class="commit-author-link js-user-link"
>
{{ commit.author.name }}
</gl-link>
{{ commit.author.name }}</gl-link
>
<template v-else>
{{ commit.authorName }}
</template>
......
......@@ -16,12 +16,7 @@ import { performanceMarkAndMeasure } from '~/performance_utils';
import UpdateSnippetMutation from '../mutations/updateSnippet.mutation.graphql';
import CreateSnippetMutation from '../mutations/createSnippet.mutation.graphql';
import { getSnippetMixin } from '../mixins/snippets';
import {
SNIPPET_CREATE_MUTATION_ERROR,
SNIPPET_UPDATE_MUTATION_ERROR,
SNIPPET_VISIBILITY_PRIVATE,
} from '../constants';
import defaultVisibilityQuery from '../queries/snippet_visibility.query.graphql';
import { SNIPPET_CREATE_MUTATION_ERROR, SNIPPET_UPDATE_MUTATION_ERROR } from '../constants';
import { markBlobPerformance } from '../utils/blob';
import SnippetBlobActionsEdit from './snippet_blob_actions_edit.vue';
......@@ -41,15 +36,7 @@ export default {
GlLoadingIcon,
},
mixins: [getSnippetMixin],
apollo: {
defaultVisibility: {
query: defaultVisibilityQuery,
manual: true,
result({ data: { selectedLevel } }) {
this.selectedLevelDefault = selectedLevel;
},
},
},
inject: ['selectedLevel'],
props: {
markdownPreviewPath: {
type: String,
......@@ -73,9 +60,12 @@ export default {
data() {
return {
isUpdating: false,
newSnippet: false,
actions: [],
selectedLevelDefault: SNIPPET_VISIBILITY_PRIVATE,
snippet: {
title: '',
description: '',
visibilityLevel: this.selectedLevel,
},
};
},
computed: {
......@@ -112,13 +102,6 @@ export default {
}
return this.snippet.webUrl;
},
newSnippetSchema() {
return {
title: '',
description: '',
visibilityLevel: this.selectedLevelDefault,
};
},
},
beforeCreate() {
performanceMarkAndMeasure({ mark: SNIPPET_MARK_EDIT_APP_START });
......@@ -145,20 +128,6 @@ export default {
Flash(sprintf(defaultErrorMsg, { err }));
this.isUpdating = false;
},
onNewSnippetFetched() {
this.newSnippet = true;
this.snippet = this.newSnippetSchema;
},
onExistingSnippetFetched() {
this.newSnippet = false;
},
onSnippetFetch(snippetRes) {
if (snippetRes.data.snippets.nodes.length === 0) {
this.onNewSnippetFetched();
} else {
this.onExistingSnippetFetched();
}
},
getAttachedFiles() {
const fileInputs = Array.from(this.$el.querySelectorAll('[name="files[]"]'));
return fileInputs.map(node => node.value);
......@@ -209,7 +178,7 @@ export default {
</script>
<template>
<form
class="snippet-form js-requires-input js-quick-submit common-note-form"
class="snippet-form js-quick-submit common-note-form"
:data-snippet-type="isProjectSnippet ? 'project' : 'personal'"
data-testid="snippet-edit-form"
@submit.prevent="handleFormSubmit"
......
<script>
import { GlIcon, GlFormGroup, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
import defaultVisibilityQuery from '../queries/snippet_visibility.query.graphql';
import { defaultSnippetVisibilityLevels } from '../utils/blob';
import { SNIPPET_LEVELS_RESTRICTED, SNIPPET_LEVELS_DISABLED } from '~/snippets/constants';
......@@ -12,16 +11,7 @@ export default {
GlFormRadioGroup,
GlLink,
},
apollo: {
defaultVisibility: {
query: defaultVisibilityQuery,
manual: true,
result({ data: { visibilityLevels, multipleLevelsRestricted } }) {
this.visibilityLevels = defaultSnippetVisibilityLevels(visibilityLevels);
this.multipleLevelsRestricted = multipleLevelsRestricted;
},
},
},
inject: ['visibilityLevels', 'multipleLevelsRestricted'],
props: {
helpLink: {
type: String,
......@@ -38,11 +28,10 @@ export default {
required: true,
},
},
data() {
return {
visibilityLevels: [],
multipleLevelsRestricted: false,
};
computed: {
defaultVisibilityLevels() {
return defaultSnippetVisibilityLevels(this.visibilityLevels);
},
},
SNIPPET_LEVELS_DISABLED,
SNIPPET_LEVELS_RESTRICTED,
......@@ -59,7 +48,7 @@ export default {
<gl-form-group id="visibility-level-setting" class="gl-mb-0">
<gl-form-radio-group :checked="value" stacked v-bind="$attrs" v-on="$listeners">
<gl-form-radio
v-for="option in visibilityLevels"
v-for="option in defaultVisibilityLevels"
:key="option.value"
:value="option.value"
class="mb-3"
......@@ -78,7 +67,9 @@ export default {
</gl-form-group>
<div class="text-muted" data-testid="restricted-levels-info">
<template v-if="!visibilityLevels.length">{{ $options.SNIPPET_LEVELS_DISABLED }}</template>
<template v-if="!defaultVisibilityLevels.length">{{
$options.SNIPPET_LEVELS_DISABLED
}}</template>
<template v-else-if="multipleLevelsRestricted">{{
$options.SNIPPET_LEVELS_RESTRICTED
}}</template>
......
......@@ -24,17 +24,14 @@ export default function appFactory(el, Component) {
...restDataset
} = el.dataset;
apolloProvider.clients.defaultClient.cache.writeData({
data: {
return new Vue({
el,
apolloProvider,
provide: {
visibilityLevels: JSON.parse(visibilityLevels),
selectedLevel: SNIPPET_LEVELS_MAP[selectedLevel] ?? SNIPPET_VISIBILITY_PRIVATE,
multipleLevelsRestricted: 'multipleLevelsRestricted' in el.dataset,
},
});
return new Vue({
el,
apolloProvider,
render(createElement) {
return createElement(Component, {
props: {
......
......@@ -21,9 +21,9 @@ export const getSnippetMixin = {
},
result(res) {
this.blobs = res.data.snippets.nodes[0]?.blobs || blobsDefault;
if (this.onSnippetFetch) {
this.onSnippetFetch(res);
}
},
skip() {
return this.newSnippet;
},
},
},
......@@ -36,7 +36,7 @@ export const getSnippetMixin = {
data() {
return {
snippet: {},
newSnippet: false,
newSnippet: !this.snippetGid,
blobs: blobsDefault,
};
},
......
query defaultSnippetVisibility {
visibilityLevels @client
selectedLevel @client
multipleLevelsRestricted @client
}
......@@ -51,6 +51,7 @@ export const FIELDS = [
key: 'actions',
thClass: 'col-actions',
tdClass: 'col-actions',
showFunction: 'showActionsField',
},
];
......
......@@ -2,6 +2,12 @@
import { mapState } from 'vuex';
import { GlTable, GlBadge } from '@gitlab/ui';
import MembersTableCell from 'ee_else_ce/vue_shared/components/members/table/members_table_cell.vue';
import {
canOverride,
canRemove,
canResend,
canUpdate,
} from 'ee_else_ce/vue_shared/components/members/utils';
import { FIELDS } from '../constants';
import initUserPopovers from '~/user_popovers';
import MemberAvatar from './member_avatar.vue';
......@@ -33,14 +39,40 @@ export default {
),
},
computed: {
...mapState(['members', 'tableFields']),
...mapState(['members', 'tableFields', 'currentUserId', 'sourceId']),
filteredFields() {
return FIELDS.filter(field => this.tableFields.includes(field.key));
return FIELDS.filter(field => this.tableFields.includes(field.key) && this.showField(field));
},
userIsLoggedIn() {
return this.currentUserId !== null;
},
},
mounted() {
initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
},
methods: {
showField(field) {
if (!Object.prototype.hasOwnProperty.call(field, 'showFunction')) {
return true;
}
return this[field.showFunction]();
},
showActionsField() {
if (!this.userIsLoggedIn) {
return false;
}
return this.members.some(member => {
return (
canRemove(member, this.sourceId) ||
canResend(member) ||
canUpdate(member, this.currentUserId, this.sourceId) ||
canOverride(member)
);
});
},
},
};
</script>
......
<script>
import { mapState } from 'vuex';
import { MEMBER_TYPES } from '../constants';
import { isGroup, isDirectMember, isCurrentUser, canRemove, canResend, canUpdate } from '../utils';
export default {
name: 'MembersTableCell',
......@@ -13,7 +14,7 @@ export default {
computed: {
...mapState(['sourceId', 'currentUserId']),
isGroup() {
return Boolean(this.member.sharedWithGroup);
return isGroup(this.member);
},
isInvite() {
return Boolean(this.member.invite);
......@@ -33,19 +34,19 @@ export default {
return MEMBER_TYPES.user;
},
isDirectMember() {
return this.isGroup || this.member.source?.id === this.sourceId;
return isDirectMember(this.member, this.sourceId);
},
isCurrentUser() {
return this.member.user?.id === this.currentUserId;
return isCurrentUser(this.member, this.currentUserId);
},
canRemove() {
return this.isDirectMember && this.member.canRemove;
return canRemove(this.member, this.sourceId);
},
canResend() {
return Boolean(this.member.invite?.canResend);
return canResend(this.member);
},
canUpdate() {
return !this.isCurrentUser && this.isDirectMember && this.member.canUpdate;
return canUpdate(this.member, this.currentUserId, this.sourceId);
},
},
render() {
......
......@@ -17,3 +17,32 @@ export const generateBadges = (member, isCurrentUser) => [
variant: 'info',
},
];
export const isGroup = member => {
return Boolean(member.sharedWithGroup);
};
export const isDirectMember = (member, sourceId) => {
return isGroup(member) || member.source?.id === sourceId;
};
export const isCurrentUser = (member, currentUserId) => {
return member.user?.id === currentUserId;
};
export const canRemove = (member, sourceId) => {
return isDirectMember(member, sourceId) && member.canRemove;
};
export const canResend = member => {
return Boolean(member.invite?.canResend);
};
export const canUpdate = (member, currentUserId, sourceId) => {
return (
!isCurrentUser(member, currentUserId) && isDirectMember(member, sourceId) && member.canUpdate
);
};
// Defined in `ee/app/assets/javascripts/vue_shared/components/members/utils.js`
export const canOverride = () => false;
......@@ -109,10 +109,6 @@
content: '\f0da';
}
.fa-chevron-up::before {
content: '\f077';
}
.fa-exclamation-circle::before {
content: '\f06a';
}
......
......@@ -51,7 +51,7 @@ class Projects::IssuesController < Projects::ApplicationController
real_time_feature_flag = :real_time_issue_sidebar
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_b)
......
......@@ -40,7 +40,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:highlight_current_diff_row, @project)
push_frontend_feature_flag(:default_merge_ref_for_diffs, @project)
push_frontend_feature_flag(:core_security_mr_widget, @project, default_enabled: true)
push_frontend_feature_flag(:remove_resolve_note, @project)
push_frontend_feature_flag(:remove_resolve_note, @project, default_enabled: true)
record_experiment_user(:invite_members_version_a)
record_experiment_user(:invite_members_version_b)
......@@ -318,7 +318,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def export_csv
return render_404 unless Feature.enabled?(:export_merge_requests_as_csv, project)
return render_404 unless Feature.enabled?(:export_merge_requests_as_csv, project, default_enabled: true)
IssuableExportCsvWorker.perform_async(:merge_request, current_user.id, project.id, finder_options.to_h) # rubocop:disable CodeReuse/Worker
......
......@@ -75,7 +75,7 @@ module Projects
[
:runners_token, :builds_enabled, :build_allow_git_fetch,
:build_timeout_human_readable, :build_coverage_regex, :public_builds,
:auto_cancel_pending_pipelines, :ci_config_path,
:auto_cancel_pending_pipelines, :ci_config_path, :auto_rollback_enabled,
auto_devops_attributes: [:id, :domain, :enabled, :deploy_strategy],
ci_cd_settings_attributes: [:default_git_depth, :forward_deployment_enabled]
].tap do |list|
......
......@@ -18,14 +18,13 @@ module Projects
end
def cleanup
cleanup_params = params.require(:project).permit(:bfg_object_map)
result = Projects::UpdateService.new(project, current_user, cleanup_params).execute
bfg_object_map = params.require(:project).require(:bfg_object_map)
result = Projects::CleanupService.enqueue(project, current_user, bfg_object_map)
if result[:status] == :success
RepositoryCleanupWorker.perform_async(project.id, current_user.id) # rubocop:disable CodeReuse/Worker
flash[:notice] = _('Repository cleanup has started. You will receive an email once the cleanup operation is complete.')
else
flash[:alert] = _('Failed to upload object map file')
flash[:alert] = status.fetch(:message, _('Failed to upload object map file'))
end
redirect_to project_settings_repository_path(project)
......
......@@ -32,7 +32,7 @@ module FinderWithCrossProjectAccess
end
override :execute
def execute(*args)
def execute(*args, **kwargs)
check = Gitlab::CrossProjectAccess.find_check(self)
original = -> { super }
......
# frozen_string_literal: true
module Mutations
module AlertManagement
module HttpIntegration
class Destroy < HttpIntegrationBase
graphql_name 'HttpIntegrationDestroy'
argument :id, Types::GlobalIDType[::AlertManagement::HttpIntegration],
required: true,
description: "The id of the integration to remove"
def resolve(id:)
integration = authorized_find!(id: id)
response ::AlertManagement::HttpIntegrations::DestroyService.new(
integration,
current_user
).execute
end
end
end
end
end
......@@ -7,7 +7,7 @@ module Mutations
field :integration,
Types::AlertManagement::HttpIntegrationType,
null: true,
description: "The updated HTTP integration"
description: "The HTTP integration"
authorize :admin_operations
......
query permissions($fullPath: ID!, $iid: String!) {
project(fullPath: $fullPath) {
__typename
id
issue(iid: $iid) {
__typename
userPermissions {
__typename
createDesign
}
}
......
#import "../fragments/design_list.fragment.graphql"
#import "../fragments/version.fragment.graphql"
query getDesignList($fullPath: ID!, $iid: String!, $atVersion: ID) {
project(fullPath: $fullPath) {
__typename
id
issue(iid: $iid) {
__typename
designCollection {
__typename
copyState
designs(atVersion: $atVersion) {
__typename
nodes {
...DesignListItem
__typename
id
event
filename
notesCount
image
imageV432x230
currentUserTodos(state: pending) {
__typename
nodes {
__typename
id
}
}
}
}
versions {
__typename
nodes {
...VersionListItem
__typename
id
sha
}
}
}
......
......@@ -13,10 +13,20 @@ module Resolvers
required: true,
description: 'The type of measurement/statistics to retrieve'
def resolve(identifier:)
argument :recorded_after, Types::TimeType,
required: false,
description: 'Measurement recorded after this date'
argument :recorded_before, Types::TimeType,
required: false,
description: 'Measurement recorded before this date'
def resolve(identifier:, recorded_before: nil, recorded_after: nil)
authorize!
::Analytics::InstanceStatistics::Measurement
.recorded_after(recorded_after)
.recorded_before(recorded_before)
.with_identifier(identifier)
.order_by_latest
end
......
......@@ -9,7 +9,7 @@ module Resolvers
authorize :read_design
argument :id, GraphQL::ID_TYPE,
argument :id, ::Types::GlobalIDType[::DesignManagement::DesignAtVersion],
required: true,
description: 'The Global ID of the design at this version'
......@@ -18,7 +18,10 @@ module Resolvers
end
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)
dav
......
......@@ -3,7 +3,7 @@
module Resolvers
module DesignManagement
class DesignResolver < BaseResolver
argument :id, GraphQL::ID_TYPE,
argument :id, ::Types::GlobalIDType[::DesignManagement::Design],
required: false,
description: 'Find a design by its ID'
......@@ -50,7 +50,11 @@ module Resolvers
end
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
......
......@@ -3,16 +3,16 @@
module Resolvers
module DesignManagement
class DesignsResolver < BaseResolver
argument :ids,
[GraphQL::ID_TYPE],
DesignID = ::Types::GlobalIDType[::DesignManagement::Design]
VersionID = ::Types::GlobalIDType[::DesignManagement::Version]
argument :ids, [DesignID],
required: false,
description: 'Filters designs by their ID'
argument :filenames,
[GraphQL::STRING_TYPE],
argument :filenames, [GraphQL::STRING_TYPE],
required: false,
description: 'Filters designs by their filename'
argument :at_version,
GraphQL::ID_TYPE,
argument :at_version, VersionID,
required: false,
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'
......@@ -36,11 +36,20 @@ module Resolvers
def version(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
def design_ids(ids)
ids&.map { |id| GlobalID.parse(id, expected_type: ::DesignManagement::Design).model_id }
def design_ids(gids)
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
def issue
......
......@@ -5,17 +5,20 @@ module Resolvers
module Version
# Resolver for a DesignAtVersion object given an implicit version context
class DesignAtVersionResolver < BaseResolver
DesignAtVersionID = ::Types::GlobalIDType[::DesignManagement::DesignAtVersion]
DesignID = ::Types::GlobalIDType[::DesignManagement::Design]
include Gitlab::Graphql::Authorize::AuthorizeResource
type Types::DesignManagement::DesignAtVersionType, null: true
authorize :read_design
argument :id, GraphQL::ID_TYPE,
argument :id, DesignAtVersionID,
required: false,
as: :design_at_version_id,
description: 'The ID of the DesignAtVersion'
argument :design_id, GraphQL::ID_TYPE,
argument :design_id, DesignID,
required: false,
description: 'The ID of a specific design'
argument :filename, GraphQL::STRING_TYPE,
......@@ -29,6 +32,11 @@ module Resolvers
def resolve(design_id: nil, filename: nil, design_at_version_id: nil)
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 specific_design_at_version(design_at_version_id) if design_at_version_id
......@@ -49,7 +57,7 @@ module Resolvers
end
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)
dav
......@@ -65,8 +73,8 @@ module Resolvers
dav.design.visible_in?(version)
end
def find(id, filename)
ids = [parse_design_id(id).model_id] if id
def find(gid, filename)
ids = [gid.model_id] if gid
filenames = [filename] if filename
::DesignManagement::DesignsFinder
......@@ -74,10 +82,6 @@ module Resolvers
.execute
end
def parse_design_id(id)
GitlabSchema.parse_gid(id, expected_type: ::DesignManagement::Design)
end
def issue
version.issue
end
......
......@@ -11,8 +11,9 @@ module Resolvers
authorize :read_design
argument :ids,
[GraphQL::ID_TYPE],
DesignID = ::Types::GlobalIDType[::DesignManagement::Design]
argument :ids, [DesignID],
required: false,
description: 'Filters designs by their ID'
argument :filenames,
......@@ -31,16 +32,19 @@ module Resolvers
private
def find(ids, filenames)
ids = ids&.map { |id| parse_design_id(id).model_id }
::DesignManagement::DesignsFinder.new(issue, current_user,
ids: ids,
ids: design_ids(ids),
filenames: filenames,
visible_at_version: version)
end
def parse_design_id(id)
GitlabSchema.parse_gid(id, expected_type: ::DesignManagement::Design)
def design_ids(gids)
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
def issue
......
......@@ -11,20 +11,25 @@ module Resolvers
alias_method :collection, :object
VersionID = ::Types::GlobalIDType[::DesignManagement::Version]
argument :sha, GraphQL::STRING_TYPE,
required: false,
description: "The SHA256 of a specific version"
argument :id, GraphQL::ID_TYPE,
argument :id, VersionID,
as: :version_id,
required: false,
description: 'The Global ID of the version'
def resolve(id: nil, sha: nil)
check_args(id, sha)
def resolve(version_id: nil, sha: nil)
# 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
.new(collection, current_user, sha: sha, version_id: gid&.model_id)
.new(collection, current_user, sha: sha, version_id: version_id&.model_id)
.execute
.first
end
......
......@@ -9,7 +9,7 @@ module Resolvers
authorize :read_design
argument :id, GraphQL::ID_TYPE,
argument :id, ::Types::GlobalIDType[::DesignManagement::Version],
required: true,
description: 'The Global ID of the version'
......@@ -18,7 +18,11 @@ module Resolvers
end
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
......
......@@ -7,12 +7,14 @@ module Resolvers
alias_method :design_or_collection, :object
VersionID = ::Types::GlobalIDType[::DesignManagement::Version]
argument :earlier_or_equal_to_sha, GraphQL::STRING_TYPE,
as: :sha,
required: false,
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,
required: false,
description: 'The Global ID of the most recent acceptable version'
......@@ -23,6 +25,9 @@ module Resolvers
end
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)
raise ::Gitlab::Graphql::Errors::ResourceNotAvailable, 'cutoff not found' unless version.present?
......@@ -47,8 +52,7 @@ module Resolvers
end
end
def specific_version(id, sha)
gid = GitlabSchema.parse_gid(id, expected_type: ::DesignManagement::Version) if id
def specific_version(gid, sha)
find(sha: sha, version_id: gid&.model_id).first
end
......@@ -58,8 +62,8 @@ module Resolvers
.execute
end
def by_id(id)
GitlabSchema.object_from_id(id, expected_type: ::DesignManagement::Version).sync
def by_id(gid)
::Gitlab::Graphql::Lazy.force(GitlabSchema.find_by_gid(gid))
end
# Find an `at_version` argument passed to a parent node.
......@@ -69,7 +73,11 @@ module Resolvers
# for consistency we should only present versions up to the given
# version here.
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
......
......@@ -15,7 +15,9 @@ module Resolvers
def preloads
{
jobs: [:statuses]
jobs: [:statuses],
upstream: [:triggered_by_pipeline],
downstream: [:triggered_pipelines]
}
end
end
......
......@@ -18,10 +18,14 @@ module Resolvers
required: false,
default_value: 'created_desc'
def resolve(ids: nil, usernames: nil, sort: nil)
argument :search, GraphQL::STRING_TYPE,
required: false,
description: "Query to search users by name, username, or primary email."
def resolve(ids: nil, usernames: nil, sort: nil, search: nil)
authorize!
::UsersFinder.new(context[:current_user], finder_params(ids, usernames, sort)).execute
::UsersFinder.new(context[:current_user], finder_params(ids, usernames, sort, search)).execute
end
def ready?(**args)
......@@ -42,11 +46,12 @@ module Resolvers
private
def finder_params(ids, usernames, sort)
def finder_params(ids, usernames, sort, search)
params = {}
params[:sort] = sort if sort
params[:username] = usernames if usernames
params[:id] = parse_gids(ids) if ids
params[:search] = search if search
params
end
......
......@@ -56,12 +56,24 @@ module Types
description: 'Specifies if a pipeline can be canceled',
method: :cancelable?,
null: false
field :jobs,
::Types::Ci::JobType.connection_type,
null: true,
description: 'Jobs belonging to the pipeline',
method: :statuses
field :source_job, Types::Ci::JobType, null: true,
description: 'Job where pipeline was triggered from'
field :downstream, Types::Ci::PipelineType.connection_type, null: true,
description: 'Pipelines this pipeline will trigger',
method: :triggered_pipelines_with_preloads
field :upstream, Types::Ci::PipelineType, null: true,
description: 'Pipeline that triggered the pipeline',
method: :triggered_by_pipeline
field :path, GraphQL::STRING_TYPE, null: true,
description: "Relative path to the pipeline's page",
resolve: -> (obj, _args, _ctx) { ::Gitlab::Routing.url_helpers.project_pipeline_path(obj.project, obj) }
field :project, Types::ProjectType, null: true,
description: 'Project the pipeline belongs to'
end
end
end
......
......@@ -118,8 +118,7 @@ module Types
resolver: Resolvers::MergeRequestPipelinesResolver
field :milestone, Types::MilestoneType, null: true,
description: 'The milestone of the merge request',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Milestone, obj.milestone_id).find }
description: 'The milestone of the merge request'
field :assignees, Types::UserType.connection_type, null: true, complexity: 5,
description: 'Assignees of the merge request'
field :author, Types::UserType, null: true,
......
......@@ -14,6 +14,7 @@ module Types
mount_mutation Mutations::AlertManagement::HttpIntegration::Create
mount_mutation Mutations::AlertManagement::HttpIntegration::Update
mount_mutation Mutations::AlertManagement::HttpIntegration::ResetToken
mount_mutation Mutations::AlertManagement::HttpIntegration::Destroy
mount_mutation Mutations::AlertManagement::PrometheusIntegration::Create
mount_mutation Mutations::AlertManagement::PrometheusIntegration::Update
mount_mutation Mutations::AlertManagement::PrometheusIntegration::ResetToken
......
......@@ -30,7 +30,7 @@ module OperationsHelper
'alerts_setup_url' => help_page_path('operations/incident_management/alert_integrations.md', anchor: 'generic-http-endpoint'),
'alerts_usage_url' => project_alert_management_index_path(@project),
'disabled' => disabled.to_s,
'project_path' => project_path(@project)
'project_path' => @project.full_path
}
end
......
......@@ -92,11 +92,27 @@ module SearchHelper
end
end
def search_entries_empty_message(scope, term)
(s_("SearchResults|We couldn't find any %{scope} matching %{term}") % {
def search_entries_empty_message(scope, term, group, project)
options = {
scope: search_entries_scope_label(scope, 0),
term: "<code>#{h(term)}</code>"
}).html_safe
term: "<code>#{h(term)}</code>".html_safe
}
# We check project first because we have 3 possible combinations here:
# - group && project
# - group
# - group: nil, project: nil
if project
html_escape(_("We couldn't find any %{scope} matching %{term} in project %{project}")) % options.merge(
project: link_to(project.full_name, project_path(project), target: '_blank', rel: 'noopener noreferrer').html_safe
)
elsif group
html_escape(_("We couldn't find any %{scope} matching %{term} in group %{group}")) % options.merge(
group: link_to(group.full_name, group_path(group), target: '_blank', rel: 'noopener noreferrer').html_safe
)
else
html_escape(_("We couldn't find any %{scope} matching %{term}")) % options
end
end
def repository_ref(project)
......
......@@ -56,12 +56,9 @@ module Emails
subject: @message.subject)
end
def prometheus_alert_fired_email(project_id, user_id, alert_attributes)
@project = ::Project.find(project_id)
user = ::User.find(user_id)
@alert = AlertManagement::Alert.new(alert_attributes.with_indifferent_access).present
return unless @alert.parsed_payload.has_required_attributes?
def prometheus_alert_fired_email(project, user, alert)
@project = project
@alert = alert.present
subject_text = "Alert: #{@alert.email_title}"
mail(to: user.notification_email_for(@project.group), subject: subject(subject_text))
......
......@@ -36,6 +36,8 @@ module Analytics
scope :order_by_latest, -> { order(recorded_at: :desc) }
scope :with_identifier, -> (identifier) { where(identifier: identifier) }
scope :recorded_after, -> (date) { where(self.model.arel_table[:recorded_at].gteq(date)) if date.present? }
scope :recorded_before, -> (date) { where(self.model.arel_table[:recorded_at].lteq(date)) if date.present? }
def self.measurement_identifier_values
identifiers.values
......
# frozen_string_literal: true
module AlertManagement
module HttpIntegrations
class DestroyService
# @param integration [AlertManagement::HttpIntegration]
# @param current_user [User]
def initialize(integration, current_user)
@integration = integration
@current_user = current_user
end
def execute
return error_no_permissions unless allowed?
return error_multiple_integrations unless Feature.enabled?(:multiple_http_integrations, integration.project)
if integration.destroy
success
else
error(integration.errors.full_messages.to_sentence)
end
end
private
attr_reader :integration, :current_user
def allowed?
current_user&.can?(:admin_operations, integration)
end
def error(message)
ServiceResponse.error(message: message)
end
def success
ServiceResponse.success(payload: { integration: integration })
end
def error_no_permissions
error(_('You have insufficient permissions to remove this HTTP integration'))
end
def error_multiple_integrations
error(_('Removing integrations is not supported for this project'))
end
end
end
end
......@@ -9,6 +9,10 @@ module AlertManagement
return bad_request unless incoming_payload.has_required_attributes?
process_alert_management_alert
return bad_request unless alert.persisted?
process_incident_issues if process_issues?
send_alert_email if send_email?
ServiceResponse.success
end
......@@ -30,8 +34,6 @@ module AlertManagement
else
create_alert_management_alert
end
process_incident_issues if process_issues?
end
def reset_alert_management_alert_status
......@@ -85,12 +87,17 @@ module AlertManagement
end
def process_incident_issues
return unless alert.persisted?
return if alert.issue
return if alert.issue || alert.resolved?
IncidentManagement::ProcessAlertWorker.perform_async(nil, nil, alert.id)
end
def send_alert_email
notification_service
.async
.prometheus_alerts_fired(project, [alert])
end
def logger
@logger ||= Gitlab::AppLogger
end
......
......@@ -601,7 +601,7 @@ class NotificationService
return if project.emails_disabled?
owners_and_maintainers_without_invites(project).to_a.product(alerts).each do |recipient, alert|
mailer.prometheus_alert_fired_email(project.id, recipient.user.id, alert).deliver_later
mailer.prometheus_alert_fired_email(project, recipient.user, alert).deliver_later
end
end
......
......@@ -73,7 +73,7 @@ module Projects
end
def process_incident_issues
return if alert.issue
return if alert.issue || alert.resolved?
::IncidentManagement::ProcessAlertWorker.perform_async(nil, nil, alert.id)
end
......@@ -81,7 +81,7 @@ module Projects
def send_alert_email
notification_service
.async
.prometheus_alerts_fired(project, [alert.attributes])
.prometheus_alerts_fired(project, [alert])
end
def alert
......
......@@ -11,6 +11,24 @@ module Projects
include Gitlab::Utils::StrongMemoize
class << self
def enqueue(project, current_user, bfg_object_map)
Projects::UpdateService.new(project, current_user, bfg_object_map: bfg_object_map).execute.tap do |result|
next unless result[:status] == :success
project.set_repository_read_only!
RepositoryCleanupWorker.perform_async(project.id, current_user.id)
end
rescue Project::RepositoryReadOnlyError => err
{ status: :error, message: (_('Failed to make repository read-only. %{reason}') % { reason: err.message }) }
end
def cleanup_after(project)
project.bfg_object_map.remove!
project.set_repository_writable!
end
end
# Attempt to clean up the project following the push. Warning: this is
# destructive!
#
......@@ -29,7 +47,7 @@ module Projects
# time. Better to feel the pain immediately.
project.repository.expire_all_method_caches
project.bfg_object_map.remove!
self.class.cleanup_after(project)
end
private
......
......@@ -23,7 +23,6 @@ module Projects
return unauthorized unless valid_alert_manager_token?(token)
process_prometheus_alerts
send_alert_email if send_email?
ServiceResponse.success
end
......@@ -120,14 +119,6 @@ module Projects
ActiveSupport::SecurityUtils.secure_compare(expected, actual)
end
def send_alert_email
return unless firings.any?
notification_service
.async
.prometheus_alerts_fired(project, alerts_attributes)
end
def process_prometheus_alerts
alerts.each do |alert|
AlertManagement::ProcessPrometheusAlertService
......@@ -136,18 +127,6 @@ module Projects
end
end
def alerts_attributes
firings.map do |payload|
alert_params = Gitlab::AlertManagement::Payload.parse(
project,
payload,
monitoring_tool: Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus]
).alert_params
AlertManagement::Alert.new(alert_params).attributes
end
end
def bad_request
ServiceResponse.error(message: 'Bad Request', http_status: :bad_request)
end
......
......@@ -50,11 +50,11 @@
= f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block'
%span.form-text.text-muted#home_help_block We will redirect non-logged in users to this page
.form-group
= f.label :after_sign_out_path, class: 'label-bold'
= f.label :after_sign_out_path, _('After sign-out path'), class: 'label-bold'
= f.text_field :after_sign_out_path, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'after_sign_out_path_help_block'
%span.form-text.text-muted#after_sign_out_path_help_block We will redirect users to this page after they sign out
.form-group
= f.label :sign_in_text, class: 'label-bold'
= f.label :sign_in_text, _('Sign-in text'), class: 'label-bold'
= f.text_area :sign_in_text, class: 'form-control', rows: 4
.form-text.text-muted Markdown enabled
= f.submit 'Save changes', class: "gl-button btn btn-success"
- breadcrumb_title _("Dashboard")
- page_title _("Dashboard")
- billable_users_url = help_page_path('subscriptions/self_managed/index', anchor: 'billable-users')
- billable_users_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer nofollow">'.html_safe % { url: billable_users_url }
- if @notices
- @notices.each do |notice|
......@@ -22,10 +24,20 @@
= link_to(s_('AdminArea|New project'), new_project_path, class: "btn gl-button btn-success gl-w-full")
.col-sm-4
.info-well.dark-well
.well-segment.well-centered
.well-segment.well-centered.gl-text-center
= link_to admin_users_path do
%h3.text-center
%h3.gl-display-inline-block.gl-mb-0
= s_('AdminArea|Users: %{number_of_users}') % { number_of_users: approximate_count_with_delimiters(@counts, User) }
%span.gl-outline-0.gl-ml-2{ href: "#", tabindex: "0", data: { container: "body",
toggle: "popover",
placement: "top",
html: "true",
trigger: "focus",
content: s_("AdminArea|All users created in the instance, including users who are not %{billable_users_link_start}billable users%{billable_users_link_end}.").html_safe % { billable_users_link_start: billable_users_link_start, billable_users_link_end: '</a>'.html_safe },
} }
= sprite_icon('question', size: 16, css_class: 'gl-text-gray-700 gl-mb-1')
%hr
.btn-group.d-flex{ role: 'group' }
= link_to s_('AdminArea|New user'), new_admin_user_path, class: "btn gl-button btn-success gl-w-full"
......
......@@ -4,17 +4,7 @@
.container
.gl-mt-3
- if Feature.enabled?(:devops_adoption)
%h2
= _('DevOps Report')
%ul.nav-links.nav-tabs.nav.js-devops-tabs{ role: 'tablist' }
= render 'tab', active: true, title: _('DevOps Score'), target: '#devops_score_pane'
= render 'tab', active: false, title: _('Adoption'), target: '#devops_adoption_pane'
.tab-content
.tab-pane.active#devops_score_pane
= render 'report'
.tab-pane#devops_adoption_pane
.js-devops-adoption{ data: { empty_state_svg_path: image_path('illustrations/monitoring/getting_started.svg') } }
= render_if_exists 'admin/dev_ops_report/devops_tabs'
- else
= render 'report'
......@@ -2,17 +2,15 @@
%li.note.note-discussion.timeline-entry.unstyled-comments
.timeline-entry-inner
.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
.timeline-icon
= link_to user_path(discussion.author) do
= image_tag avatar_icon_for_user(discussion.author), class: "avatar s40"
.discussion-actions
%button.note-action-button.discussion-toggle-button.js-toggle-button{ type: "button", class: ("js-toggle-lazy-diff" unless expanded) }
- if expanded
= icon("chevron-up")
- else
= icon("chevron-down")
= sprite_icon('chevron-up', css_class: "js-sidebar-collapse #{'hidden' unless expanded}")
= sprite_icon('chevron-down', css_class: "js-sidebar-expand #{'hidden' if expanded}")
= _('Toggle thread')
= link_to_member(@project, discussion.author, avatar: false)
......
- body = @alert.resolved? ? _('An alert has been resolved in %{project_path}.') : _('An alert has been triggered in %{project_path}.')
%p
= body % { project_path: @alert.project.full_path }
%p
= _('An alert has been triggered in %{project_path}.') % { project_path: @alert.project.full_path }
= link_to(_('View alert details.'), @alert.details_url)
- if description = @alert.description
%p
......
<%= _('An alert has been triggered in %{project_path}.') % { project_path: @alert.project.full_path } %>.
<% body = @alert.resolved? ? _('An alert has been resolved in %{project_path}.') : _('An alert has been triggered in %{project_path}.') %>
<%= body % { project_path: @alert.project.full_path } %>
<%= _('View alert details at') %> <%= @alert.details_url %>
<% if description = @alert.description %>
<%= _('Description:') %> <%= description %>
......
......@@ -6,6 +6,8 @@
- enable_lfs_message = s_("DesignManagement|To upload designs, you'll need to enable LFS and have admin enable hashed storage. %{requirements_link_start}More information%{requirements_link_end}").html_safe % { requirements_link_start: requirements_link_start, requirements_link_end: link_end }
- if @project.design_management_enabled?
- add_page_startup_graphql_call('design_management/get_design_list', { fullPath: @project.full_path, iid: @issue.iid.to_s, atVersion: nil })
- add_page_startup_graphql_call('design_management/design_permissions', { fullPath: @project.full_path, iid: @issue.iid.to_s })
.js-design-management{ data: { project_path: @project.full_path, issue_iid: @issue.iid, issue_path: project_issue_path(@project, @issue) } }
- else
.gl-border-solid.gl-border-1.gl-border-gray-100.gl-rounded-base.gl-mt-5.gl-p-3.gl-text-center
......
- if Feature.enabled?(:export_merge_requests_as_csv, @project)
- if Feature.enabled?(:export_merge_requests_as_csv, @project, default_enabled: true)
.btn-group
= render 'shared/issuable/csv_export/button', issuable_type: 'merge-requests'
......@@ -8,5 +8,5 @@
= link_to new_merge_request_path, class: "gl-button btn btn-success", title: "New merge request" do
New merge request
- if Feature.enabled?(:export_merge_requests_as_csv, @project)
- if Feature.enabled?(:export_merge_requests_as_csv, @project, default_enabled: true)
= render 'shared/issuable/csv_export/modal', issuable_type: 'merge_requests'
......@@ -75,6 +75,8 @@
.settings-content
= render 'projects/registry/settings/index'
= render_if_exists 'projects/settings/ci_cd/auto_rollback', expanded: expanded
- if can?(current_user, :create_freeze_period, @project)
%section.settings.no-animate#js-deploy-freeze-settings{ class: ('expanded' if expanded) }
.settings-header
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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