Commit fdef227c authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch 'renovate/gitlab-uisvg' into 'master'

Update GitLab UI/SVG

See merge request gitlab-org/gitlab!46678
parents 04028c8e e286619f
...@@ -676,10 +676,14 @@ ...@@ -676,10 +676,14 @@
################## ##################
.releases:rules:canonical-dot-com-gitlab-stable-branch-only: .releases:rules:canonical-dot-com-gitlab-stable-branch-only:
rules: 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$/' - 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: .releases:rules:canonical-dot-com-security-gitlab-stable-branch-only:
rules: 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$/' - if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_PATH == "gitlab-org/security/gitlab" && $CI_COMMIT_REF_NAME =~ /^[\d-]+-stable-ee$/'
################# #################
......
...@@ -183,19 +183,6 @@ RSpec/ContextWording: ...@@ -183,19 +183,6 @@ RSpec/ContextWording:
RSpec/ExpectChange: RSpec/ExpectChange:
Enabled: false 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 # Offense count: 751
RSpec/ExpectInHook: RSpec/ExpectInHook:
Enabled: false Enabled: false
......
991ab1619abd34de70388d277892af9ad4c4994c 940a45ca938b20031820a4976f936a5b6173de92
...@@ -17,10 +17,13 @@ export default { ...@@ -17,10 +17,13 @@ export default {
}, },
}, },
computed: { computed: {
seriesData() { barSeriesData() {
return { return [
full: this.formattedData.keys.map((val, idx) => [val, this.formattedData.values[idx]]), {
}; name: 'full',
data: this.formattedData.keys.map((val, idx) => [val, this.formattedData.values[idx]]),
},
];
}, },
}, },
}; };
...@@ -30,7 +33,7 @@ export default { ...@@ -30,7 +33,7 @@ export default {
<div class="gl-xs-w-full"> <div class="gl-xs-w-full">
<gl-column-chart <gl-column-chart
v-if="formattedData.keys" v-if="formattedData.keys"
:data="seriesData" :bars="barSeriesData"
:x-axis-title="__('Value')" :x-axis-title="__('Value')"
:y-axis-title="__('Number of events')" :y-axis-title="__('Number of events')"
:x-axis-type="'category'" :x-axis-type="'category'"
......
<script> <script>
import { ApolloMutation } from 'vue-apollo'; 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 destroyDesignMutation from '../graphql/mutations/destroy_design.mutation.graphql';
import { updateStoreAfterDesignsDelete } from '../utils/cache_update'; import { updateStoreAfterDesignsDelete } from '../utils/cache_update';
......
<script> <script>
import { GlButton, GlIcon } from '@gitlab/ui'; import { GlButton, GlIcon } from '@gitlab/ui';
import permissionsQuery from 'shared_queries/design_management/design_permissions.query.graphql';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import DesignNavigation from './design_navigation.vue'; import DesignNavigation from './design_navigation.vue';
import DeleteButton from '../delete_button.vue'; import DeleteButton from '../delete_button.vue';
import permissionsQuery from '../../graphql/queries/design_permissions.query.graphql';
import { DESIGNS_ROUTE_NAME } from '../../router/constants'; import { DESIGNS_ROUTE_NAME } from '../../router/constants';
export default { export default {
......
import { propertyOf } from 'lodash'; import { propertyOf } from 'lodash';
import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql';
import createFlash, { FLASH_TYPES } from '~/flash'; import createFlash, { FLASH_TYPES } from '~/flash';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
import allVersionsMixin from './all_versions'; import allVersionsMixin from './all_versions';
import { DESIGNS_ROUTE_NAME } from '../router/constants'; 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'; import { findVersionId } from '../utils/design_management_utils';
export default { export default {
......
<script> <script>
import { GlLoadingIcon, GlButton, GlAlert } from '@gitlab/ui'; import { GlLoadingIcon, GlButton, GlAlert } from '@gitlab/ui';
import VueDraggable from 'vuedraggable'; 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 createFlash, { FLASH_TYPES } from '~/flash';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import { getFilename } from '~/lib/utils/file_upload'; import { getFilename } from '~/lib/utils/file_upload';
...@@ -12,8 +14,6 @@ import DesignVersionDropdown from '../components/upload/design_version_dropdown. ...@@ -12,8 +14,6 @@ import DesignVersionDropdown from '../components/upload/design_version_dropdown.
import DesignDropzone from '../components/upload/design_dropzone.vue'; import DesignDropzone from '../components/upload/design_dropzone.vue';
import uploadDesignMutation from '../graphql/mutations/upload_design.mutation.graphql'; import uploadDesignMutation from '../graphql/mutations/upload_design.mutation.graphql';
import moveDesignMutation from '../graphql/mutations/move_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 allDesignsMixin from '../mixins/all_designs';
import { import {
UPLOAD_DESIGN_ERROR, UPLOAD_DESIGN_ERROR,
......
...@@ -3,9 +3,8 @@ ...@@ -3,9 +3,8 @@
import $ from 'jquery'; import $ from 'jquery';
import 'vendor/jquery.scrollTo'; import 'vendor/jquery.scrollTo';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon, GlModal } from '@gitlab/ui';
import { s__, sprintf } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { HIDDEN_CLASS } from '~/lib/utils/constants'; import { HIDDEN_CLASS } from '~/lib/utils/constants';
import { getParameterByName } from '~/lib/utils/common_utils'; import { getParameterByName } from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility'; import { mergeUrlParams } from '~/lib/utils/url_utility';
...@@ -16,8 +15,8 @@ import groupsComponent from './groups.vue'; ...@@ -16,8 +15,8 @@ import groupsComponent from './groups.vue';
export default { export default {
components: { components: {
DeprecatedModal,
groupsComponent, groupsComponent,
GlModal,
GlLoadingIcon, GlLoadingIcon,
}, },
props: { props: {
...@@ -49,13 +48,30 @@ export default { ...@@ -49,13 +48,30 @@ export default {
isLoading: true, isLoading: true,
isSearchEmpty: false, isSearchEmpty: false,
searchEmptyMessage: '', searchEmptyMessage: '',
showModal: false,
groupLeaveConfirmationMessage: '',
targetGroup: null, targetGroup: null,
targetParentGroup: null, targetParentGroup: null,
}; };
}, },
computed: { 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() { groups() {
return this.store.getGroups(); return this.store.getGroups();
}, },
...@@ -171,27 +187,17 @@ export default { ...@@ -171,27 +187,17 @@ export default {
} }
}, },
showLeaveGroupModal(group, parentGroup) { showLeaveGroupModal(group, parentGroup) {
const { fullName } = group;
this.targetGroup = group; this.targetGroup = group;
this.targetParentGroup = parentGroup; 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() { leaveGroup() {
this.showModal = false;
this.targetGroup.isBeingRemoved = true; this.targetGroup.isBeingRemoved = true;
this.service this.service
.leaveGroup(this.targetGroup.leavePath) .leaveGroup(this.targetGroup.leavePath)
.then(res => { .then(res => {
$.scrollTo(0); $.scrollTo(0);
this.store.removeGroup(this.targetGroup, this.targetParentGroup); this.store.removeGroup(this.targetGroup, this.targetParentGroup);
Flash(res.data.notice, 'notice'); this.$toast.show(res.data.notice);
}) })
.catch(err => { .catch(err => {
let message = COMMON_STR.FAILURE; let message = COMMON_STR.FAILURE;
...@@ -245,21 +251,21 @@ export default { ...@@ -245,21 +251,21 @@ export default {
class="loading-animation prepend-top-20" class="loading-animation prepend-top-20"
/> />
<groups-component <groups-component
v-if="!isLoading" v-else
:groups="groups" :groups="groups"
:search-empty="isSearchEmpty" :search-empty="isSearchEmpty"
:search-empty-message="searchEmptyMessage" :search-empty-message="searchEmptyMessage"
:page-info="pageInfo" :page-info="pageInfo"
:action="action" :action="action"
/> />
<deprecated-modal <gl-modal
v-show="showModal" modal-id="leave-group-modal"
:primary-button-label="__('Leave')"
:title="__('Are you sure?')" :title="__('Are you sure?')"
:text="groupLeaveConfirmationMessage" :action-primary="primaryProps"
kind="warning" :action-cancel="cancelProps"
@cancel="hideLeaveGroupModal" @primary="leaveGroup"
@submit="leaveGroup" >
/> {{ groupLeaveConfirmationMessage }}
</gl-modal>
</div> </div>
</template> </template>
<script> <script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; import { GlTooltipDirective, GlButton, GlModalDirective } from '@gitlab/ui';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { COMMON_STR } from '../constants'; import { COMMON_STR } from '../constants';
export default { export default {
components: { components: {
GlIcon, GlButton,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
GlModal: GlModalDirective,
}, },
props: { props: {
parentGroup: { parentGroup: {
...@@ -44,28 +45,28 @@ export default { ...@@ -44,28 +45,28 @@ export default {
<template> <template>
<div class="controls d-flex justify-content-end"> <div class="controls d-flex justify-content-end">
<a <gl-button
v-if="group.canLeave" v-if="group.canLeave"
v-gl-tooltip.top v-gl-tooltip.top
:href="group.leavePath" v-gl-modal.leave-group-modal
:title="leaveBtnTitle" :title="leaveBtnTitle"
:aria-label="leaveBtnTitle" :aria-label="leaveBtnTitle"
data-testid="leave-group-btn" data-testid="leave-group-btn"
class="leave-group btn btn-xs no-expand gl-text-gray-500 gl-ml-5" size="small"
@click.prevent="onLeaveGroup" icon="leave"
> class="leave-group gl-ml-3"
<gl-icon name="leave" class="position-top-0" /> @click.stop="onLeaveGroup"
</a> />
<a <gl-button
v-if="group.canEdit" v-if="group.canEdit"
v-gl-tooltip.top v-gl-tooltip.top
:href="group.editPath" :href="group.editPath"
:title="editBtnTitle" :title="editBtnTitle"
:aria-label="editBtnTitle" :aria-label="editBtnTitle"
data-testid="edit-group-btn" data-testid="edit-group-btn"
class="edit-group btn btn-xs no-expand gl-text-gray-500 gl-ml-5" size="small"
> icon="pencil"
<gl-icon name="settings" class="position-top-0 align-middle" /> class="edit-group gl-ml-3"
</a> />
</div> </div>
</template> </template>
import Vue from 'vue'; import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
import GroupFilterableList from './groups_filterable_list'; import GroupFilterableList from './groups_filterable_list';
...@@ -31,6 +32,8 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => { ...@@ -31,6 +32,8 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
Vue.component('group-folder', groupFolderComponent); Vue.component('group-folder', groupFolderComponent);
Vue.component('group-item', groupItemComponent); Vue.component('group-item', groupItemComponent);
Vue.use(GlToast);
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
el, el,
......
<script>
import { GlButton, GlDropdown, GlDropdownItem, GlIcon, GlLink, GlModal } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import createFlash from '~/flash';
import { IssuableStatus, IssueStateEvent } from '~/issue_show/constants';
import { __ } from '~/locale';
import updateIssueMutation from '../queries/update_issue.mutation.graphql';
export default {
components: {
GlButton,
GlDropdown,
GlDropdownItem,
GlIcon,
GlLink,
GlModal,
},
actionCancel: {
text: __('Cancel'),
},
actionPrimary: {
text: __('Yes, close issue'),
attributes: [{ variant: 'warning' }],
},
inject: [
'canCreateIssue',
'canReopenIssue',
'canReportSpam',
'canUpdateIssue',
'iid',
'isIssueAuthor',
'newIssuePath',
'projectPath',
'reportAbusePath',
'submitAsSpamPath',
],
data() {
return {
isUpdatingState: false,
};
},
computed: {
...mapGetters(['getNoteableData']),
isClosed() {
return this.getNoteableData.state === IssuableStatus.Closed;
},
buttonText() {
return this.isClosed ? __('Reopen issue') : __('Close issue');
},
buttonVariant() {
return this.isClosed ? 'default' : 'warning';
},
showToggleIssueButton() {
const canClose = !this.isClosed && this.canUpdateIssue;
const canReopen = this.isClosed && this.canReopenIssue;
return canClose || canReopen;
},
},
methods: {
toggleIssueState() {
if (!this.isClosed && this.getNoteableData?.blocked_by_issues?.length) {
this.$refs.blockedByIssuesModal.show();
return;
}
this.invokeUpdateIssueMutation();
},
invokeUpdateIssueMutation() {
this.isUpdatingState = true;
this.$apollo
.mutate({
mutation: updateIssueMutation,
variables: {
input: {
iid: this.iid.toString(),
projectPath: this.projectPath,
stateEvent: this.isClosed ? IssueStateEvent.Reopen : IssueStateEvent.Close,
},
},
})
.then(({ data }) => {
if (data.updateIssue.errors.length) {
createFlash(data.updateIssue.errors.join('. '));
return;
}
const payload = {
detail: {
data: { id: this.iid },
isClosed: !this.isClosed,
},
};
// Dispatch event which updates open/close state, shared among the issue show page
document.dispatchEvent(new CustomEvent('issuable_vue_app:change', payload));
})
.catch(() => createFlash(__('Update failed. Please try again.')))
.finally(() => {
this.isUpdatingState = false;
});
},
},
};
</script>
<template>
<div class="detail-page-header-actions">
<gl-dropdown class="gl-display-block gl-display-sm-none!" block :text="__('Issue actions')">
<gl-dropdown-item
v-if="showToggleIssueButton"
:disabled="isUpdatingState"
@click="toggleIssueState"
>
{{ buttonText }}
</gl-dropdown-item>
<gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath">
{{ __('New issue') }}
</gl-dropdown-item>
<gl-dropdown-item v-if="!isIssueAuthor" :href="reportAbusePath">
{{ __('Report abuse') }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="canReportSpam"
:href="submitAsSpamPath"
data-method="post"
rel="nofollow"
>
{{ __('Submit as spam') }}
</gl-dropdown-item>
</gl-dropdown>
<gl-button
v-if="showToggleIssueButton"
class="gl-display-none gl-display-sm-inline-flex!"
category="secondary"
:loading="isUpdatingState"
:variant="buttonVariant"
@click="toggleIssueState"
>
{{ buttonText }}
</gl-button>
<gl-dropdown
class="gl-display-none gl-display-sm-inline-flex!"
toggle-class="gl-border-0! gl-shadow-none!"
no-caret
right
>
<template #button-content>
<gl-icon name="ellipsis_v" aria-hidden="true" />
<span class="gl-sr-only">{{ __('Actions') }}</span>
</template>
<gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath">
{{ __('New issue') }}
</gl-dropdown-item>
<gl-dropdown-item v-if="!isIssueAuthor" :href="reportAbusePath">
{{ __('Report abuse') }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="canReportSpam"
:href="submitAsSpamPath"
data-method="post"
rel="nofollow"
>
{{ __('Submit as spam') }}
</gl-dropdown-item>
</gl-dropdown>
<gl-modal
ref="blockedByIssuesModal"
modal-id="blocked-by-issues-modal"
:action-cancel="$options.actionCancel"
:action-primary="$options.actionPrimary"
:title="__('Are you sure you want to close this blocked issue?')"
@primary="invokeUpdateIssueMutation"
>
<p>{{ __('This issue is currently blocked by the following issues:') }}</p>
<ul>
<li v-for="issue in getNoteableData.blocked_by_issues" :key="issue.iid">
<gl-link :href="issue.web_url">#{{ issue.iid }}</gl-link>
</li>
</ul>
</gl-modal>
</div>
</template>
...@@ -18,5 +18,10 @@ export const IssuableType = { ...@@ -18,5 +18,10 @@ export const IssuableType = {
MergeRequest: 'merge_request', MergeRequest: 'merge_request',
}; };
export const IssueStateEvent = {
Close: 'CLOSE',
Reopen: 'REOPEN',
};
export const STATUS_PAGE_PUBLISHED = __('Published on status page'); export const STATUS_PAGE_PUBLISHED = __('Published on status page');
export const JOIN_ZOOM_MEETING = __('Join Zoom meeting'); export const JOIN_ZOOM_MEETING = __('Join Zoom meeting');
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import IssuableApp from './components/app.vue'; import IssuableApp from './components/app.vue';
import HeaderActions from './components/header_actions.vue';
export default function initIssuableApp(issuableData, store) { export function initIssuableApp(issuableData, store) {
return new Vue({ return new Vue({
el: document.getElementById('js-issuable-app'), el: document.getElementById('js-issuable-app'),
store, store,
...@@ -19,3 +23,36 @@ export default function initIssuableApp(issuableData, store) { ...@@ -19,3 +23,36 @@ export default function initIssuableApp(issuableData, store) {
}, },
}); });
} }
export function initIssueHeaderActions(store) {
const el = document.querySelector('.js-issue-header-actions');
if (!el) {
return undefined;
}
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
return new Vue({
el,
apolloProvider,
store,
provide: {
canCreateIssue: parseBoolean(el.dataset.canCreateIssue),
canReopenIssue: parseBoolean(el.dataset.canReopenIssue),
canReportSpam: parseBoolean(el.dataset.canReportSpam),
canUpdateIssue: parseBoolean(el.dataset.canUpdateIssue),
iid: el.dataset.iid,
isIssueAuthor: parseBoolean(el.dataset.isIssueAuthor),
newIssuePath: el.dataset.newIssuePath,
projectPath: el.dataset.projectPath,
reportAbusePath: el.dataset.reportAbusePath,
submitAsSpamPath: el.dataset.submitAsSpamPath,
},
render: createElement => createElement(HeaderActions),
});
}
mutation updateIssue($input: UpdateIssueInput!) {
updateIssue(input: $input) {
errors
}
}
...@@ -35,18 +35,14 @@ export default { ...@@ -35,18 +35,14 @@ export default {
}; };
}, },
computed: { computed: {
chartData() { barChartData() {
const queryData = this.graphData.metrics.reduce((acc, query) => { return this.graphData.metrics.reduce((acc, query) => {
const series = makeDataSeries(query.result || [], { const series = makeDataSeries(query.result || [], {
name: this.formatLegendLabel(query), name: this.formatLegendLabel(query),
}); });
return acc.concat(series); return acc.concat(series);
}, []); }, []);
return {
values: queryData[0].data,
};
}, },
chartOptions() { chartOptions() {
const xAxis = getTimeAxisOptions({ timezone: this.timezone }); const xAxis = getTimeAxisOptions({ timezone: this.timezone });
...@@ -109,7 +105,7 @@ export default { ...@@ -109,7 +105,7 @@ export default {
<gl-column-chart <gl-column-chart
ref="columnChart" ref="columnChart"
v-bind="$attrs" v-bind="$attrs"
:data="chartData" :bars="barChartData"
:option="chartOptions" :option="chartOptions"
:width="width" :width="width"
:height="height" :height="height"
......
...@@ -61,14 +61,16 @@ export default { ...@@ -61,14 +61,16 @@ export default {
}, },
computed: { computed: {
chartData() { chartData() {
return this.graphData.metrics.map(({ result }) => { return this.graphData.metrics
// This needs a fix. Not only metrics[0] should be shown. .map(({ label: name, result }) => {
// See https://gitlab.com/gitlab-org/gitlab/-/issues/220492 // This needs a fix. Not only metrics[0] should be shown.
if (!result || result.length === 0) { // See https://gitlab.com/gitlab-org/gitlab/-/issues/220492
return []; if (!result || result.length === 0) {
} return [];
return result[0].values.map(val => val[1]); }
}); return { name, data: result[0].values.map(val => val[1]) };
})
.slice(0, 1);
}, },
xAxisTitle() { xAxisTitle() {
return this.graphData.x_label !== undefined ? this.graphData.x_label : ''; return this.graphData.x_label !== undefined ? this.graphData.x_label : '';
...@@ -136,7 +138,7 @@ export default { ...@@ -136,7 +138,7 @@ export default {
<gl-stacked-column-chart <gl-stacked-column-chart
ref="chart" ref="chart"
v-bind="$attrs" v-bind="$attrs"
:data="chartData" :bars="chartData"
:option="chartOptions" :option="chartOptions"
:x-axis-title="xAxisTitle" :x-axis-title="xAxisTitle"
:y-axis-title="yAxisTitle" :y-axis-title="yAxisTitle"
...@@ -144,7 +146,6 @@ export default { ...@@ -144,7 +146,6 @@ export default {
:group-by="groupBy" :group-by="groupBy"
:width="width" :width="width"
:height="height" :height="height"
:series-names="seriesNames"
:legend-layout="legendLayout" :legend-layout="legendLayout"
:legend-average-text="legendAverageText" :legend-average-text="legendAverageText"
:legend-current-text="legendCurrentText" :legend-current-text="legendCurrentText"
......
...@@ -5,6 +5,8 @@ import { __ } from '~/locale'; ...@@ -5,6 +5,8 @@ import { __ } from '~/locale';
import CodeCoverage from '../components/code_coverage.vue'; import CodeCoverage from '../components/code_coverage.vue';
import SeriesDataMixin from './series_data_mixin'; import SeriesDataMixin from './series_data_mixin';
const seriesDataToBarData = raw => Object.entries(raw).map(([name, data]) => ({ name, data }));
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
waitForCSSLoaded(() => { waitForCSSLoaded(() => {
const languagesContainer = document.getElementById('js-languages-chart'); const languagesContainer = document.getElementById('js-languages-chart');
...@@ -41,13 +43,13 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -41,13 +43,13 @@ document.addEventListener('DOMContentLoaded', () => {
}, },
computed: { computed: {
seriesData() { 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) { render(h) {
return h(GlColumnChart, { return h(GlColumnChart, {
props: { props: {
data: this.seriesData, bars: this.seriesData,
xAxisTitle: __('Used programming language'), xAxisTitle: __('Used programming language'),
yAxisTitle: __('Percentage'), yAxisTitle: __('Percentage'),
xAxisType: 'category', xAxisType: 'category',
...@@ -86,7 +88,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -86,7 +88,7 @@ document.addEventListener('DOMContentLoaded', () => {
render(h) { render(h) {
return h(GlColumnChart, { return h(GlColumnChart, {
props: { props: {
data: this.seriesData, bars: seriesDataToBarData(this.seriesData),
xAxisTitle: __('Day of month'), xAxisTitle: __('Day of month'),
yAxisTitle: __('No. of commits'), yAxisTitle: __('No. of commits'),
xAxisType: 'category', xAxisType: 'category',
...@@ -113,13 +115,13 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -113,13 +115,13 @@ document.addEventListener('DOMContentLoaded', () => {
acc.push([key, weekDays[key]]); acc.push([key, weekDays[key]]);
return acc; return acc;
}, []); }, []);
return { full: data }; return [{ name: 'full', data }];
}, },
}, },
render(h) { render(h) {
return h(GlColumnChart, { return h(GlColumnChart, {
props: { props: {
data: this.seriesData, bars: this.seriesData,
xAxisTitle: __('Weekday'), xAxisTitle: __('Weekday'),
yAxisTitle: __('No. of commits'), yAxisTitle: __('No. of commits'),
xAxisType: 'category', xAxisType: 'category',
...@@ -143,7 +145,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -143,7 +145,7 @@ document.addEventListener('DOMContentLoaded', () => {
render(h) { render(h) {
return h(GlColumnChart, { return h(GlColumnChart, {
props: { props: {
data: this.seriesData, bars: seriesDataToBarData(this.seriesData),
xAxisTitle: __('Hour (UTC)'), xAxisTitle: __('Hour (UTC)'),
yAxisTitle: __('No. of commits'), yAxisTitle: __('No. of commits'),
xAxisType: 'category', xAxisType: 'category',
......
...@@ -5,7 +5,7 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; ...@@ -5,7 +5,7 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import ZenMode from '~/zen_mode'; import ZenMode from '~/zen_mode';
import '~/notes/index'; import '~/notes/index';
import { store } from '~/notes/stores'; import { store } from '~/notes/stores';
import initIssueApp from '~/issue_show/issue'; import { initIssuableApp, initIssueHeaderActions } from '~/issue_show/issue';
import initIncidentApp from '~/issue_show/incident'; import initIncidentApp from '~/issue_show/incident';
import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_issuable_header_warning'; import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_issuable_header_warning';
import initSentryErrorStackTraceApp from '~/sentry_error_stack_trace'; import initSentryErrorStackTraceApp from '~/sentry_error_stack_trace';
...@@ -24,13 +24,14 @@ export default function() { ...@@ -24,13 +24,14 @@ export default function() {
initIncidentApp(issuableData); initIncidentApp(issuableData);
break; break;
case IssuableType.Issue: case IssuableType.Issue:
initIssueApp(issuableData, store); initIssuableApp(issuableData, store);
break; break;
default: default:
break; break;
} }
initIssuableHeaderWarning(store); initIssuableHeaderWarning(store);
initIssueHeaderActions(store);
initSentryErrorStackTraceApp(); initSentryErrorStackTraceApp();
initRelatedMergeRequestsApp(); initRelatedMergeRequestsApp();
......
...@@ -16,6 +16,11 @@ export default { ...@@ -16,6 +16,11 @@ export default {
</script> </script>
<template> <template>
<div class="gl-border-solid gl-border-gray-100 gl-border-1"> <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> </div>
</template> </template>
<script> <script>
import { GlLoadingIcon, GlAlert } from '@gitlab/ui'; import { GlLoadingIcon, GlAlert, GlTabs, GlTab } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import TextEditor from './components/text_editor.vue'; 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'; import getBlobContent from './graphql/queries/blob_content.graphql';
...@@ -10,7 +11,10 @@ export default { ...@@ -10,7 +11,10 @@ export default {
components: { components: {
GlLoadingIcon, GlLoadingIcon,
GlAlert, GlAlert,
GlTabs,
GlTab,
TextEditor, TextEditor,
PipelineGraph,
}, },
props: { props: {
projectPath: { projectPath: {
...@@ -31,6 +35,7 @@ export default { ...@@ -31,6 +35,7 @@ export default {
return { return {
error: null, error: null,
content: '', content: '',
editorIsReady: false,
}; };
}, },
apollo: { apollo: {
...@@ -66,10 +71,16 @@ export default { ...@@ -66,10 +71,16 @@ export default {
const reason = networkReason ?? generalReason ?? this.$options.i18n.unknownError; const reason = networkReason ?? generalReason ?? this.$options.i18n.unknownError;
return sprintf(this.$options.i18n.errorMessageWithReason, { reason }); 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: { i18n: {
unknownError: __('Unknown Error'), unknownError: __('Unknown Error'),
errorMessageWithReason: s__('Pipelines|CI file could not be loaded: %{reason}'), errorMessageWithReason: s__('Pipelines|CI file could not be loaded: %{reason}'),
tabEdit: s__('Pipelines|Write pipeline configuration'),
tabGraph: s__('Pipelines|Visualize'),
}, },
}; };
</script> </script>
...@@ -79,7 +90,19 @@ export default { ...@@ -79,7 +90,19 @@ export default {
<gl-alert v-if="error" :dismissible="false" variant="danger">{{ errorMessage }}</gl-alert> <gl-alert v-if="error" :dismissible="false" variant="danger">{{ errorMessage }}</gl-alert>
<div class="gl-mt-4"> <div class="gl-mt-4">
<gl-loading-icon v-if="loading" size="lg" /> <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>
</div> </div>
</template> </template>
...@@ -45,9 +45,12 @@ export default { ...@@ -45,9 +45,12 @@ export default {
}, },
data() { data() {
return { return {
timesChartTransformedData: { timesChartTransformedData: [
full: this.mergeLabelsAndValues(this.timesChartData.labels, this.timesChartData.values), {
}, name: 'full',
data: this.mergeLabelsAndValues(this.timesChartData.labels, this.timesChartData.values),
},
],
}; };
}, },
computed: { computed: {
...@@ -128,7 +131,7 @@ export default { ...@@ -128,7 +131,7 @@ export default {
<gl-column-chart <gl-column-chart
:height="$options.chartContainerHeight" :height="$options.chartContainerHeight"
:option="$options.timesChartOptions" :option="$options.timesChartOptions"
:data="timesChartTransformedData" :bars="timesChartTransformedData"
:y-axis-title="__('Minutes')" :y-axis-title="__('Minutes')"
:x-axis-title="__('Commit')" :x-axis-title="__('Commit')"
x-axis-type="category" x-axis-type="category"
......
...@@ -16,12 +16,7 @@ import { performanceMarkAndMeasure } from '~/performance_utils'; ...@@ -16,12 +16,7 @@ import { performanceMarkAndMeasure } from '~/performance_utils';
import UpdateSnippetMutation from '../mutations/updateSnippet.mutation.graphql'; import UpdateSnippetMutation from '../mutations/updateSnippet.mutation.graphql';
import CreateSnippetMutation from '../mutations/createSnippet.mutation.graphql'; import CreateSnippetMutation from '../mutations/createSnippet.mutation.graphql';
import { getSnippetMixin } from '../mixins/snippets'; import { getSnippetMixin } from '../mixins/snippets';
import { import { SNIPPET_CREATE_MUTATION_ERROR, SNIPPET_UPDATE_MUTATION_ERROR } from '../constants';
SNIPPET_CREATE_MUTATION_ERROR,
SNIPPET_UPDATE_MUTATION_ERROR,
SNIPPET_VISIBILITY_PRIVATE,
} from '../constants';
import defaultVisibilityQuery from '../queries/snippet_visibility.query.graphql';
import { markBlobPerformance } from '../utils/blob'; import { markBlobPerformance } from '../utils/blob';
import SnippetBlobActionsEdit from './snippet_blob_actions_edit.vue'; import SnippetBlobActionsEdit from './snippet_blob_actions_edit.vue';
...@@ -41,15 +36,7 @@ export default { ...@@ -41,15 +36,7 @@ export default {
GlLoadingIcon, GlLoadingIcon,
}, },
mixins: [getSnippetMixin], mixins: [getSnippetMixin],
apollo: { inject: ['selectedLevel'],
defaultVisibility: {
query: defaultVisibilityQuery,
manual: true,
result({ data: { selectedLevel } }) {
this.selectedLevelDefault = selectedLevel;
},
},
},
props: { props: {
markdownPreviewPath: { markdownPreviewPath: {
type: String, type: String,
...@@ -73,9 +60,12 @@ export default { ...@@ -73,9 +60,12 @@ export default {
data() { data() {
return { return {
isUpdating: false, isUpdating: false,
newSnippet: false,
actions: [], actions: [],
selectedLevelDefault: SNIPPET_VISIBILITY_PRIVATE, snippet: {
title: '',
description: '',
visibilityLevel: this.selectedLevel,
},
}; };
}, },
computed: { computed: {
...@@ -112,13 +102,6 @@ export default { ...@@ -112,13 +102,6 @@ export default {
} }
return this.snippet.webUrl; return this.snippet.webUrl;
}, },
newSnippetSchema() {
return {
title: '',
description: '',
visibilityLevel: this.selectedLevelDefault,
};
},
}, },
beforeCreate() { beforeCreate() {
performanceMarkAndMeasure({ mark: SNIPPET_MARK_EDIT_APP_START }); performanceMarkAndMeasure({ mark: SNIPPET_MARK_EDIT_APP_START });
...@@ -145,20 +128,6 @@ export default { ...@@ -145,20 +128,6 @@ export default {
Flash(sprintf(defaultErrorMsg, { err })); Flash(sprintf(defaultErrorMsg, { err }));
this.isUpdating = false; 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() { getAttachedFiles() {
const fileInputs = Array.from(this.$el.querySelectorAll('[name="files[]"]')); const fileInputs = Array.from(this.$el.querySelectorAll('[name="files[]"]'));
return fileInputs.map(node => node.value); return fileInputs.map(node => node.value);
...@@ -209,7 +178,7 @@ export default { ...@@ -209,7 +178,7 @@ export default {
</script> </script>
<template> <template>
<form <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-snippet-type="isProjectSnippet ? 'project' : 'personal'"
data-testid="snippet-edit-form" data-testid="snippet-edit-form"
@submit.prevent="handleFormSubmit" @submit.prevent="handleFormSubmit"
......
<script> <script>
import { GlIcon, GlFormGroup, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui'; import { GlIcon, GlFormGroup, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
import defaultVisibilityQuery from '../queries/snippet_visibility.query.graphql';
import { defaultSnippetVisibilityLevels } from '../utils/blob'; import { defaultSnippetVisibilityLevels } from '../utils/blob';
import { SNIPPET_LEVELS_RESTRICTED, SNIPPET_LEVELS_DISABLED } from '~/snippets/constants'; import { SNIPPET_LEVELS_RESTRICTED, SNIPPET_LEVELS_DISABLED } from '~/snippets/constants';
...@@ -12,16 +11,7 @@ export default { ...@@ -12,16 +11,7 @@ export default {
GlFormRadioGroup, GlFormRadioGroup,
GlLink, GlLink,
}, },
apollo: { inject: ['visibilityLevels', 'multipleLevelsRestricted'],
defaultVisibility: {
query: defaultVisibilityQuery,
manual: true,
result({ data: { visibilityLevels, multipleLevelsRestricted } }) {
this.visibilityLevels = defaultSnippetVisibilityLevels(visibilityLevels);
this.multipleLevelsRestricted = multipleLevelsRestricted;
},
},
},
props: { props: {
helpLink: { helpLink: {
type: String, type: String,
...@@ -38,11 +28,10 @@ export default { ...@@ -38,11 +28,10 @@ export default {
required: true, required: true,
}, },
}, },
data() { computed: {
return { defaultVisibilityLevels() {
visibilityLevels: [], return defaultSnippetVisibilityLevels(this.visibilityLevels);
multipleLevelsRestricted: false, },
};
}, },
SNIPPET_LEVELS_DISABLED, SNIPPET_LEVELS_DISABLED,
SNIPPET_LEVELS_RESTRICTED, SNIPPET_LEVELS_RESTRICTED,
...@@ -59,7 +48,7 @@ export default { ...@@ -59,7 +48,7 @@ export default {
<gl-form-group id="visibility-level-setting" class="gl-mb-0"> <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-group :checked="value" stacked v-bind="$attrs" v-on="$listeners">
<gl-form-radio <gl-form-radio
v-for="option in visibilityLevels" v-for="option in defaultVisibilityLevels"
:key="option.value" :key="option.value"
:value="option.value" :value="option.value"
class="mb-3" class="mb-3"
...@@ -78,7 +67,9 @@ export default { ...@@ -78,7 +67,9 @@ export default {
</gl-form-group> </gl-form-group>
<div class="text-muted" data-testid="restricted-levels-info"> <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">{{ <template v-else-if="multipleLevelsRestricted">{{
$options.SNIPPET_LEVELS_RESTRICTED $options.SNIPPET_LEVELS_RESTRICTED
}}</template> }}</template>
......
...@@ -24,17 +24,14 @@ export default function appFactory(el, Component) { ...@@ -24,17 +24,14 @@ export default function appFactory(el, Component) {
...restDataset ...restDataset
} = el.dataset; } = el.dataset;
apolloProvider.clients.defaultClient.cache.writeData({ return new Vue({
data: { el,
apolloProvider,
provide: {
visibilityLevels: JSON.parse(visibilityLevels), visibilityLevels: JSON.parse(visibilityLevels),
selectedLevel: SNIPPET_LEVELS_MAP[selectedLevel] ?? SNIPPET_VISIBILITY_PRIVATE, selectedLevel: SNIPPET_LEVELS_MAP[selectedLevel] ?? SNIPPET_VISIBILITY_PRIVATE,
multipleLevelsRestricted: 'multipleLevelsRestricted' in el.dataset, multipleLevelsRestricted: 'multipleLevelsRestricted' in el.dataset,
}, },
});
return new Vue({
el,
apolloProvider,
render(createElement) { render(createElement) {
return createElement(Component, { return createElement(Component, {
props: { props: {
......
...@@ -21,9 +21,9 @@ export const getSnippetMixin = { ...@@ -21,9 +21,9 @@ export const getSnippetMixin = {
}, },
result(res) { result(res) {
this.blobs = res.data.snippets.nodes[0]?.blobs || blobsDefault; 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 = { ...@@ -36,7 +36,7 @@ export const getSnippetMixin = {
data() { data() {
return { return {
snippet: {}, snippet: {},
newSnippet: false, newSnippet: !this.snippetGid,
blobs: blobsDefault, blobs: blobsDefault,
}; };
}, },
......
query defaultSnippetVisibility {
visibilityLevels @client
selectedLevel @client
multipleLevelsRestricted @client
}
...@@ -51,6 +51,7 @@ export const FIELDS = [ ...@@ -51,6 +51,7 @@ export const FIELDS = [
key: 'actions', key: 'actions',
thClass: 'col-actions', thClass: 'col-actions',
tdClass: 'col-actions', tdClass: 'col-actions',
showFunction: 'showActionsField',
}, },
]; ];
......
...@@ -2,6 +2,12 @@ ...@@ -2,6 +2,12 @@
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import { GlTable, GlBadge } from '@gitlab/ui'; import { GlTable, GlBadge } from '@gitlab/ui';
import MembersTableCell from 'ee_else_ce/vue_shared/components/members/table/members_table_cell.vue'; 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 { FIELDS } from '../constants';
import initUserPopovers from '~/user_popovers'; import initUserPopovers from '~/user_popovers';
import MemberAvatar from './member_avatar.vue'; import MemberAvatar from './member_avatar.vue';
...@@ -33,14 +39,40 @@ export default { ...@@ -33,14 +39,40 @@ export default {
), ),
}, },
computed: { computed: {
...mapState(['members', 'tableFields']), ...mapState(['members', 'tableFields', 'currentUserId', 'sourceId']),
filteredFields() { 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() { mounted() {
initUserPopovers(this.$el.querySelectorAll('.js-user-link')); 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>
......
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import { MEMBER_TYPES } from '../constants'; import { MEMBER_TYPES } from '../constants';
import { isGroup, isDirectMember, isCurrentUser, canRemove, canResend, canUpdate } from '../utils';
export default { export default {
name: 'MembersTableCell', name: 'MembersTableCell',
...@@ -13,7 +14,7 @@ export default { ...@@ -13,7 +14,7 @@ export default {
computed: { computed: {
...mapState(['sourceId', 'currentUserId']), ...mapState(['sourceId', 'currentUserId']),
isGroup() { isGroup() {
return Boolean(this.member.sharedWithGroup); return isGroup(this.member);
}, },
isInvite() { isInvite() {
return Boolean(this.member.invite); return Boolean(this.member.invite);
...@@ -33,19 +34,19 @@ export default { ...@@ -33,19 +34,19 @@ export default {
return MEMBER_TYPES.user; return MEMBER_TYPES.user;
}, },
isDirectMember() { isDirectMember() {
return this.isGroup || this.member.source?.id === this.sourceId; return isDirectMember(this.member, this.sourceId);
}, },
isCurrentUser() { isCurrentUser() {
return this.member.user?.id === this.currentUserId; return isCurrentUser(this.member, this.currentUserId);
}, },
canRemove() { canRemove() {
return this.isDirectMember && this.member.canRemove; return canRemove(this.member, this.sourceId);
}, },
canResend() { canResend() {
return Boolean(this.member.invite?.canResend); return canResend(this.member);
}, },
canUpdate() { canUpdate() {
return !this.isCurrentUser && this.isDirectMember && this.member.canUpdate; return canUpdate(this.member, this.currentUserId, this.sourceId);
}, },
}, },
render() { render() {
......
...@@ -17,3 +17,32 @@ export const generateBadges = (member, isCurrentUser) => [ ...@@ -17,3 +17,32 @@ export const generateBadges = (member, isCurrentUser) => [
variant: 'info', 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;
...@@ -44,6 +44,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -44,6 +44,7 @@ class Projects::IssuesController < Projects::ApplicationController
push_frontend_feature_flag(:vue_issuable_sidebar, project.group) push_frontend_feature_flag(:vue_issuable_sidebar, project.group)
push_frontend_feature_flag(:tribute_autocomplete, @project) push_frontend_feature_flag(:tribute_autocomplete, @project)
push_frontend_feature_flag(:vue_issuables_list, project) push_frontend_feature_flag(:vue_issuables_list, project)
push_frontend_feature_flag(:vue_issue_header, @project)
end end
before_action only: :show do before_action only: :show do
......
...@@ -40,7 +40,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -40,7 +40,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:highlight_current_diff_row, @project) push_frontend_feature_flag(:highlight_current_diff_row, @project)
push_frontend_feature_flag(:default_merge_ref_for_diffs, @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(: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_a)
record_experiment_user(:invite_members_version_b) record_experiment_user(:invite_members_version_b)
...@@ -318,7 +318,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -318,7 +318,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end end
def export_csv 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 IssuableExportCsvWorker.perform_async(:merge_request, current_user.id, project.id, finder_options.to_h) # rubocop:disable CodeReuse/Worker
......
...@@ -75,7 +75,7 @@ module Projects ...@@ -75,7 +75,7 @@ module Projects
[ [
:runners_token, :builds_enabled, :build_allow_git_fetch, :runners_token, :builds_enabled, :build_allow_git_fetch,
:build_timeout_human_readable, :build_coverage_regex, :public_builds, :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], auto_devops_attributes: [:id, :domain, :enabled, :deploy_strategy],
ci_cd_settings_attributes: [:default_git_depth, :forward_deployment_enabled] ci_cd_settings_attributes: [:default_git_depth, :forward_deployment_enabled]
].tap do |list| ].tap do |list|
......
...@@ -18,14 +18,13 @@ module Projects ...@@ -18,14 +18,13 @@ module Projects
end end
def cleanup def cleanup
cleanup_params = params.require(:project).permit(:bfg_object_map) bfg_object_map = params.require(:project).require(:bfg_object_map)
result = Projects::UpdateService.new(project, current_user, cleanup_params).execute result = Projects::CleanupService.enqueue(project, current_user, bfg_object_map)
if result[:status] == :success 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.') flash[:notice] = _('Repository cleanup has started. You will receive an email once the cleanup operation is complete.')
else else
flash[:alert] = _('Failed to upload object map file') flash[:alert] = status.fetch(:message, _('Failed to upload object map file'))
end end
redirect_to project_settings_repository_path(project) redirect_to project_settings_repository_path(project)
......
# 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 ...@@ -7,7 +7,7 @@ module Mutations
field :integration, field :integration,
Types::AlertManagement::HttpIntegrationType, Types::AlertManagement::HttpIntegrationType,
null: true, null: true,
description: "The updated HTTP integration" description: "The HTTP integration"
authorize :admin_operations authorize :admin_operations
......
query permissions($fullPath: ID!, $iid: String!) { query permissions($fullPath: ID!, $iid: String!) {
project(fullPath: $fullPath) { project(fullPath: $fullPath) {
__typename
id id
issue(iid: $iid) { issue(iid: $iid) {
__typename
userPermissions { userPermissions {
__typename
createDesign createDesign
} }
} }
......
#import "../fragments/design_list.fragment.graphql"
#import "../fragments/version.fragment.graphql"
query getDesignList($fullPath: ID!, $iid: String!, $atVersion: ID) { query getDesignList($fullPath: ID!, $iid: String!, $atVersion: ID) {
project(fullPath: $fullPath) { project(fullPath: $fullPath) {
__typename
id id
issue(iid: $iid) { issue(iid: $iid) {
__typename
designCollection { designCollection {
__typename
copyState copyState
designs(atVersion: $atVersion) { designs(atVersion: $atVersion) {
__typename
nodes { nodes {
...DesignListItem __typename
id
event
filename
notesCount
image
imageV432x230
currentUserTodos(state: pending) {
__typename
nodes {
__typename
id
}
}
} }
} }
versions { versions {
__typename
nodes { nodes {
...VersionListItem __typename
id
sha
} }
} }
} }
......
...@@ -13,10 +13,20 @@ module Resolvers ...@@ -13,10 +13,20 @@ module Resolvers
required: true, required: true,
description: 'The type of measurement/statistics to retrieve' 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! authorize!
::Analytics::InstanceStatistics::Measurement ::Analytics::InstanceStatistics::Measurement
.recorded_after(recorded_after)
.recorded_before(recorded_before)
.with_identifier(identifier) .with_identifier(identifier)
.order_by_latest .order_by_latest
end end
......
...@@ -15,7 +15,9 @@ module Resolvers ...@@ -15,7 +15,9 @@ module Resolvers
def preloads def preloads
{ {
jobs: [:statuses] jobs: [:statuses],
upstream: [:triggered_by_pipeline],
downstream: [:triggered_pipelines]
} }
end end
end end
......
...@@ -18,10 +18,14 @@ module Resolvers ...@@ -18,10 +18,14 @@ module Resolvers
required: false, required: false,
default_value: 'created_desc' 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! 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 end
def ready?(**args) def ready?(**args)
...@@ -42,11 +46,12 @@ module Resolvers ...@@ -42,11 +46,12 @@ module Resolvers
private private
def finder_params(ids, usernames, sort) def finder_params(ids, usernames, sort, search)
params = {} params = {}
params[:sort] = sort if sort params[:sort] = sort if sort
params[:username] = usernames if usernames params[:username] = usernames if usernames
params[:id] = parse_gids(ids) if ids params[:id] = parse_gids(ids) if ids
params[:search] = search if search
params params
end end
......
...@@ -56,12 +56,24 @@ module Types ...@@ -56,12 +56,24 @@ module Types
description: 'Specifies if a pipeline can be canceled', description: 'Specifies if a pipeline can be canceled',
method: :cancelable?, method: :cancelable?,
null: false null: false
field :jobs, field :jobs,
::Types::Ci::JobType.connection_type, ::Types::Ci::JobType.connection_type,
null: true, null: true,
description: 'Jobs belonging to the pipeline', description: 'Jobs belonging to the pipeline',
method: :statuses 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 end
end end
......
...@@ -118,8 +118,7 @@ module Types ...@@ -118,8 +118,7 @@ module Types
resolver: Resolvers::MergeRequestPipelinesResolver resolver: Resolvers::MergeRequestPipelinesResolver
field :milestone, Types::MilestoneType, null: true, field :milestone, Types::MilestoneType, null: true,
description: 'The milestone of the merge request', description: 'The milestone of the merge request'
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Milestone, obj.milestone_id).find }
field :assignees, Types::UserType.connection_type, null: true, complexity: 5, field :assignees, Types::UserType.connection_type, null: true, complexity: 5,
description: 'Assignees of the merge request' description: 'Assignees of the merge request'
field :author, Types::UserType, null: true, field :author, Types::UserType, null: true,
......
...@@ -14,6 +14,7 @@ module Types ...@@ -14,6 +14,7 @@ module Types
mount_mutation Mutations::AlertManagement::HttpIntegration::Create mount_mutation Mutations::AlertManagement::HttpIntegration::Create
mount_mutation Mutations::AlertManagement::HttpIntegration::Update mount_mutation Mutations::AlertManagement::HttpIntegration::Update
mount_mutation Mutations::AlertManagement::HttpIntegration::ResetToken mount_mutation Mutations::AlertManagement::HttpIntegration::ResetToken
mount_mutation Mutations::AlertManagement::HttpIntegration::Destroy
mount_mutation Mutations::AlertManagement::PrometheusIntegration::Create mount_mutation Mutations::AlertManagement::PrometheusIntegration::Create
mount_mutation Mutations::AlertManagement::PrometheusIntegration::Update mount_mutation Mutations::AlertManagement::PrometheusIntegration::Update
mount_mutation Mutations::AlertManagement::PrometheusIntegration::ResetToken mount_mutation Mutations::AlertManagement::PrometheusIntegration::ResetToken
......
...@@ -152,6 +152,21 @@ module IssuesHelper ...@@ -152,6 +152,21 @@ module IssuesHelper
sort: 'desc' sort: 'desc'
} }
end end
def issue_header_actions_data(project, issue, current_user)
{
can_create_issue: show_new_issue_link?(project).to_s,
can_reopen_issue: can?(current_user, :reopen_issue, issue).to_s,
can_report_spam: issue.submittable_as_spam_by?(current_user).to_s,
can_update_issue: can?(current_user, :update_issue, issue).to_s,
iid: issue.iid,
is_issue_author: issue.author == current_user,
new_issue_path: new_project_issue_path(project),
project_path: project.full_path,
report_abuse_path: new_abuse_report_path(user_id: issue.author.id, ref_url: issue_url(issue)),
submit_as_spam_path: mark_as_spam_project_issue_path(project, issue)
}
end
end end
IssuesHelper.prepend_if_ee('EE::IssuesHelper') IssuesHelper.prepend_if_ee('EE::IssuesHelper')
...@@ -92,11 +92,27 @@ module SearchHelper ...@@ -92,11 +92,27 @@ module SearchHelper
end end
end end
def search_entries_empty_message(scope, term) def search_entries_empty_message(scope, term, group, project)
(s_("SearchResults|We couldn't find any %{scope} matching %{term}") % { options = {
scope: search_entries_scope_label(scope, 0), scope: search_entries_scope_label(scope, 0),
term: "<code>#{h(term)}</code>" term: "<code>#{h(term)}</code>".html_safe
}).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 end
def repository_ref(project) def repository_ref(project)
......
...@@ -56,12 +56,9 @@ module Emails ...@@ -56,12 +56,9 @@ module Emails
subject: @message.subject) subject: @message.subject)
end end
def prometheus_alert_fired_email(project_id, user_id, alert_attributes) def prometheus_alert_fired_email(project, user, alert)
@project = ::Project.find(project_id) @project = project
user = ::User.find(user_id) @alert = alert.present
@alert = AlertManagement::Alert.new(alert_attributes.with_indifferent_access).present
return unless @alert.parsed_payload.has_required_attributes?
subject_text = "Alert: #{@alert.email_title}" subject_text = "Alert: #{@alert.email_title}"
mail(to: user.notification_email_for(@project.group), subject: subject(subject_text)) mail(to: user.notification_email_for(@project.group), subject: subject(subject_text))
......
...@@ -36,6 +36,8 @@ module Analytics ...@@ -36,6 +36,8 @@ module Analytics
scope :order_by_latest, -> { order(recorded_at: :desc) } scope :order_by_latest, -> { order(recorded_at: :desc) }
scope :with_identifier, -> (identifier) { where(identifier: identifier) } 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 def self.measurement_identifier_values
identifiers.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 ...@@ -9,6 +9,10 @@ module AlertManagement
return bad_request unless incoming_payload.has_required_attributes? return bad_request unless incoming_payload.has_required_attributes?
process_alert_management_alert process_alert_management_alert
return bad_request unless alert.persisted?
process_incident_issues if process_issues?
send_alert_email if send_email?
ServiceResponse.success ServiceResponse.success
end end
...@@ -30,8 +34,6 @@ module AlertManagement ...@@ -30,8 +34,6 @@ module AlertManagement
else else
create_alert_management_alert create_alert_management_alert
end end
process_incident_issues if process_issues?
end end
def reset_alert_management_alert_status def reset_alert_management_alert_status
...@@ -85,12 +87,17 @@ module AlertManagement ...@@ -85,12 +87,17 @@ module AlertManagement
end end
def process_incident_issues def process_incident_issues
return unless alert.persisted? return if alert.issue || alert.resolved?
return if alert.issue
IncidentManagement::ProcessAlertWorker.perform_async(nil, nil, alert.id) IncidentManagement::ProcessAlertWorker.perform_async(nil, nil, alert.id)
end end
def send_alert_email
notification_service
.async
.prometheus_alerts_fired(project, [alert])
end
def logger def logger
@logger ||= Gitlab::AppLogger @logger ||= Gitlab::AppLogger
end end
......
...@@ -601,7 +601,7 @@ class NotificationService ...@@ -601,7 +601,7 @@ class NotificationService
return if project.emails_disabled? return if project.emails_disabled?
owners_and_maintainers_without_invites(project).to_a.product(alerts).each do |recipient, alert| 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
end end
......
...@@ -73,7 +73,7 @@ module Projects ...@@ -73,7 +73,7 @@ module Projects
end end
def process_incident_issues def process_incident_issues
return if alert.issue return if alert.issue || alert.resolved?
::IncidentManagement::ProcessAlertWorker.perform_async(nil, nil, alert.id) ::IncidentManagement::ProcessAlertWorker.perform_async(nil, nil, alert.id)
end end
...@@ -81,7 +81,7 @@ module Projects ...@@ -81,7 +81,7 @@ module Projects
def send_alert_email def send_alert_email
notification_service notification_service
.async .async
.prometheus_alerts_fired(project, [alert.attributes]) .prometheus_alerts_fired(project, [alert])
end end
def alert def alert
......
...@@ -11,6 +11,24 @@ module Projects ...@@ -11,6 +11,24 @@ module Projects
include Gitlab::Utils::StrongMemoize 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 # Attempt to clean up the project following the push. Warning: this is
# destructive! # destructive!
# #
...@@ -29,7 +47,7 @@ module Projects ...@@ -29,7 +47,7 @@ module Projects
# time. Better to feel the pain immediately. # time. Better to feel the pain immediately.
project.repository.expire_all_method_caches project.repository.expire_all_method_caches
project.bfg_object_map.remove! self.class.cleanup_after(project)
end end
private private
......
...@@ -23,7 +23,6 @@ module Projects ...@@ -23,7 +23,6 @@ module Projects
return unauthorized unless valid_alert_manager_token?(token) return unauthorized unless valid_alert_manager_token?(token)
process_prometheus_alerts process_prometheus_alerts
send_alert_email if send_email?
ServiceResponse.success ServiceResponse.success
end end
...@@ -120,14 +119,6 @@ module Projects ...@@ -120,14 +119,6 @@ module Projects
ActiveSupport::SecurityUtils.secure_compare(expected, actual) ActiveSupport::SecurityUtils.secure_compare(expected, actual)
end end
def send_alert_email
return unless firings.any?
notification_service
.async
.prometheus_alerts_fired(project, alerts_attributes)
end
def process_prometheus_alerts def process_prometheus_alerts
alerts.each do |alert| alerts.each do |alert|
AlertManagement::ProcessPrometheusAlertService AlertManagement::ProcessPrometheusAlertService
...@@ -136,18 +127,6 @@ module Projects ...@@ -136,18 +127,6 @@ module Projects
end end
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 def bad_request
ServiceResponse.error(message: 'Bad Request', http_status: :bad_request) ServiceResponse.error(message: 'Bad Request', http_status: :bad_request)
end end
......
...@@ -50,11 +50,11 @@ ...@@ -50,11 +50,11 @@
= f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block' = 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 %span.form-text.text-muted#home_help_block We will redirect non-logged in users to this page
.form-group .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' = 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 %span.form-text.text-muted#after_sign_out_path_help_block We will redirect users to this page after they sign out
.form-group .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 = f.text_area :sign_in_text, class: 'form-control', rows: 4
.form-text.text-muted Markdown enabled .form-text.text-muted Markdown enabled
= f.submit 'Save changes', class: "gl-button btn btn-success" = f.submit 'Save changes', class: "gl-button btn btn-success"
- 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 %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 - if description = @alert.description
%p %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 %> <% if description = @alert.description %>
<%= _('Description:') %> <%= description %> <%= _('Description:') %> <%= description %>
......
...@@ -6,6 +6,8 @@ ...@@ -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 } - 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? - 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) } } .js-design-management{ data: { project_path: @project.full_path, issue_iid: @issue.iid, issue_path: project_issue_path(@project, @issue) } }
- else - else
.gl-border-solid.gl-border-1.gl-border-gray-100.gl-rounded-base.gl-mt-5.gl-p-3.gl-text-center .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 .btn-group
= render 'shared/issuable/csv_export/button', issuable_type: 'merge-requests' = render 'shared/issuable/csv_export/button', issuable_type: 'merge-requests'
...@@ -8,5 +8,5 @@ ...@@ -8,5 +8,5 @@
= link_to new_merge_request_path, class: "gl-button btn btn-success", title: "New merge request" do = link_to new_merge_request_path, class: "gl-button btn btn-success", title: "New merge request" do
New merge request 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' = render 'shared/issuable/csv_export/modal', issuable_type: 'merge_requests'
...@@ -75,6 +75,8 @@ ...@@ -75,6 +75,8 @@
.settings-content .settings-content
= render 'projects/registry/settings/index' = render 'projects/registry/settings/index'
= render_if_exists 'projects/settings/ci_cd/auto_rollback', expanded: expanded
- if can?(current_user, :create_freeze_period, @project) - if can?(current_user, :create_freeze_period, @project)
%section.settings.no-animate#js-deploy-freeze-settings{ class: ('expanded' if expanded) } %section.settings.no-animate#js-deploy-freeze-settings{ class: ('expanded' if expanded) }
.settings-header .settings-header
......
.search_box .search_box.gl-my-8
.search_glyph .search_glyph
%h4 %h4
= sprite_icon('search', size: 24, css_class: 'gl-vertical-align-text-bottom') = sprite_icon('search', size: 24, css_class: 'gl-vertical-align-text-bottom')
= search_entries_empty_message(@scope, @search_term) = search_entries_empty_message(@scope, @search_term, @group, @project)
...@@ -23,30 +23,33 @@ ...@@ -23,30 +23,33 @@
%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')
.detail-page-header-actions.js-issuable-actions.js-issuable-buttons{ data: { "action": "close-reopen" } } - if Feature.enabled?(:vue_issue_header, @project)
.clearfix.issue-btn-group.dropdown .js-issue-header-actions{ data: issue_header_actions_data(@project, @issue, current_user) }
%button.btn.gl-button.btn-default.float-left.d-md-none.d-lg-none.d-xl-none{ type: "button", data: { toggle: "dropdown" } } - else
= _('Options') .detail-page-header-actions.js-issuable-actions.js-issuable-buttons{ data: { "action": "close-reopen" } }
= icon('caret-down') .clearfix.issue-btn-group.dropdown
.dropdown-menu.dropdown-menu-right.d-lg-none.d-xl-none %button.btn.gl-button.btn-default.float-left.d-md-none.d-lg-none.d-xl-none{ type: "button", data: { toggle: "dropdown" } }
%ul = _('Options')
- unless current_user == issuable.author = icon('caret-down')
%li= link_to _('Report abuse'), new_abuse_report_path(user_id: issuable.author.id, ref_url: issue_url(issuable)) .dropdown-menu.dropdown-menu-right.d-lg-none.d-xl-none
- if can_update_issue %ul
%li= link_to _('Close %{display_issuable_type}') % { display_issuable_type: display_issuable_type }, issue_path(issuable, issue: { state_event: :close }, format: 'json'), class: "btn-close js-btn-issue-action #{issue_button_visibility(issuable, true)}", title: _('Close %{display_issuable_type}') % { display_issuable_type: display_issuable_type } - unless current_user == issuable.author
- if can_reopen_issue %li= link_to _('Report abuse'), new_abuse_report_path(user_id: issuable.author.id, ref_url: issue_url(issuable))
%li= link_to _('Reopen %{display_issuable_type}') % { display_issuable_type: display_issuable_type }, issue_path(issuable, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen js-btn-issue-action #{issue_button_visibility(issuable, false)}", title: _('Reopen %{display_issuable_type}') % { display_issuable_type: display_issuable_type } - if can_update_issue
- if can_report_spam %li= link_to _('Close %{display_issuable_type}') % { display_issuable_type: display_issuable_type }, issue_path(issuable, issue: { state_event: :close }, format: 'json'), class: "btn-close js-btn-issue-action #{issue_button_visibility(issuable, true)}", title: _('Close %{display_issuable_type}') % { display_issuable_type: display_issuable_type }
%li= link_to _('Submit as spam'), mark_as_spam_project_issue_path(@project, issuable), method: :post, class: 'btn-spam', title: 'Submit as spam' - if can_reopen_issue
- if can_create_issue %li= link_to _('Reopen %{display_issuable_type}') % { display_issuable_type: display_issuable_type }, issue_path(issuable, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen js-btn-issue-action #{issue_button_visibility(issuable, false)}", title: _('Reopen %{display_issuable_type}') % { display_issuable_type: display_issuable_type }
- if can_update_issue || can_report_spam - if can_report_spam
%li.divider %li= link_to _('Submit as spam'), mark_as_spam_project_issue_path(@project, issuable), method: :post, class: 'btn-spam', title: 'Submit as spam'
%li= link_to _('New %{display_issuable_type}') % { display_issuable_type: display_issuable_type }, new_project_issue_path(@project, new_issuable_params), id: 'new_%{display_issuable_type}_link' % { display_issuable_type: display_issuable_type } - if can_create_issue
- if can_update_issue || can_report_spam
%li.divider
%li= link_to _('New %{display_issuable_type}') % { display_issuable_type: display_issuable_type }, new_project_issue_path(@project, new_issuable_params), id: 'new_%{display_issuable_type}_link' % { display_issuable_type: display_issuable_type }
= render 'shared/issuable/close_reopen_button', issuable: issuable, can_update: can_update_issue, can_reopen: can_reopen_issue, warn_before_close: defined?(issuable.blocked?) && issuable.blocked? = render 'shared/issuable/close_reopen_button', issuable: issuable, can_update: can_update_issue, can_reopen: can_reopen_issue, warn_before_close: defined?(issuable.blocked?) && issuable.blocked?
- if can_report_spam - if can_report_spam
= link_to _('Submit as spam'), mark_as_spam_project_issue_path(@project, issuable), method: :post, class: 'gl-display-none gl-display-sm-none gl-display-md-block gl-button btn btn-grouped btn-spam', title: 'Submit as spam' = link_to _('Submit as spam'), mark_as_spam_project_issue_path(@project, issuable), method: :post, class: 'gl-display-none gl-display-sm-none gl-display-md-block gl-button btn btn-grouped btn-spam', title: 'Submit as spam'
- if can_create_issue - if can_create_issue
= link_to new_project_issue_path(@project, new_issuable_params), class: 'gl-display-none gl-display-sm-none gl-display-md-block gl-button btn btn-grouped btn-success btn-inverted', title: _('New %{display_issuable_type}') % { display_issuable_type: display_issuable_type }, id: 'new_%{display_issuable_type}_link' % { display_issuable_type: display_issuable_type } do = link_to new_project_issue_path(@project, new_issuable_params), class: 'gl-display-none gl-display-sm-none gl-display-md-block gl-button btn btn-grouped btn-success btn-inverted', title: _('New %{display_issuable_type}') % { display_issuable_type: display_issuable_type }, id: 'new_%{display_issuable_type}_link' % { display_issuable_type: display_issuable_type } do
= _('New %{display_issuable_type}') % { display_issuable_type: display_issuable_type } = _('New %{display_issuable_type}') % { display_issuable_type: display_issuable_type }
...@@ -27,8 +27,9 @@ class RepositoryCleanupWorker # rubocop:disable Scalability/IdempotentWorker ...@@ -27,8 +27,9 @@ class RepositoryCleanupWorker # rubocop:disable Scalability/IdempotentWorker
project = Project.find(project_id) project = Project.find(project_id)
user = User.find(user_id) user = User.find(user_id)
# Ensure the file is removed # Ensure the file is removed and the repository is made read-write again
project.bfg_object_map.remove! Projects::CleanupService.cleanup_after(project)
notification_service.repository_cleanup_failure(project, user, error) notification_service.repository_cleanup_failure(project, user, error)
end end
......
---
title: Make the repository read-only while running cleanup
merge_request: 45058
author:
type: changed
---
title: Update leave group modal to gl-modal
merge_request: 41817
author:
type: changed
---
title: Resolve Implement GraphQL Startup.js for Design Management app
merge_request: 46660
author:
type: other
---
title: Improve empty search results message for group and project scopes
merge_request: 46237
author:
type: changed
---
title: Add filtering by recorded date to instance statistics measurements GraphQL API
merge_request: 46344
author:
type: changed
---
title: Do not query snippet infromation on the new snippet's creation
merge_request: 46355
author:
type: fixed
---
title: Corrected grammar in Sign-in restrictions text
merge_request: 46500
author:
type: other
---
title: Add search param to Users GraphQL type
merge_request: 46609
author:
type: added
---
title: Add auto_rollback_enabled column to project_ci_cd_settings table
merge_request: 45816
author:
type: other
---
title: Enable MR CSV export
merge_request: 46662
author:
type: added
---
title: Fix example responses for Project Issue Board creation API in the docs
merge_request: 46749
author: Takuya Noguchi
type: fixed
---
title: Add `has_vulnerabilities` column into project_settings table
merge_request: 45944
author:
type: added
---
title: 'GraphQL: Adds downstream, upstream, source job, path, and project to PipelineType'
merge_request: 45212
author:
type: added
---
title: Remove the ability to resole individual notes
merge_request: 46775
author:
type: removed
---
title: Improve messaging for emails from alerts
merge_request: 43054
author:
type: changed
---
name: cd_auto_rollback
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45816
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/35404
type: development
group: group::progressive delivery
default_enabled: false
...@@ -4,4 +4,4 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45130 ...@@ -4,4 +4,4 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45130
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/267129 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/267129
type: development type: development
group: group::compliance group: group::compliance
default_enabled: false default_enabled: true
...@@ -4,4 +4,4 @@ introduced_by_url: ...@@ -4,4 +4,4 @@ introduced_by_url:
rollout_issue_url: rollout_issue_url:
type: development type: development
group: group::source code group: group::source code
default_enabled: false default_enabled: true
---
name: vue_issue_header
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44440
rollout_issue_url:
type: development
group: group::project management
default_enabled: false
...@@ -16,7 +16,6 @@ if Gitlab.ee? && Gitlab.dev_or_test_env? ...@@ -16,7 +16,6 @@ if Gitlab.ee? && Gitlab.dev_or_test_env?
IGNORED_FEATURE_FLAGS = %i[ IGNORED_FEATURE_FLAGS = %i[
ci_secrets_management ci_secrets_management
feature_flags_related_issues feature_flags_related_issues
group_coverage_reports
group_wikis group_wikis
incident_sla incident_sla
swimlanes swimlanes
......
require './spec/support/sidekiq_middleware' require './spec/support/sidekiq_middleware'
SNIPPET_REPO_URL = "https://gitlab.com/gitlab-org/gitlab-snippet-test.git" SNIPPET_REPO_URL = "https://gitlab.com/gitlab-org/gitlab-snippet-test.git"
BUNDLE_PATH = File.join(Rails.root, 'db/fixtures/development/gitlab-snippet-test.bundle')
class Gitlab::Seeder::SnippetRepository
def initialize(snippet)
@snippet = snippet
end
def import
if File.exists?(BUNDLE_PATH)
@snippet.repository.create_from_bundle(BUNDLE_PATH)
else
@snippet.repository.import_repository(SNIPPET_REPO_URL)
@snippet.repository.bundle_to_disk(BUNDLE_PATH)
end
end
def self.cleanup
File.delete(BUNDLE_PATH) if File.exists?(BUNDLE_PATH)
rescue => e
warn "\nError cleaning up snippet bundle: #{e}"
end
end
Gitlab::Seeder.quiet do Gitlab::Seeder.quiet do
20.times do |i| 20.times do |i|
...@@ -14,7 +36,7 @@ Gitlab::Seeder.quiet do ...@@ -14,7 +36,7 @@ Gitlab::Seeder.quiet do
content: 'foo' content: 'foo'
}).tap do |snippet| }).tap do |snippet|
unless snippet.repository_exists? unless snippet.repository_exists?
snippet.repository.import_repository(SNIPPET_REPO_URL) Gitlab::Seeder::SnippetRepository.new(snippet).import
end end
snippet.track_snippet_repository(snippet.repository.storage) snippet.track_snippet_repository(snippet.repository.storage)
...@@ -23,5 +45,7 @@ Gitlab::Seeder.quiet do ...@@ -23,5 +45,7 @@ Gitlab::Seeder.quiet do
print('.') print('.')
end end
Gitlab::Seeder::SnippetRepository.cleanup
end end
# frozen_string_literal: true
class AddHasVulnerabilitiesIntoProjectSettings < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_column :project_settings, :has_vulnerabilities, :boolean, default: false, null: false
end
end
def down
with_lock_retries do
remove_column :project_settings, :has_vulnerabilities
end
end
end
# frozen_string_literal: true
class IndexProjectSettingsOnProjectIdPartially < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX_NAME = 'index_project_settings_on_project_id_partially'
disable_ddl_transaction!
def up
add_concurrent_index :project_settings, :project_id, name: INDEX_NAME, where: 'has_vulnerabilities IS TRUE'
end
def down
remove_concurrent_index_by_name :project_settings, INDEX_NAME
end
end
# frozen_string_literal: true
class AddAutoRollbackSetting < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_column :project_ci_cd_settings, :auto_rollback_enabled, :boolean, default: false, null: false
end
end
def down
with_lock_retries do
remove_column :project_ci_cd_settings, :auto_rollback_enabled
end
end
end
205cb628e9637bcd1acb90c5211b71b51015fa5f50aadcacd5fbafc4f09c00d0
\ No newline at end of file
9f942de6f83629a144e9d460b4bed7a246afe95275b5913745109fc0ab9dacc1
\ No newline at end of file
3f24bfc2d18ffa5f171e027d4e7aaf9994b255e5806e2de57fd36b4a193db122
\ No newline at end of file
...@@ -14866,7 +14866,8 @@ CREATE TABLE project_ci_cd_settings ( ...@@ -14866,7 +14866,8 @@ CREATE TABLE project_ci_cd_settings (
merge_pipelines_enabled boolean, merge_pipelines_enabled boolean,
default_git_depth integer, default_git_depth integer,
forward_deployment_enabled boolean, forward_deployment_enabled boolean,
merge_trains_enabled boolean DEFAULT false merge_trains_enabled boolean DEFAULT false,
auto_rollback_enabled boolean DEFAULT false NOT NULL
); );
CREATE SEQUENCE project_ci_cd_settings_id_seq CREATE SEQUENCE project_ci_cd_settings_id_seq
...@@ -15190,6 +15191,7 @@ CREATE TABLE project_settings ( ...@@ -15190,6 +15191,7 @@ CREATE TABLE project_settings (
allow_merge_on_skipped_pipeline boolean, allow_merge_on_skipped_pipeline boolean,
squash_option smallint DEFAULT 3, squash_option smallint DEFAULT 3,
has_confluence boolean DEFAULT false NOT NULL, has_confluence boolean DEFAULT false NOT NULL,
has_vulnerabilities boolean DEFAULT false NOT NULL,
CONSTRAINT check_bde223416c CHECK ((show_default_award_emojis IS NOT NULL)) CONSTRAINT check_bde223416c CHECK ((show_default_award_emojis IS NOT NULL))
); );
...@@ -21460,6 +21462,8 @@ CREATE UNIQUE INDEX index_project_repository_states_on_project_id ON project_rep ...@@ -21460,6 +21462,8 @@ CREATE UNIQUE INDEX index_project_repository_states_on_project_id ON project_rep
CREATE INDEX index_project_repository_storage_moves_on_project_id ON project_repository_storage_moves USING btree (project_id); CREATE INDEX index_project_repository_storage_moves_on_project_id ON project_repository_storage_moves USING btree (project_id);
CREATE INDEX index_project_settings_on_project_id_partially ON project_settings USING btree (project_id) WHERE (has_vulnerabilities IS TRUE);
CREATE UNIQUE INDEX index_project_settings_on_push_rule_id ON project_settings USING btree (push_rule_id); CREATE UNIQUE INDEX index_project_settings_on_push_rule_id ON project_settings USING btree (push_rule_id);
CREATE INDEX index_project_statistics_on_namespace_id ON project_statistics USING btree (namespace_id); CREATE INDEX index_project_statistics_on_namespace_id ON project_statistics USING btree (namespace_id);
......
...@@ -17,12 +17,9 @@ a service networking solution that you can manage by using `/etc/gitlab/gitlab.r ...@@ -17,12 +17,9 @@ a service networking solution that you can manage by using `/etc/gitlab/gitlab.r
## Configure the Consul nodes ## Configure the Consul nodes
NOTE: **Important:** After you review the [reference architecture](reference_architectures/index.md#available-reference-architectures)
Before proceeding, refer to the documentation to determine the number of Consul server nodes you should have,
[available reference architectures](reference_architectures/index.md#available-reference-architectures) on _each_ Consul server node:
to find out how many Consul server nodes you should have.
On **each** Consul server node perform the following:
1. Follow the instructions to [install](https://about.gitlab.com/install/) 1. Follow the instructions to [install](https://about.gitlab.com/install/)
GitLab by choosing your preferred platform, but do not supply the GitLab by choosing your preferred platform, but do not supply the
...@@ -93,10 +90,9 @@ Consult the [troubleshooting section](#troubleshooting-consul) if the cluster is ...@@ -93,10 +90,9 @@ Consult the [troubleshooting section](#troubleshooting-consul) if the cluster is
able to recover after the upgrade. The [outage recovery](#outage-recovery) may able to recover after the upgrade. The [outage recovery](#outage-recovery) may
be of particular interest. be of particular interest.
NOTE: **Note:** GitLab uses Consul to store only easily regenerated, transient data. If the
GitLab uses Consul to store only transient data that is easily regenerated. If bundled Consul wasn't used by any process other than GitLab itself, you can
the bundled Consul was not used by any process other than GitLab itself, then [rebuild the cluster from scratch](#recreate-from-scratch).
[rebuilding the cluster from scratch](#recreate-from-scratch) is fine.
## Troubleshooting Consul ## Troubleshooting Consul
......
...@@ -188,7 +188,7 @@ successfully, you must replicate their data using some other means. ...@@ -188,7 +188,7 @@ successfully, you must replicate their data using some other means.
| [Composer Repository](../../../user/packages/composer_repository/index.md) | **Yes** (13.2) | [No](https://gitlab.com/groups/gitlab-org/-/epics/1817) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_package_file_replication`, enabled by default | | [Composer Repository](../../../user/packages/composer_repository/index.md) | **Yes** (13.2) | [No](https://gitlab.com/groups/gitlab-org/-/epics/1817) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_package_file_replication`, enabled by default |
| [Generic packages](../../../user/packages/generic_packages/index.md) | **Yes** (13.5) | [No](https://gitlab.com/groups/gitlab-org/-/epics/1817) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_package_file_replication`, enabled by default | | [Generic packages](../../../user/packages/generic_packages/index.md) | **Yes** (13.5) | [No](https://gitlab.com/groups/gitlab-org/-/epics/1817) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_package_file_replication`, enabled by default |
| [Versioned Terraform State](../../terraform_state.md) | **Yes** (13.5) | No | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_terraform_state_version_replication`, enabled by default | | [Versioned Terraform State](../../terraform_state.md) | **Yes** (13.5) | No | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_terraform_state_version_replication`, enabled by default |
| [External merge request diffs](../../merge_request_diffs.md) | **Yes** (13.5) | No | Behind feature flag `geo_merge_request_diff_replication`, enabled by default | | | [External merge request diffs](../../merge_request_diffs.md) | **Yes** (13.5) | No | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_merge_request_diff_replication`, enabled by default |
| [Versioned snippets](../../../user/snippets.md#versioned-snippets) | [No](https://gitlab.com/groups/gitlab-org/-/epics/2809) | [No](https://gitlab.com/groups/gitlab-org/-/epics/2810) | No | | | [Versioned snippets](../../../user/snippets.md#versioned-snippets) | [No](https://gitlab.com/groups/gitlab-org/-/epics/2809) | [No](https://gitlab.com/groups/gitlab-org/-/epics/2810) | No | |
| [Server-side Git hooks](../../server_hooks.md) | [No](https://gitlab.com/groups/gitlab-org/-/epics/1867) | No | No | | | [Server-side Git hooks](../../server_hooks.md) | [No](https://gitlab.com/groups/gitlab-org/-/epics/1867) | No | No | |
| [Elasticsearch integration](../../../integration/elasticsearch.md) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/1186) | No | No | | | [Elasticsearch integration](../../../integration/elasticsearch.md) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/1186) | No | No | |
......
...@@ -179,7 +179,6 @@ Plan.default.actual_limits.update!(project_hooks: 100) ...@@ -179,7 +179,6 @@ Plan.default.actual_limits.update!(project_hooks: 100)
Plan.default.actual_limits.update!(group_hooks: 100) Plan.default.actual_limits.update!(group_hooks: 100)
``` ```
NOTE: **Note:**
Set the limit to `0` to disable it. Set the limit to `0` to disable it.
## Incoming emails from auto-responders ## Incoming emails from auto-responders
...@@ -217,7 +216,6 @@ Plan.default.actual_limits.update!(offset_pagination_limit: 10000) ...@@ -217,7 +216,6 @@ Plan.default.actual_limits.update!(offset_pagination_limit: 10000)
- **Default offset pagination limit:** 50000 - **Default offset pagination limit:** 50000
NOTE: **Note:**
Set the limit to `0` to disable it. Set the limit to `0` to disable it.
## CI/CD limits ## CI/CD limits
...@@ -250,7 +248,6 @@ To set this limit on a self-managed installation, run the following in the ...@@ -250,7 +248,6 @@ To set this limit on a self-managed installation, run the following in the
Plan.default.actual_limits.update!(ci_active_jobs: 500) Plan.default.actual_limits.update!(ci_active_jobs: 500)
``` ```
NOTE: **Note:**
Set the limit to `0` to disable it. Set the limit to `0` to disable it.
### Number of CI/CD subscriptions to a project ### Number of CI/CD subscriptions to a project
...@@ -273,7 +270,6 @@ To set this limit on a self-managed installation, run the following in the ...@@ -273,7 +270,6 @@ To set this limit on a self-managed installation, run the following in the
Plan.default.actual_limits.update!(ci_project_subscriptions: 500) Plan.default.actual_limits.update!(ci_project_subscriptions: 500)
``` ```
NOTE: **Note:**
Set the limit to `0` to disable it. Set the limit to `0` to disable it.
### Number of pipeline schedules ### Number of pipeline schedules
...@@ -462,11 +458,10 @@ Setting a limit helps reduce the memory usage of the indexing processes as well ...@@ -462,11 +458,10 @@ Setting a limit helps reduce the memory usage of the indexing processes as well
as the overall index size. This value defaults to `1024 KiB` (1 MiB) as any as the overall index size. This value defaults to `1024 KiB` (1 MiB) as any
text files larger than this likely aren't meant to be read by humans. text files larger than this likely aren't meant to be read by humans.
NOTE: **Note:** You must set a limit, as unlimited file sizes aren't supported. Setting this
You must set a limit, as an unlimited file size is not supported. Setting this value to be greater than the amount of memory on GitLab's Sidekiq nodes causes
value to be greater than the amount of memory on GitLab's Sidekiq nodes will GitLab's Sidekiq nodes to run out of memory, as they will pre-allocate this
lead to GitLab's Sidekiq nodes running out of memory as they will pre-allocate amount of memory during indexing.
this amount of memory during indexing.
### Maximum field length ### Maximum field length
...@@ -486,7 +481,6 @@ indexed](#maximum-file-size-indexed)). ...@@ -486,7 +481,6 @@ indexed](#maximum-file-size-indexed)).
This limit can be configured for self-managed installations when [enabling This limit can be configured for self-managed installations when [enabling
Elasticsearch](../integration/elasticsearch.md#enabling-advanced-search). Elasticsearch](../integration/elasticsearch.md#enabling-advanced-search).
NOTE: **Note:**
Set the limit to `0` to disable it. Set the limit to `0` to disable it.
## Wiki limits ## Wiki limits
......
...@@ -19,11 +19,10 @@ From GitLab 13.0, using NFS for Git repositories is deprecated. In GitLab 14.0, ...@@ -19,11 +19,10 @@ From GitLab 13.0, using NFS for Git repositories is deprecated. In GitLab 14.0,
support for NFS for Git repositories is scheduled to be removed. Upgrade to support for NFS for Git repositories is scheduled to be removed. Upgrade to
[Gitaly Cluster](gitaly/praefect.md) as soon as possible. [Gitaly Cluster](gitaly/praefect.md) as soon as possible.
NOTE: **Note:** Filesystem performance can impact overall GitLab performance, especially for
Filesystem performance has a big impact on overall GitLab actions that read or write to Git repositories. For steps you can use to test
performance, especially for actions that read or write to Git repositories. See filesystem performance, see
[Filesystem Performance Benchmarking](operations/filesystem_benchmarking.md) [Filesystem Performance Benchmarking](operations/filesystem_benchmarking.md).
for steps to test filesystem performance.
## Known kernel version incompatibilities ## Known kernel version incompatibilities
......
...@@ -61,18 +61,17 @@ must be enabled, only the following providers can be used: ...@@ -61,18 +61,17 @@ must be enabled, only the following providers can be used:
- [Google Cloud Storage](#google-cloud-storage-gcs) - [Google Cloud Storage](#google-cloud-storage-gcs)
- [Azure Blob storage](#azure-blob-storage) - [Azure Blob storage](#azure-blob-storage)
Background upload is not supported with the consolidated object storage Background upload isn't supported with the consolidated object storage
configuration. We recommend enabling direct upload mode because it does configuration. We recommend enabling direct upload mode because it doesn't
not require a shared folder, and [this setting may become the require a shared folder, and [this setting may become the
default](https://gitlab.com/gitlab-org/gitlab/-/issues/27331). default](https://gitlab.com/gitlab-org/gitlab/-/issues/27331).
NOTE: **Note:** Consolidated object storage configuration can't be used for backups or
Consolidated object storage configuration cannot be used for Mattermost. See the [full table for a complete list](#storage-specific-configuration).
backups or Mattermost. See [the full table for a complete list](#storage-specific-configuration).
NOTE: **Note:** Enabling consolidated object storage enables object storage for all object
Enabling consolidated object storage will enable object storage for all object types. types. If you want to use local storage for specific object types, you can
If you wish to use local storage for specific object types, you can [selectively disable object storages](#selectively-disabling-object-storage). [selectively disable object storages](#selectively-disabling-object-storage).
Most types of objects, such as CI artifacts, LFS files, upload Most types of objects, such as CI artifacts, LFS files, upload
attachments, and so on can be saved in object storage by specifying a single attachments, and so on can be saved in object storage by specifying a single
...@@ -347,10 +346,9 @@ gitlab_rails['object_store']['connection'] = { ...@@ -347,10 +346,9 @@ gitlab_rails['object_store']['connection'] = {
###### Azure Workhorse settings (source installs only) ###### Azure Workhorse settings (source installs only)
NOTE: **Note:** For source installations, Workhorse also needs to be configured with Azure
For source installations, Workhorse needs to be configured with the credentials. This isn't needed in Omnibus installs, because the Workhorse
Azure credentials as well. This is not needed in Omnibus installs because settings are populated from the previous settings.
the Workhorse settings are populated from the settings above.
1. Edit `/home/git/gitlab-workhorse/config.toml` and add or amend the following lines: 1. Edit `/home/git/gitlab-workhorse/config.toml` and add or amend the following lines:
...@@ -370,14 +368,14 @@ GitLab Rails and Workhorse. ...@@ -370,14 +368,14 @@ GitLab Rails and Workhorse.
#### OpenStack-compatible connection settings #### OpenStack-compatible connection settings
NOTE: **Note:** Although OpenStack Swift provides S3 compatibility, some users may want to use
This is not compatible with the consolidated object storage form. the [Swift API](https://docs.openstack.org/swift/latest/api/object_api_v1_overview.html).
OpenStack Swift is only supported with the storage-specific form. See the
[S3 settings](#s3-compatible-connection-settings) if you want to use the consolidated form.
While OpenStack Swift provides S3 compatibility, some users may want to use the This isn't compatible with the consolidated object storage form. OpenStack Swift
[Swift API](https://docs.openstack.org/swift/latest/api/object_api_v1_overview.html). is supported only with the storage-specific form. If you want to use the
Here are the valid connection settings below for the Swift API, provided by consolidated form, see the [S3 settings](#s3-compatible-connection-settings).
Here are the valid connection settings for the Swift API, provided by
[fog-openstack](https://github.com/fog/fog-openstack): [fog-openstack](https://github.com/fog/fog-openstack):
| Setting | Description | Default | | Setting | Description | Default |
...@@ -392,12 +390,11 @@ Here are the valid connection settings below for the Swift API, provided by ...@@ -392,12 +390,11 @@ Here are the valid connection settings below for the Swift API, provided by
#### Rackspace Cloud Files #### Rackspace Cloud Files
NOTE: **Note:** The following table describes the valid connection parameters for
This is not compatible with the consolidated object Rackspace Cloud, provided by [fog-rackspace](https://github.com/fog/fog-rackspace/).
storage form. Rackspace Cloud is only supported with the storage-specific form.
Here are the valid connection parameters for Rackspace Cloud, provided by This isn't compatible with the consolidated object storage form.
[fog-rackspace](https://github.com/fog/fog-rackspace/): Rackspace Cloud is supported only with the storage-specific form.
| Setting | Description | example | | Setting | Description | example |
|---------|-------------|---------| |---------|-------------|---------|
...@@ -407,13 +404,13 @@ Here are the valid connection parameters for Rackspace Cloud, provided by ...@@ -407,13 +404,13 @@ Here are the valid connection parameters for Rackspace Cloud, provided by
| `rackspace_region` | The Rackspace storage region to use, a three letter code from the [list of service access endpoints](https://developer.rackspace.com/docs/cloud-files/v1/general-api-info/service-access/) | `iad` | | `rackspace_region` | The Rackspace storage region to use, a three letter code from the [list of service access endpoints](https://developer.rackspace.com/docs/cloud-files/v1/general-api-info/service-access/) | `iad` |
| `rackspace_temp_url_key` | The private key you have set in the Rackspace API for [temporary URLs](https://developer.rackspace.com/docs/cloud-files/v1/use-cases/public-access-to-your-cloud-files-account/#tempurl). | `ABC123DEF456ABC123DEF456ABC123DE` | | `rackspace_temp_url_key` | The private key you have set in the Rackspace API for [temporary URLs](https://developer.rackspace.com/docs/cloud-files/v1/use-cases/public-access-to-your-cloud-files-account/#tempurl). | `ABC123DEF456ABC123DEF456ABC123DE` |
NOTE: **Note:** Regardless of whether the container has public access enabled or disabled, Fog
Regardless of whether the container has public access enabled or disabled, Fog will uses the TempURL method to grant access to LFS objects. If you see error
use the TempURL method to grant access to LFS objects. If you see errors in logs referencing messages in logs that refer to instantiating storage with a `temp-url-key`,
instantiating storage with a `temp-url-key`, ensure that you have set the key properly be sure you have set the key properly both in the Rackspace API and in
on the Rackspace API and in `gitlab.rb`. You can verify the value of the key Rackspace `gitlab.rb`. You can verify the value of the key Rackspace has set by sending a
has set by sending a GET request with token header to the service access endpoint URL GET request with token header to the service access endpoint URL and comparing
and comparing the output of the returned headers. the output of the returned headers.
### Object-specific configuration ### Object-specific configuration
...@@ -521,15 +518,16 @@ gitlab_rails['uploads_object_store_remote_directory'] = 'uploads' ...@@ -521,15 +518,16 @@ gitlab_rails['uploads_object_store_remote_directory'] = 'uploads'
gitlab_rails['uploads_object_store_connection'] = { 'provider' => 'AWS', 'aws_access_key_id' => 'access_key', 'aws_secret_access_key' => 'secret' } gitlab_rails['uploads_object_store_connection'] = { 'provider' => 'AWS', 'aws_access_key_id' => 'access_key', 'aws_secret_access_key' => 'secret' }
``` ```
While this provides flexibility in that it makes it possible for GitLab Although this provides flexibility in that it makes it possible for GitLab
to store objects across different cloud providers, it also creates to store objects across different cloud providers, it also creates
additional complexity and unnecessary redundancy. Since both GitLab additional complexity and unnecessary redundancy. Since both GitLab
Rails and Workhorse components need access to object storage, the Rails and Workhorse components need access to object storage, the
consolidated form avoids excessive duplication of credentials. consolidated form avoids excessive duplication of credentials.
NOTE: **Note:** The consolidated object storage configuration is used _only_ if all lines from
The consolidated object storage configuration is **only** used if all the original form is omitted. To move to the consolidated form, remove the
lines from the original form is omitted. To move to the consolidated form, remove the original configuration (for example, `artifacts_object_store_enabled`, `uploads_object_store_connection`, and so on.) original configuration (for example, `artifacts_object_store_enabled`, or
`uploads_object_store_connection`)
## Storage-specific configuration ## Storage-specific configuration
......
...@@ -999,3 +999,19 @@ project = Project.find_by_full_path('<group/project>') ...@@ -999,3 +999,19 @@ project = Project.find_by_full_path('<group/project>')
Geo::RepositorySyncService.new(project).execute Geo::RepositorySyncService.new(project).execute
``` ```
### Generate usage ping
#### Generate or get the cached usage ping
```ruby
Gitlab::UsageData.to_json
```
#### Generate a fresh new usage ping
This will also refresh the cached usage ping displayed in the admin area
```ruby
Gitlab::UsageData.to_json(force_refresh: true)
```
...@@ -203,48 +203,12 @@ Example response: ...@@ -203,48 +203,12 @@ Example response:
"web_url": "http://example.com/diaspora/diaspora-project-site" "web_url": "http://example.com/diaspora/diaspora-project-site"
}, },
"name": "newboard", "name": "newboard",
"milestone": { "lists" : [],
"id": 12 "group": null,
"title": "10.0" "milestone": null,
}, "assignee" : null,
"lists" : [ "labels" : [],
{ "weight" : null
"id" : 1,
"label" : {
"name" : "Testing",
"color" : "#F0AD4E",
"description" : null
},
"position" : 1,
"max_issue_count": 0,
"max_issue_weight": 0,
"limit_metric": null
},
{
"id" : 2,
"label" : {
"name" : "Ready",
"color" : "#FF0000",
"description" : null
},
"position" : 2,
"max_issue_count": 0,
"max_issue_weight": 0,
"limit_metric": null
},
{
"id" : 3,
"label" : {
"name" : "Production",
"color" : "#FF5F00",
"description" : null
},
"position" : 3,
"max_issue_count": 0,
"max_issue_weight": 0,
"limit_metric": null
}
]
} }
``` ```
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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