summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue4
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js31
-rw-r--r--app/assets/javascripts/namespaces/cascading_settings/components/lock_popovers.vue77
-rw-r--r--app/assets/javascripts/namespaces/cascading_settings/index.js15
-rw-r--r--app/assets/javascripts/pages/groups/edit/index.js2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue13
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue28
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue77
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue28
-rw-r--r--app/assets/javascripts/pipelines/components/unwrapping_utils.js14
-rw-r--r--app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue1
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/app.vue27
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue3
-rw-r--r--app/assets/javascripts/whats_new/components/app.vue58
-rw-r--r--app/assets/javascripts/whats_new/index.js6
-rw-r--r--app/assets/javascripts/whats_new/store/actions.js10
-rw-r--r--app/assets/javascripts/whats_new/utils/notification.js13
-rw-r--r--app/assets/stylesheets/page_bundles/pipeline.scss14
-rw-r--r--app/controllers/concerns/creates_commit.rb24
-rw-r--r--app/controllers/concerns/renders_commits.rb3
-rw-r--r--app/controllers/projects/commit_controller.rb23
-rw-r--r--app/controllers/whats_new_controller.rb12
-rw-r--r--app/helpers/commits_helper.rb2
-rw-r--r--app/helpers/namespaces_helper.rb27
-rw-r--r--app/helpers/whats_new_helper.rb12
-rw-r--r--app/models/release_highlight.rb23
-rw-r--r--app/views/admin/broadcast_messages/index.html.haml21
-rw-r--r--app/views/dashboard/_projects_head.html.haml4
-rw-r--r--app/views/devise/mailer/_confirmation_instructions_account.html.haml12
-rw-r--r--app/views/groups/edit.html.haml1
-rw-r--r--app/views/groups/settings/_permissions.html.haml30
-rw-r--r--app/views/groups/settings/_project_access_token_creation.html.haml10
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/layouts/header/_whats_new_dropdown_item.html.haml2
-rw-r--r--app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml14
-rw-r--r--app/views/shared/namespaces/cascading_settings/_lock_popovers.html.haml1
-rw-r--r--app/views/shared/namespaces/cascading_settings/_setting_label.html.haml21
37 files changed, 445 insertions, 220 deletions
diff --git a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
index a17378edafd..b0f19e5b585 100644
--- a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
@@ -214,7 +214,7 @@ export default {
<gl-form-checkbox
v-model="issueTransitionEnabled"
:disabled="isInheriting"
- data-qa-selector="service_jira_issue_transition_enabled"
+ data-qa-selector="service_jira_issue_transition_enabled_checkbox"
>
{{ s__('JiraService|Enable Jira transitions') }}
</gl-form-checkbox>
@@ -232,7 +232,7 @@ export default {
name="service[jira_issue_transition_automatic]"
:value="issueTransitionOption.value"
:disabled="isInheriting"
- :data-qa-selector="`service_jira_issue_transition_automatic_${issueTransitionOption.value}`"
+ :data-qa-selector="`service_jira_issue_transition_automatic_${issueTransitionOption.value}_radio`"
>
{{ issueTransitionOption.label }}
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 955f25a6f26..a509828815a 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -254,6 +254,37 @@ export const timeIntervalInWords = (intervalInSeconds) => {
: secondsText;
};
+/**
+ * Similar to `timeIntervalInWords`, but rounds the return value
+ * to 1/10th of the largest time unit. For example:
+ *
+ * 30 => 30 seconds
+ * 90 => 1.5 minutes
+ * 7200 => 2 hours
+ * 86400 => 1 day
+ * ... etc.
+ *
+ * The largest supported unit is "days".
+ *
+ * @param {Number} intervalInSeconds The time interval in seconds
+ * @returns {String} A humanized description of the time interval
+ */
+export const humanizeTimeInterval = (intervalInSeconds) => {
+ if (intervalInSeconds < 60 /* = 1 minute */) {
+ const seconds = Math.round(intervalInSeconds * 10) / 10;
+ return n__('%d second', '%d seconds', seconds);
+ } else if (intervalInSeconds < 3600 /* = 1 hour */) {
+ const minutes = Math.round(intervalInSeconds / 6) / 10;
+ return n__('%d minute', '%d minutes', minutes);
+ } else if (intervalInSeconds < 86400 /* = 1 day */) {
+ const hours = Math.round(intervalInSeconds / 360) / 10;
+ return n__('%d hour', '%d hours', hours);
+ }
+
+ const days = Math.round(intervalInSeconds / 8640) / 10;
+ return n__('%d day', '%d days', days);
+};
+
export const dateInWords = (date, abbreviated = false, hideYear = false) => {
if (!date) return date;
diff --git a/app/assets/javascripts/namespaces/cascading_settings/components/lock_popovers.vue b/app/assets/javascripts/namespaces/cascading_settings/components/lock_popovers.vue
new file mode 100644
index 00000000000..8de6e910bb6
--- /dev/null
+++ b/app/assets/javascripts/namespaces/cascading_settings/components/lock_popovers.vue
@@ -0,0 +1,77 @@
+<script>
+import { GlPopover, GlSprintf, GlLink } from '@gitlab/ui';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+export default {
+ name: 'LockPopovers',
+ components: {
+ GlPopover,
+ GlSprintf,
+ GlLink,
+ },
+ data() {
+ return {
+ targets: [],
+ };
+ },
+ mounted() {
+ this.targets = [...document.querySelectorAll('.js-cascading-settings-lock-popover-target')].map(
+ (el) => {
+ const {
+ dataset: { popoverData },
+ } = el;
+
+ const {
+ lockedByAncestor,
+ lockedByApplicationSetting,
+ ancestorNamespace,
+ } = convertObjectPropsToCamelCase(JSON.parse(popoverData || '{}'), { deep: true });
+
+ return {
+ el,
+ lockedByAncestor,
+ lockedByApplicationSetting,
+ ancestorNamespace,
+ };
+ },
+ );
+ },
+};
+</script>
+
+<template>
+ <div>
+ <template
+ v-for="(
+ { el, lockedByApplicationSetting, lockedByAncestor, ancestorNamespace }, index
+ ) in targets"
+ >
+ <gl-popover
+ v-if="lockedByApplicationSetting || lockedByAncestor"
+ :key="index"
+ :target="el"
+ placement="top"
+ >
+ <template #title>{{ s__('CascadingSettings|Setting enforced') }}</template>
+ <p data-testid="cascading-settings-lock-popover">
+ <template v-if="lockedByApplicationSetting">{{
+ s__('CascadingSettings|This setting has been enforced by an instance admin.')
+ }}</template>
+
+ <gl-sprintf
+ v-else-if="lockedByAncestor && ancestorNamespace"
+ :message="
+ s__('CascadingSettings|This setting has been enforced by an owner of %{link}.')
+ "
+ >
+ <template #link>
+ <gl-link :href="ancestorNamespace.path" class="gl-font-sm">{{
+ ancestorNamespace.fullName
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </gl-popover>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/namespaces/cascading_settings/index.js b/app/assets/javascripts/namespaces/cascading_settings/index.js
new file mode 100644
index 00000000000..3e44d1e9e2d
--- /dev/null
+++ b/app/assets/javascripts/namespaces/cascading_settings/index.js
@@ -0,0 +1,15 @@
+import Vue from 'vue';
+import LockPopovers from './components/lock_popovers.vue';
+
+export const initCascadingSettingsLockPopovers = () => {
+ const el = document.querySelector('.js-cascading-settings-lock-popovers');
+
+ if (!el) return false;
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(LockPopovers);
+ },
+ });
+};
diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js
index 176d2406751..49b9822795c 100644
--- a/app/assets/javascripts/pages/groups/edit/index.js
+++ b/app/assets/javascripts/pages/groups/edit/index.js
@@ -4,6 +4,7 @@ import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
import initFilePickers from '~/file_pickers';
import TransferDropdown from '~/groups/transfer_dropdown';
import groupsSelect from '~/groups_select';
+import { initCascadingSettingsLockPopovers } from '~/namespaces/cascading_settings';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import projectSelect from '~/project_select';
import initSearchSettings from '~/search_settings';
@@ -26,6 +27,7 @@ document.addEventListener('DOMContentLoaded', () => {
projectSelect();
initSearchSettings();
+ initCascadingSettingsLockPopovers();
return new TransferDropdown();
});
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index 6ff7b356438..63048777724 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -54,6 +54,7 @@ export default {
data() {
return {
hoveredJobName: '',
+ hoveredSourceJobName: '',
highlightedJobs: [],
measurements: {
width: 0,
@@ -93,6 +94,9 @@ export default {
shouldHideLinks() {
return this.isStageView;
},
+ shouldShowStageName() {
+ return !this.isStageView;
+ },
// The show downstream check prevents showing redundant linked columns
showDownstreamPipelines() {
return (
@@ -148,6 +152,9 @@ export default {
setJob(jobName) {
this.hoveredJobName = jobName;
},
+ setSourceJob(jobName) {
+ this.hoveredSourceJobName = jobName;
+ },
slidePipelineContainer() {
this.$refs.mainPipelineContainer.scrollBy({
left: ONE_COL_WIDTH,
@@ -204,11 +211,13 @@ export default {
<stage-column-component
v-for="column in layout"
:key="column.id || column.name"
- :title="column.name"
+ :name="column.name"
:groups="column.groups"
:action="column.status.action"
:highlighted-jobs="highlightedJobs"
+ :show-stage-name="shouldShowStageName"
:job-hovered="hoveredJobName"
+ :source-job-hovered="hoveredSourceJobName"
:pipeline-expanded="pipelineExpanded"
:pipeline-id="pipeline.id"
@refreshPipelineGraph="$emit('refreshPipelineGraph')"
@@ -227,7 +236,7 @@ export default {
:column-title="__('Downstream')"
:type="$options.pipelineTypeConstants.DOWNSTREAM"
:view-type="viewType"
- @downstreamHovered="setJob"
+ @downstreamHovered="setSourceJob"
@pipelineExpandToggle="togglePipelineExpanded"
@scrollContainer="slidePipelineContainer"
@error="onError"
diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
index 6e1d818fba0..78fee6a75a8 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
@@ -1,6 +1,4 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { reportToSentry } from '../../utils';
import JobItem from './job_item.vue';
@@ -11,12 +9,8 @@ import JobItem from './job_item.vue';
*
*/
export default {
- directives: {
- GlTooltip: GlTooltipDirective,
- },
components: {
JobItem,
- CiIcon,
},
props: {
group: {
@@ -28,6 +22,11 @@ export default {
required: false,
default: -1,
},
+ stageName: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
computedJobId() {
@@ -51,22 +50,21 @@ export default {
<template>
<div :id="computedJobId" class="ci-job-dropdown-container dropdown dropright">
<button
- v-gl-tooltip.hover="{ boundary: 'viewport' }"
- :title="tooltipText"
type="button"
data-toggle="dropdown"
data-display="static"
class="dropdown-menu-toggle build-content gl-build-content gl-pipeline-job-width! gl-pr-4!"
>
<div class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
- <span class="gl-display-flex gl-align-items-center gl-min-w-0">
- <ci-icon :status="group.status" :size="24" class="gl-line-height-0" />
- <span class="gl-text-truncate mw-70p gl-pl-3">
- {{ group.name }}
- </span>
- </span>
+ <job-item
+ :dropdown-length="group.size"
+ :group-tooltip="tooltipText"
+ :job="group"
+ :stage-name="stageName"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
+ />
- <span class="gl-font-weight-100 gl-font-size-lg"> {{ group.size }} </span>
+ <div class="gl-font-weight-100 gl-font-size-lg gl-ml-n4">{{ group.size }}</div>
</div>
</button>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index 7ff9ad8da2d..0f95037597f 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -3,6 +3,7 @@ import { GlTooltipDirective, GlLink } from '@gitlab/ui';
import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { sprintf } from '~/locale';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { reportToSentry } from '../../utils';
import ActionComponent from '../jobs_shared/action_component.vue';
import JobNameComponent from '../jobs_shared/job_name_component.vue';
@@ -38,6 +39,7 @@ export default {
hoverClass: 'gl-shadow-x0-y0-b3-s1-blue-500',
components: {
ActionComponent,
+ CiIcon,
JobNameComponent,
GlLink,
},
@@ -65,6 +67,11 @@ export default {
required: false,
default: Infinity,
},
+ groupTooltip: {
+ type: String,
+ required: false,
+ default: '',
+ },
jobHovered: {
type: String,
required: false,
@@ -80,24 +87,47 @@ export default {
required: false,
default: -1,
},
+ sourceJobHovered: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ stageName: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
boundary() {
return this.dropdownLength === 1 ? 'viewport' : 'scrollParent';
},
+ computedJobId() {
+ return this.pipelineId > -1 ? `${this.job.name}-${this.pipelineId}` : '';
+ },
detailsPath() {
return accessValue(this.dataMethod, 'detailsPath', this.status);
},
hasDetails() {
return accessValue(this.dataMethod, 'hasDetails', this.status);
},
- computedJobId() {
- return this.pipelineId > -1 ? `${this.job.name}-${this.pipelineId}` : '';
+ nameComponent() {
+ return this.hasDetails ? 'gl-link' : 'div';
+ },
+ showStageName() {
+ return Boolean(this.stageName);
},
status() {
return this.job && this.job.status ? this.job.status : {};
},
+ testId() {
+ return this.hasDetails ? 'job-with-link' : 'job-without-link';
+ },
tooltipText() {
+ if (this.groupTooltip) {
+ return this.groupTooltip;
+ }
+
const textBuilder = [];
const { name: jobName } = this.job;
@@ -129,7 +159,7 @@ export default {
return this.job.status && this.job.status.action && this.job.status.action.path;
},
relatedDownstreamHovered() {
- return this.job.name === this.jobHovered;
+ return this.job.name === this.sourceJobHovered;
},
relatedDownstreamExpanded() {
return this.job.name === this.pipelineExpanded.jobName && this.pipelineExpanded.expanded;
@@ -156,44 +186,45 @@ export default {
<template>
<div
:id="computedJobId"
- class="ci-job-component gl-display-flex gl-align-items-center gl-justify-content-space-between"
+ class="ci-job-component gl-display-flex gl-align-items-center gl-justify-content-space-between gl-w-full"
data-qa-selector="job_item_container"
>
- <gl-link
- v-if="hasDetails"
+ <component
+ :is="nameComponent"
v-gl-tooltip="{
boundary: 'viewport',
placement: 'bottom',
customClass: 'gl-pointer-events-none',
}"
- :href="detailsPath"
:title="tooltipText"
:class="jobClasses"
- class="js-pipeline-graph-job-link qa-job-link menu-item gl-text-gray-900 gl-active-text-decoration-none gl-focus-text-decoration-none gl-hover-text-decoration-none"
- data-testid="job-with-link"
+ :href="detailsPath"
+ class="js-pipeline-graph-job-link qa-job-link menu-item gl-text-gray-900 gl-active-text-decoration-none gl-focus-text-decoration-none gl-hover-text-decoration-none gl-w-full"
+ :data-testid="testId"
@click.stop="hideTooltips"
@mouseout="hideTooltips"
>
- <job-name-component :name="job.name" :status="job.status" :icon-size="24" />
- </gl-link>
-
- <div
- v-else
- v-gl-tooltip="{ boundary, placement: 'bottom', customClass: 'gl-pointer-events-none' }"
- :title="tooltipText"
- :class="jobClasses"
- class="js-job-component-tooltip non-details-job-component menu-item"
- data-testid="job-without-link"
- @mouseout="hideTooltips"
- >
- <job-name-component :name="job.name" :status="job.status" :icon-size="24" />
- </div>
+ <div class="ci-job-name-component gl-display-flex gl-align-items-center">
+ <ci-icon :size="24" :status="job.status" class="gl-line-height-0" />
+ <div class="gl-pl-3 gl-display-flex gl-flex-direction-column gl-w-full">
+ <div class="gl-text-truncate mw-70p gl-line-height-normal">{{ job.name }}</div>
+ <div
+ v-if="showStageName"
+ data-testid="stage-name-in-job"
+ class="gl-text-truncate mw-70p gl-font-sm gl-text-gray-500 gl-line-height-normal"
+ >
+ {{ stageName }}
+ </div>
+ </div>
+ </div>
+ </component>
<action-component
v-if="hasAction"
:tooltip-text="status.action.title"
:link="status.action.path"
:action-icon="status.action.icon"
+ class="gl-mr-1"
data-qa-selector="action_button"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
index 5cc9eec9440..fa2f381c8a4 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -22,12 +22,12 @@ export default {
type: Array,
required: true,
},
- pipelineId: {
- type: Number,
+ name: {
+ type: String,
required: true,
},
- title: {
- type: String,
+ pipelineId: {
+ type: Number,
required: true,
},
action: {
@@ -50,6 +50,16 @@ export default {
required: false,
default: () => ({}),
},
+ showStageName: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ sourceJobHovered: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
titleClasses: [
'gl-font-weight-bold',
@@ -75,7 +85,7 @@ export default {
});
},
formattedTitle() {
- return capitalize(escape(this.title));
+ return capitalize(escape(this.name));
},
hasAction() {
return !isEmpty(this.action);
@@ -145,14 +155,20 @@ export default {
v-if="singleJobExists(group)"
:job="group.jobs[0]"
:job-hovered="jobHovered"
+ :source-job-hovered="sourceJobHovered"
:pipeline-expanded="pipelineExpanded"
:pipeline-id="pipelineId"
+ :stage-name="showStageName ? group.stageName : ''"
css-class-job-name="gl-build-content"
:class="{ 'gl-opacity-3': isFadedOut(group.name) }"
@pipelineActionRequestComplete="$emit('refreshPipelineGraph')"
/>
<div v-else-if="isParallel(group)" :class="{ 'gl-opacity-3': isFadedOut(group.name) }">
- <job-group-dropdown :group="group" :pipeline-id="pipelineId" />
+ <job-group-dropdown
+ :group="group"
+ :stage-name="showStageName ? group.stageName : ''"
+ :pipeline-id="pipelineId"
+ />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/unwrapping_utils.js b/app/assets/javascripts/pipelines/components/unwrapping_utils.js
index 15316fe3279..2d24beb8323 100644
--- a/app/assets/javascripts/pipelines/components/unwrapping_utils.js
+++ b/app/assets/javascripts/pipelines/components/unwrapping_utils.js
@@ -5,7 +5,19 @@ const unwrapGroups = (stages) => {
const {
groups: { nodes: groups },
} = stage;
- return { node: { ...stage, groups }, lookup: { stageIdx: idx } };
+
+ /*
+ Being peformance conscious here means we don't want to spread and copy the
+ group value just to add one parameter.
+ */
+ /* eslint-disable no-param-reassign */
+ const groupsWithStageName = groups.map((group) => {
+ group.stageName = stage.name;
+ return group;
+ });
+ /* eslint-enable no-param-reassign */
+
+ return { node: { ...stage, groups: groupsWithStageName }, lookup: { stageIdx: idx } };
});
};
diff --git a/app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue b/app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue
index d92ccbd5f67..d96d1035ed0 100644
--- a/app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue
+++ b/app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue
@@ -72,6 +72,7 @@ export default {
<gl-dropdown-item
v-if="canCherryPick"
data-testid="cherry-pick-link"
+ data-qa-selector="cherry_pick_button"
@click="showModal($options.openCherryPickModal)"
>
{{ s__('ChangeTypeAction|Cherry-pick') }}
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
index 4a8e1424fa8..5f11bd62fdc 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
@@ -3,8 +3,6 @@ import { GlTabs, GlTab } from '@gitlab/ui';
import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility';
import PipelineCharts from './pipeline_charts.vue';
-const charts = ['pipelines', 'deployments'];
-
export default {
components: {
GlTabs,
@@ -12,6 +10,8 @@ export default {
PipelineCharts,
DeploymentFrequencyCharts: () =>
import('ee_component/projects/pipelines/charts/components/deployment_frequency_charts.vue'),
+ LeadTimeCharts: () =>
+ import('ee_component/projects/pipelines/charts/components/lead_time_charts.vue'),
},
inject: {
shouldRenderDeploymentFrequencyCharts: {
@@ -24,20 +24,29 @@ export default {
selectedTab: 0,
};
},
+ computed: {
+ charts() {
+ if (this.shouldRenderDeploymentFrequencyCharts) {
+ return ['pipelines', 'deployments', 'lead-time'];
+ }
+
+ return ['pipelines', 'lead-time'];
+ },
+ },
created() {
this.selectTab();
window.addEventListener('popstate', this.selectTab);
},
methods: {
selectTab() {
- const [chart] = getParameterValues('chart') || charts;
- const tab = charts.indexOf(chart);
+ const [chart] = getParameterValues('chart') || this.charts;
+ const tab = this.charts.indexOf(chart);
this.selectedTab = tab >= 0 ? tab : 0;
},
onTabChange(index) {
if (index !== this.selectedTab) {
this.selectedTab = index;
- const path = mergeUrlParams({ chart: charts[index] }, window.location.pathname);
+ const path = mergeUrlParams({ chart: this.charts[index] }, window.location.pathname);
updateHistory({ url: path, title: window.title });
}
},
@@ -46,14 +55,16 @@ export default {
</script>
<template>
<div>
- <gl-tabs v-if="shouldRenderDeploymentFrequencyCharts" :value="selectedTab" @input="onTabChange">
+ <gl-tabs :value="selectedTab" @input="onTabChange">
<gl-tab :title="__('Pipelines')">
<pipeline-charts />
</gl-tab>
- <gl-tab :title="__('Deployments')">
+ <gl-tab v-if="shouldRenderDeploymentFrequencyCharts" :title="__('Deployments')">
<deployment-frequency-charts />
</gl-tab>
+ <gl-tab :title="__('Lead Time')">
+ <lead-time-charts />
+ </gl-tab>
</gl-tabs>
- <pipeline-charts v-else />
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
index 6388b817e46..41b5983ae0c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
@@ -44,7 +44,8 @@ export default {
:checked="value"
:disabled="isDisabled"
name="squash"
- class="qa-squash-checkbox js-squash-checkbox gl-mr-2 gl-display-flex gl-align-items-center"
+ class="js-squash-checkbox gl-mr-2 gl-display-flex gl-align-items-center"
+ data-qa-selector="squash_checkbox"
:title="tooltipTitle"
@change="(checked) => $emit('input', checked)"
>
diff --git a/app/assets/javascripts/whats_new/components/app.vue b/app/assets/javascripts/whats_new/components/app.vue
index f4ac4f81eac..4a387edbe3f 100644
--- a/app/assets/javascripts/whats_new/components/app.vue
+++ b/app/assets/javascripts/whats_new/components/app.vue
@@ -1,13 +1,5 @@
<script>
-import {
- GlDrawer,
- GlInfiniteScroll,
- GlResizeObserverDirective,
- GlTabs,
- GlTab,
- GlBadge,
- GlLoadingIcon,
-} from '@gitlab/ui';
+import { GlDrawer, GlInfiniteScroll, GlResizeObserverDirective } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import Tracking from '~/tracking';
import { getDrawerBodyHeight } from '../utils/get_drawer_body_height';
@@ -20,37 +12,24 @@ export default {
components: {
GlDrawer,
GlInfiniteScroll,
- GlTabs,
- GlTab,
SkeletonLoader,
Feature,
- GlBadge,
- GlLoadingIcon,
},
directives: {
GlResizeObserver: GlResizeObserverDirective,
},
mixins: [trackingMixin],
props: {
- storageKey: {
+ versionDigest: {
type: String,
required: true,
},
- versions: {
- type: Array,
- required: true,
- },
- gitlabDotCom: {
- type: Boolean,
- required: false,
- default: false,
- },
},
computed: {
...mapState(['open', 'features', 'pageInfo', 'drawerBodyHeight', 'fetching']),
},
mounted() {
- this.openDrawer(this.storageKey);
+ this.openDrawer(this.versionDigest);
this.fetchItems();
const body = document.querySelector('body');
@@ -70,16 +49,6 @@ export default {
const height = getDrawerBodyHeight(this.$refs.drawer.$el);
this.setDrawerBodyHeight(height);
},
- featuresForVersion(version) {
- return this.features.filter((feature) => {
- return feature.release === parseFloat(version);
- });
- },
- fetchVersion(version) {
- if (this.featuresForVersion(version).length === 0) {
- this.fetchItems({ version });
- }
- },
},
};
</script>
@@ -99,7 +68,6 @@ export default {
</template>
<template v-if="features.length">
<gl-infinite-scroll
- v-if="gitlabDotCom"
:fetched-items="features.length"
:max-list-height="drawerBodyHeight"
class="gl-p-0"
@@ -109,26 +77,6 @@ export default {
<feature v-for="feature in features" :key="feature.title" :feature="feature" />
</template>
</gl-infinite-scroll>
- <gl-tabs v-else :style="{ height: `${drawerBodyHeight}px` }" class="gl-p-0">
- <gl-tab
- v-for="(version, index) in versions"
- :key="version"
- @click="fetchVersion(version)"
- >
- <template #title>
- <span>{{ version }}</span>
- <gl-badge v-if="index === 0">{{ __('Your Version') }}</gl-badge>
- </template>
- <gl-loading-icon v-if="fetching" size="lg" class="text-center" />
- <template v-else>
- <feature
- v-for="feature in featuresForVersion(version)"
- :key="feature.title"
- :feature="feature"
- />
- </template>
- </gl-tab>
- </gl-tabs>
</template>
<div v-else class="gl-mt-5">
<skeleton-loader />
diff --git a/app/assets/javascripts/whats_new/index.js b/app/assets/javascripts/whats_new/index.js
index 6da141cb19a..3ac3a3a3611 100644
--- a/app/assets/javascripts/whats_new/index.js
+++ b/app/assets/javascripts/whats_new/index.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import { mapState } from 'vuex';
import App from './components/app.vue';
import store from './store';
-import { getStorageKey, setNotification } from './utils/notification';
+import { getVersionDigest, setNotification } from './utils/notification';
let whatsNewApp;
@@ -27,9 +27,7 @@ export default (el) => {
render(createElement) {
return createElement('app', {
props: {
- storageKey: getStorageKey(el),
- versions: JSON.parse(el.getAttribute('data-versions')),
- gitlabDotCom: el.getAttribute('data-gitlab-dot-com'),
+ versionDigest: getVersionDigest(el),
},
});
},
diff --git a/app/assets/javascripts/whats_new/store/actions.js b/app/assets/javascripts/whats_new/store/actions.js
index 4b3cfa55977..1dc92ea2606 100644
--- a/app/assets/javascripts/whats_new/store/actions.js
+++ b/app/assets/javascripts/whats_new/store/actions.js
@@ -1,19 +1,20 @@
import axios from '~/lib/utils/axios_utils';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
+import { STORAGE_KEY } from '../utils/notification';
import * as types from './mutation_types';
export default {
closeDrawer({ commit }) {
commit(types.CLOSE_DRAWER);
},
- openDrawer({ commit }, storageKey) {
+ openDrawer({ commit }, versionDigest) {
commit(types.OPEN_DRAWER);
- if (storageKey) {
- localStorage.setItem(storageKey, JSON.stringify(false));
+ if (versionDigest) {
+ localStorage.setItem(STORAGE_KEY, versionDigest);
}
},
- fetchItems({ commit, state }, { page, version } = { page: null, version: null }) {
+ fetchItems({ commit, state }, { page } = { page: null }) {
if (state.fetching) {
return false;
}
@@ -24,7 +25,6 @@ export default {
.get('/-/whats_new', {
params: {
page,
- version,
},
})
.then(({ data, headers }) => {
diff --git a/app/assets/javascripts/whats_new/utils/notification.js b/app/assets/javascripts/whats_new/utils/notification.js
index 52ca8058d1c..3d4326c4b3a 100644
--- a/app/assets/javascripts/whats_new/utils/notification.js
+++ b/app/assets/javascripts/whats_new/utils/notification.js
@@ -1,11 +1,18 @@
-export const getStorageKey = (appEl) => appEl.getAttribute('data-storage-key');
+export const STORAGE_KEY = 'display-whats-new-notification';
+
+export const getVersionDigest = (appEl) => appEl.getAttribute('data-version-digest');
export const setNotification = (appEl) => {
- const storageKey = getStorageKey(appEl);
+ const versionDigest = getVersionDigest(appEl);
const notificationEl = document.querySelector('.header-help');
let notificationCountEl = notificationEl.querySelector('.js-whats-new-notification-count');
- if (JSON.parse(localStorage.getItem(storageKey)) === false) {
+ const legacyStorageKey = 'display-whats-new-notification-13.10';
+ const localStoragePairs = [
+ [legacyStorageKey, false],
+ [STORAGE_KEY, versionDigest],
+ ];
+ if (localStoragePairs.some((pair) => localStorage.getItem(pair[0]) === pair[1].toString())) {
notificationEl.classList.remove('with-notifications');
if (notificationCountEl) {
notificationCountEl.parentElement.removeChild(notificationCountEl);
diff --git a/app/assets/stylesheets/page_bundles/pipeline.scss b/app/assets/stylesheets/page_bundles/pipeline.scss
index a005daef2e4..2f3cf889549 100644
--- a/app/assets/stylesheets/page_bundles/pipeline.scss
+++ b/app/assets/stylesheets/page_bundles/pipeline.scss
@@ -148,7 +148,19 @@
}
.gl-build-content {
- @include build-content();
+ display: inline-block;
+ padding: 8px 10px 9px;
+ width: 100%;
+ border: 1px solid var(--border-color, $border-color);
+ border-radius: 30px;
+ background-color: var(--white, $white);
+
+ &:hover,
+ &:focus {
+ background-color: var(--gray-50, $gray-50);
+ border: 1px solid $dropdown-toggle-active-border-color;
+ color: var(--gl-text-color, $gl-text-color);
+ }
}
.gl-ci-action-icon-container {
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index e8e681ce649..7bfcda67aa2 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -5,19 +5,23 @@ module CreatesCommit
include Gitlab::Utils::StrongMemoize
# rubocop:disable Gitlab/ModuleWithInstanceVariables
- def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil)
- if user_access(@project).can_push_to_branch?(branch_name_or_ref)
- @project_to_commit_into = @project
+ def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil, target_project: nil)
+ target_project ||= @project
+
+ if user_access(target_project).can_push_to_branch?(branch_name_or_ref)
+ @project_to_commit_into = target_project
@branch_name ||= @ref
else
- @project_to_commit_into = current_user.fork_of(@project)
+ @project_to_commit_into = current_user.fork_of(target_project)
@branch_name ||= @project_to_commit_into.repository.next_branch('patch')
end
@start_branch ||= @ref || @branch_name
+ start_project = Feature.enabled?(:pick_into_project, @project, default_enabled: :yaml) ? @project_to_commit_into : @project
+
commit_params = @commit_params.merge(
- start_project: @project,
+ start_project: start_project,
start_branch: @start_branch,
branch_name: @branch_name
)
@@ -27,7 +31,7 @@ module CreatesCommit
if result[:status] == :success
update_flash_notice(success_notice)
- success_path = final_success_path(success_path)
+ success_path = final_success_path(success_path, target_project)
respond_to do |format|
format.html { redirect_to success_path }
@@ -79,9 +83,9 @@ module CreatesCommit
end
end
- def final_success_path(success_path)
+ def final_success_path(success_path, target_project)
if create_merge_request?
- merge_request_exists? ? existing_merge_request_path : new_merge_request_path
+ merge_request_exists? ? existing_merge_request_path : new_merge_request_path(target_project)
else
success_path = success_path.call if success_path.respond_to?(:call)
@@ -90,12 +94,12 @@ module CreatesCommit
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
- def new_merge_request_path
+ def new_merge_request_path(target_project)
project_new_merge_request_path(
@project_to_commit_into,
merge_request: {
source_project_id: @project_to_commit_into.id,
- target_project_id: @project.id,
+ target_project_id: target_project.id,
source_branch: @branch_name,
target_branch: @start_branch
}
diff --git a/app/controllers/concerns/renders_commits.rb b/app/controllers/concerns/renders_commits.rb
index 826fae834fa..4ea07c814ef 100644
--- a/app/controllers/concerns/renders_commits.rb
+++ b/app/controllers/concerns/renders_commits.rb
@@ -17,12 +17,13 @@ module RendersCommits
def set_commits_for_rendering(commits, commits_count: nil)
@total_commit_count = commits_count || commits.size
limited, @hidden_commit_count = limited_commits(commits, @total_commit_count)
- commits.each(&:lazy_author) # preload authors
prepare_commits_for_rendering(limited)
end
# rubocop: enable Gitlab/ModuleWithInstanceVariables
def prepare_commits_for_rendering(commits)
+ commits.each(&:lazy_author) # preload commits' authors
+
Banzai::CommitRenderer.render(commits, @project, current_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables
commits
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index fdf66340cbb..1e65974a3cd 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -114,7 +114,7 @@ class Projects::CommitController < Projects::ApplicationController
@branch_name = create_new_branch? ? @commit.revert_branch_name : @start_branch
create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully reverted.",
- success_path: -> { successful_change_path }, failure_path: failed_change_path)
+ success_path: -> { successful_change_path(@project) }, failure_path: failed_change_path)
end
def cherry_pick
@@ -122,10 +122,15 @@ class Projects::CommitController < Projects::ApplicationController
return render_404 if @start_branch.blank?
+ target_project = find_cherry_pick_target_project
+ return render_404 unless target_project
+
@branch_name = create_new_branch? ? @commit.cherry_pick_branch_name : @start_branch
create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully cherry-picked into #{@branch_name}.",
- success_path: -> { successful_change_path }, failure_path: failed_change_path)
+ success_path: -> { successful_change_path(target_project) },
+ failure_path: failed_change_path,
+ target_project: target_project)
end
private
@@ -138,8 +143,8 @@ class Projects::CommitController < Projects::ApplicationController
params[:create_merge_request].present? || !can?(current_user, :push_code, @project)
end
- def successful_change_path
- referenced_merge_request_url || project_commits_url(@project, @branch_name)
+ def successful_change_path(target_project)
+ referenced_merge_request_url || project_commits_url(target_project, @branch_name)
end
def failed_change_path
@@ -218,4 +223,14 @@ class Projects::CommitController < Projects::ApplicationController
@start_branch = params[:start_branch]
@commit_params = { commit: @commit }
end
+
+ def find_cherry_pick_target_project
+ return @project if params[:target_project_id].blank?
+ return @project unless Feature.enabled?(:pick_into_project, @project, default_enabled: :yaml)
+
+ MergeRequestTargetProjectFinder
+ .new(current_user: current_user, source_project: @project, project_feature: :repository)
+ .execute
+ .find_by_id(params[:target_project_id])
+ end
end
diff --git a/app/controllers/whats_new_controller.rb b/app/controllers/whats_new_controller.rb
index 12a52f30bd0..e24b0bbc7bb 100644
--- a/app/controllers/whats_new_controller.rb
+++ b/app/controllers/whats_new_controller.rb
@@ -5,7 +5,7 @@ class WhatsNewController < ApplicationController
skip_before_action :authenticate_user!
- before_action :check_valid_page_param, :set_pagination_headers, unless: -> { has_version_param? }
+ before_action :check_valid_page_param, :set_pagination_headers
feature_category :navigation
@@ -29,19 +29,11 @@ class WhatsNewController < ApplicationController
def highlights
strong_memoize(:highlights) do
- if has_version_param?
- ReleaseHighlight.for_version(version: params[:version])
- else
- ReleaseHighlight.paginated(page: current_page)
- end
+ ReleaseHighlight.paginated(page: current_page)
end
end
def set_pagination_headers
response.set_header('X-Next-Page', highlights.next_page)
end
-
- def has_version_param?
- params[:version].present?
- end
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 652ba9950bc..e7a81eb5629 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -139,7 +139,7 @@ module CommitsHelper
def cherry_pick_projects_data(project)
return [] unless Feature.enabled?(:pick_into_project, project, default_enabled: :yaml)
- target_projects(project).map do |project|
+ [project, project.forked_from_project].compact.map do |project|
{
id: project.id.to_s,
name: project.full_path,
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index 8cf5cd49322..a4521541bf9 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -56,6 +56,33 @@ module NamespacesHelper
namespaces_options(selected, **options)
end
+ def cascading_namespace_settings_enabled?
+ NamespaceSetting.cascading_settings_feature_enabled?
+ end
+
+ def cascading_namespace_settings_popover_data(attribute, group, settings_path_helper)
+ locked_by_ancestor = group.namespace_settings.public_send("#{attribute}_locked_by_ancestor?") # rubocop:disable GitlabSecurity/PublicSend
+
+ popover_data = {
+ locked_by_application_setting: group.namespace_settings.public_send("#{attribute}_locked_by_application_setting?"), # rubocop:disable GitlabSecurity/PublicSend
+ locked_by_ancestor: locked_by_ancestor
+ }
+
+ if locked_by_ancestor
+ ancestor_namespace = group.namespace_settings.public_send("#{attribute}_locked_ancestor").namespace # rubocop:disable GitlabSecurity/PublicSend
+
+ popover_data[:ancestor_namespace] = {
+ full_name: ancestor_namespace.full_name,
+ path: settings_path_helper.call(ancestor_namespace)
+ }
+ end
+
+ {
+ popover_data: popover_data.to_json,
+ testid: 'cascading-settings-lock-icon'
+ }
+ end
+
private
# Many importers create a temporary Group, so use the real
diff --git a/app/helpers/whats_new_helper.rb b/app/helpers/whats_new_helper.rb
index bbf5bde5904..23ed2fc987c 100644
--- a/app/helpers/whats_new_helper.rb
+++ b/app/helpers/whats_new_helper.rb
@@ -5,15 +5,7 @@ module WhatsNewHelper
ReleaseHighlight.most_recent_item_count
end
- def whats_new_storage_key
- most_recent_version = ReleaseHighlight.versions&.first
-
- return unless most_recent_version
-
- ['display-whats-new-notification', most_recent_version].join('-')
- end
-
- def whats_new_versions
- ReleaseHighlight.versions
+ def whats_new_version_digest
+ ReleaseHighlight.most_recent_version_digest
end
end
diff --git a/app/models/release_highlight.rb b/app/models/release_highlight.rb
index 1efba6380e9..98d9899a349 100644
--- a/app/models/release_highlight.rb
+++ b/app/models/release_highlight.rb
@@ -3,17 +3,6 @@
class ReleaseHighlight
CACHE_DURATION = 1.hour
FILES_PATH = Rails.root.join('data', 'whats_new', '*.yml')
- RELEASE_VERSIONS_IN_A_YEAR = 12
-
- def self.for_version(version:)
- index = self.versions.index(version)
-
- return if index.nil?
-
- page = index + 1
-
- self.paginated(page: page)
- end
def self.paginated(page: 1)
key = self.cache_key("items:page-#{page}")
@@ -82,15 +71,15 @@ class ReleaseHighlight
end
end
- def self.versions
- key = self.cache_key('versions')
+ def self.most_recent_version_digest
+ key = self.cache_key('most_recent_version_digest')
Gitlab::ProcessMemoryCache.cache_backend.fetch(key, expires_in: CACHE_DURATION) do
- versions = self.file_paths.first(RELEASE_VERSIONS_IN_A_YEAR).map do |path|
- /\d*\_(\d*\_\d*)\.yml$/.match(path).captures[0].gsub(/0(?=\d)/, "").tr("_", ".")
- end
+ version = self.paginated&.items&.first&.[]('release')&.to_s
+
+ next if version.nil?
- versions.uniq
+ Digest::SHA256.hexdigest(version)
end
end
diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml
index a14d342bc14..9dce33bf037 100644
--- a/app/views/admin/broadcast_messages/index.html.haml
+++ b/app/views/admin/broadcast_messages/index.html.haml
@@ -2,10 +2,9 @@
- page_title _("Broadcast Messages")
%h3.page-title
- Broadcast Messages
+ = _('Broadcast Messages')
%p.light
- Broadcast messages are displayed for every user and can be used to notify
- users about scheduled maintenance, recent upgrades and more.
+ = _('Broadcast messages are displayed for every user and can be used to notify users about scheduled maintenance, recent upgrades and more.')
= render 'form'
@@ -15,12 +14,12 @@
%table.table.table-responsive
%thead
%tr
- %th Status
- %th Preview
- %th Starts
- %th Ends
- %th Target Path
- %th Type
+ %th= _('Status')
+ %th= _('Preview')
+ %th= _('Starts')
+ %th= _('Ends')
+ %th= _(' Target Path')
+ %th= _(' Type')
%th &nbsp;
%tbody
- @broadcast_messages.each do |message|
@@ -38,7 +37,7 @@
%td
= message.broadcast_type.capitalize
%td.gl-white-space-nowrap.gl-display-flex
- = link_to sprite_icon('pencil-square', css_class: 'gl-icon'), edit_admin_broadcast_message_path(message), title: 'Edit', class: 'btn btn-icon gl-button'
- = link_to sprite_icon('remove', css_class: 'gl-icon'), admin_broadcast_message_path(message), method: :delete, remote: true, title: 'Remove', class: 'js-remove-tr btn btn-icon gl-button btn-danger ml-2'
+ = link_to sprite_icon('pencil-square', css_class: 'gl-icon'), edit_admin_broadcast_message_path(message), title: _('Edit'), class: 'btn btn-icon gl-button'
+ = link_to sprite_icon('remove', css_class: 'gl-icon'), admin_broadcast_message_path(message), method: :delete, remote: true, title: _('Remove'), class: 'js-remove-tr btn btn-icon gl-button btn-danger ml-2'
= paginate @broadcast_messages, theme: 'gitlab'
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index 57c0801074b..90a49e4bbe3 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -18,11 +18,11 @@
= nav_link(page: [dashboard_projects_path, root_path]) do
= link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do
= _("Your projects")
- %span.badge.badge-pill= limited_counter_with_delimiter(@total_user_projects_count)
+ %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(@total_user_projects_count)
= nav_link(page: starred_dashboard_projects_path) do
= link_to starred_dashboard_projects_path, data: {placement: 'right'} do
= _("Starred projects")
- %span.badge.badge-pill= limited_counter_with_delimiter(@total_starred_projects_count)
+ %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(@total_starred_projects_count)
= nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do
= link_to explore_root_path, data: {placement: 'right'} do
= _("Explore projects")
diff --git a/app/views/devise/mailer/_confirmation_instructions_account.html.haml b/app/views/devise/mailer/_confirmation_instructions_account.html.haml
index 27ef586d90f..9d469ff6e7b 100644
--- a/app/views/devise/mailer/_confirmation_instructions_account.html.haml
+++ b/app/views/devise/mailer/_confirmation_instructions_account.html.haml
@@ -2,15 +2,15 @@
- if @resource.unconfirmed_email.present? || !@resource.created_recently?
#content
= email_default_heading(@resource.unconfirmed_email || @resource.email)
- %p Click the link below to confirm your email address.
+ %p= _('Click the link below to confirm your email address.')
#cta
- = link_to 'Confirm your email address', confirmation_link
+ = link_to _('Confirm your email address'), confirmation_link
- else
#content
- if Gitlab.com?
- = email_default_heading('Thanks for signing up to GitLab!')
+ = email_default_heading(_('Thanks for signing up to GitLab!'))
- else
- = email_default_heading("Welcome, #{@resource.name}!")
- %p To get started, click the link below to confirm your account.
+ = email_default_heading(_("Welcome, %{name}!") % { name: @resource.name })
+ %p= _("To get started, click the link below to confirm your account.")
#cta
- = link_to 'Confirm your account', confirmation_link
+ = link_to _('Confirm your account'), confirmation_link
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 71e61436410..ee9eed7e6f6 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -3,6 +3,7 @@
- @content_class = "limit-container-width" unless fluid_layout
- expanded = expanded_by_default?
+= render 'shared/namespaces/cascading_settings/lock_popovers'
%section.settings.gs-general.no-animate#js-general-settings{ class: ('expanded') }
.settings-header
diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml
index 3e29e42126e..fcfe70bd694 100644
--- a/app/views/groups/settings/_permissions.html.haml
+++ b/app/views/groups/settings/_permissions.html.haml
@@ -8,27 +8,27 @@
= render 'shared/allow_request_access', form: f
.form-group.gl-mb-3
- .form-check
- = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group), class: 'form-check-input'
- = f.label :share_with_group_lock, class: 'form-check-label' do
- %span.d-block
+ .gl-form-checkbox.custom-control.custom-checkbox
+ = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group), class: 'custom-control-input'
+ = f.label :share_with_group_lock, class: 'custom-control-label' do
+ %span
- group_link = link_to @group.name, group_path(@group)
= s_('GroupSettings|Prevent sharing a project within %{group} with other groups').html_safe % { group: group_link }
- %span.js-descr.text-muted= share_with_group_lock_help_text(@group)
+ %p.js-descr.help-text= share_with_group_lock_help_text(@group)
.form-group.gl-mb-3
- .form-check
- = f.check_box :emails_disabled, checked: @group.emails_disabled?, disabled: !can_disable_group_emails?(@group), class: 'form-check-input'
- = f.label :emails_disabled, class: 'form-check-label' do
- %span.d-block= s_('GroupSettings|Disable email notifications')
- %span.text-muted= s_('GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects.')
+ .gl-form-checkbox.custom-control.custom-checkbox
+ = f.check_box :emails_disabled, checked: @group.emails_disabled?, disabled: !can_disable_group_emails?(@group), class: 'custom-control-input'
+ = f.label :emails_disabled, class: 'custom-control-label' do
+ %span= s_('GroupSettings|Disable email notifications')
+ %p.help-text= s_('GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects.')
.form-group.gl-mb-3
- .form-check
- = f.check_box :mentions_disabled, checked: @group.mentions_disabled?, class: 'form-check-input'
- = f.label :mentions_disabled, class: 'form-check-label' do
- %span.d-block= s_('GroupSettings|Disable group mentions')
- %span.text-muted= s_('GroupSettings|This setting will prevent group members from being notified if the group is mentioned.')
+ .gl-form-checkbox.custom-control.custom-checkbox
+ = f.check_box :mentions_disabled, checked: @group.mentions_disabled?, class: 'custom-control-input'
+ = f.label :mentions_disabled, class: 'custom-control-label' do
+ %span= s_('GroupSettings|Disable group mentions')
+ %p.help-text= s_('GroupSettings|This setting will prevent group members from being notified if the group is mentioned.')
= render 'groups/settings/project_access_token_creation', f: f, group: @group
= render_if_exists 'groups/settings/delayed_project_removal', f: f, group: @group
diff --git a/app/views/groups/settings/_project_access_token_creation.html.haml b/app/views/groups/settings/_project_access_token_creation.html.haml
index ac0ebfbd7f5..8be17c6cc30 100644
--- a/app/views/groups/settings/_project_access_token_creation.html.haml
+++ b/app/views/groups/settings/_project_access_token_creation.html.haml
@@ -1,10 +1,10 @@
- return unless render_setting_to_allow_project_access_token_creation?(group)
.form-group.gl-mb-3
- .form-check
- = f.check_box :resource_access_token_creation_allowed, checked: group.namespace_settings.resource_access_token_creation_allowed?, class: 'form-check-input', data: { qa_selector: 'resource_access_token_creation_allowed_checkbox' }
- = f.label :resource_access_token_creation_allowed, class: 'form-check-label' do
- %span.gl-display-block= s_('GroupSettings|Allow project access token creation')
+ .gl-form-checkbox.custom-control.custom-checkbox
+ = f.check_box :resource_access_token_creation_allowed, checked: group.namespace_settings.resource_access_token_creation_allowed?, class: 'custom-control-input', data: { qa_selector: 'resource_access_token_creation_allowed_checkbox' }
+ = f.label :resource_access_token_creation_allowed, class: 'custom-control-label' do
+ %span= s_('GroupSettings|Allow project access token creation')
- project_access_tokens_link = help_page_path('user/project/settings/project_access_tokens')
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: project_access_tokens_link }
- %span.text-muted= s_('GroupSettings|Users can create %{link_start}project access tokens%{link_end} for projects in this group.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
+ %p.help-text= s_('GroupSettings|Users can create %{link_start}project access tokens%{link_end} for projects in this group.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 7203919bc85..3225dad5d57 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -120,7 +120,7 @@
= sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon js-navbar-toggle-right')
= sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
-#whats-new-app{ data: { storage_key: whats_new_storage_key, versions: whats_new_versions, gitlab_dot_com: Gitlab.dev_env_org_or_com? } }
+#whats-new-app{ data: { version_digest: whats_new_version_digest } }
- if can?(current_user, :update_user_status, current_user)
.js-set-status-modal-wrapper{ data: user_status_data }
diff --git a/app/views/layouts/header/_whats_new_dropdown_item.html.haml b/app/views/layouts/header/_whats_new_dropdown_item.html.haml
index f79b741ced0..61fe2f1e711 100644
--- a/app/views/layouts/header/_whats_new_dropdown_item.html.haml
+++ b/app/views/layouts/header/_whats_new_dropdown_item.html.haml
@@ -1,5 +1,5 @@
%li
- %button.gl-justify-content-space-between.gl-align-items-center.js-whats-new-trigger{ type: 'button', data: { storage_key: whats_new_storage_key }, class: 'gl-display-flex!' }
+ %button.gl-justify-content-space-between.gl-align-items-center.js-whats-new-trigger{ type: 'button', class: 'gl-display-flex!' }
= _("What's new")
%span.js-whats-new-notification-count.whats-new-notification-count
= whats_new_most_recent_release_items_count
diff --git a/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml b/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml
new file mode 100644
index 00000000000..1e9aa4ec5ff
--- /dev/null
+++ b/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml
@@ -0,0 +1,14 @@
+- attribute = local_assigns.fetch(:attribute, nil)
+- group = local_assigns.fetch(:group, nil)
+- form = local_assigns.fetch(:form, nil)
+
+- return unless attribute && group && form && cascading_namespace_settings_enabled?
+- return if group.namespace_settings.public_send("#{attribute}_locked?")
+
+- lock_attribute = "lock_#{attribute}"
+
+.gl-form-checkbox.custom-control.custom-checkbox
+ = form.check_box lock_attribute, checked: group.namespace_settings.public_send(lock_attribute), class: 'custom-control-input', data: { testid: 'enforce-for-all-subgroups-checkbox' }
+ = form.label lock_attribute, class: 'custom-control-label' do
+ %span= s_('CascadingSettings|Enforce for all subgroups')
+ %p.help-text= s_('CascadingSettings|Subgroups cannot change this setting.')
diff --git a/app/views/shared/namespaces/cascading_settings/_lock_popovers.html.haml b/app/views/shared/namespaces/cascading_settings/_lock_popovers.html.haml
new file mode 100644
index 00000000000..91458bf180b
--- /dev/null
+++ b/app/views/shared/namespaces/cascading_settings/_lock_popovers.html.haml
@@ -0,0 +1 @@
+.js-cascading-settings-lock-popovers
diff --git a/app/views/shared/namespaces/cascading_settings/_setting_label.html.haml b/app/views/shared/namespaces/cascading_settings/_setting_label.html.haml
new file mode 100644
index 00000000000..6596ce2bc73
--- /dev/null
+++ b/app/views/shared/namespaces/cascading_settings/_setting_label.html.haml
@@ -0,0 +1,21 @@
+- attribute = local_assigns.fetch(:attribute, nil)
+- group = local_assigns.fetch(:group, nil)
+- form = local_assigns.fetch(:form, nil)
+- settings_path_helper = local_assigns.fetch(:settings_path_helper, nil)
+- help_text = local_assigns.fetch(:help_text, nil)
+
+- return unless attribute && group && form && settings_path_helper
+
+- setting_locked = group.namespace_settings.public_send("#{attribute}_locked?")
+
+= form.label attribute, class: 'custom-control-label', aria: { disabled: setting_locked } do
+ %span.position-relative.gl-pr-6.gl-display-inline-flex
+ = yield
+ - if setting_locked
+ %button.position-absolute.gl-top-3.gl-right-0.gl-translate-y-n50.gl-cursor-default.btn.btn-default.btn-sm.gl-button.btn-default-tertiary.js-cascading-settings-lock-popover-target{ class: 'gl-p-1! gl-text-gray-600! gl-bg-transparent!',
+ type: 'button',
+ data: cascading_namespace_settings_popover_data(attribute, group, settings_path_helper) }
+ = sprite_icon('lock', size: 16)
+ - if help_text
+ %p.help-text
+ = help_text