Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-05-04 15:10:04 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-05-04 15:10:04 +0300
commit998adcc422d4161515bf2960ef4dce71258f69a3 (patch)
tree34ffbf53b8587d3367457d4175fc8dfd71477dca /app
parente2c471038ab2b4f09487c1feb74520f74f834986 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/analytics/devops_report/components/devops_score.vue87
-rw-r--r--app/assets/javascripts/analytics/devops_report/devops_score.js20
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue11
-rw-r--r--app/assets/javascripts/issuable/components/status_box.vue103
-rw-r--r--app/assets/javascripts/merge_request/components/status_box.vue71
-rw-r--r--app/assets/javascripts/merge_request/eventhub.js3
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue4
-rw-r--r--app/assets/javascripts/notes/stores/getters.js5
-rw-r--r--app/assets/javascripts/pages/admin/dev_ops_report/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js12
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/queries/get_state.query.graphql7
-rw-r--r--app/assets/javascripts/projects/compare/components/app.vue57
-rw-r--r--app/assets/javascripts/projects/compare/components/repo_dropdown.vue52
-rw-r--r--app/assets/javascripts/projects/compare/components/revision_card.vue26
-rw-r--r--app/assets/javascripts/projects/compare/components/revision_dropdown.vue13
-rw-r--r--app/assets/javascripts/projects/compare/index.js6
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_editable_item.vue8
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue42
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue202
-rw-r--r--app/assets/javascripts/sidebar/constants.js21
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js23
-rw-r--r--app/assets/javascripts/sidebar/queries/epic_subscribed.query.graphql11
-rw-r--r--app/assets/javascripts/sidebar/queries/issue_subscribed.query.graphql11
-rw-r--r--app/assets/javascripts/sidebar/queries/merge_request_subscribed.query.graphql10
-rw-r--r--app/assets/javascripts/sidebar/queries/update_epic_subscription.mutation.graphql9
-rw-r--r--app/assets/javascripts/sidebar/queries/update_issue_subscription.mutation.graphql11
-rw-r--r--app/assets/javascripts/sidebar/queries/update_merge_request_subscription.mutation.graphql11
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js23
-rw-r--r--app/assets/stylesheets/page_bundles/dev_ops_report.scss255
-rw-r--r--app/controllers/application_controller.rb4
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb2
-rw-r--r--app/graphql/resolvers/ci/runner_resolver.rb36
-rw-r--r--app/graphql/types/ci/job_type.rb2
-rw-r--r--app/graphql/types/ci/runner_access_level_enum.rb15
-rw-r--r--app/graphql/types/ci/runner_status_enum.rb15
-rw-r--r--app/graphql/types/ci/runner_type.rb42
-rw-r--r--app/graphql/types/ci/runner_type_enum.rb15
-rw-r--r--app/graphql/types/permission_types/ci/job.rb14
-rw-r--r--app/graphql/types/query_type.rb7
-rw-r--r--app/helpers/auth_helper.rb12
-rw-r--r--app/helpers/dev_ops_report_helper.rb72
-rw-r--r--app/helpers/issuables_helper.rb12
-rw-r--r--app/helpers/merge_requests_helper.rb10
-rw-r--r--app/helpers/users_helper.rb2
-rw-r--r--app/models/note.rb1
-rw-r--r--app/models/project.rb6
-rw-r--r--app/presenters/project_presenter.rb18
-rw-r--r--app/services/ci/test_failure_history_service.rb2
-rw-r--r--app/services/merge_requests/create_from_issue_service.rb2
-rw-r--r--app/services/projects/create_service.rb2
-rw-r--r--app/services/repositories/changelog_service.rb2
-rw-r--r--app/views/admin/dev_ops_report/_card.html.haml25
-rw-r--r--app/views/admin/dev_ops_report/_report.html.haml22
-rw-r--r--app/views/doorkeeper/authorizations/new.html.haml4
-rw-r--r--app/views/layouts/header/_new_dropdown.html.haml19
-rw-r--r--app/views/layouts/header/_new_repo_experiment.html.haml11
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml6
-rw-r--r--app/views/projects/empty.html.haml2
-rw-r--r--app/views/projects/environments/index.html.haml2
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml6
-rw-r--r--app/views/shared/issuable/_status_box.html.haml6
61 files changed, 952 insertions, 560 deletions
diff --git a/app/assets/javascripts/analytics/devops_report/components/devops_score.vue b/app/assets/javascripts/analytics/devops_report/components/devops_score.vue
new file mode 100644
index 00000000000..7dc552623f6
--- /dev/null
+++ b/app/assets/javascripts/analytics/devops_report/components/devops_score.vue
@@ -0,0 +1,87 @@
+<script>
+import { GlBadge, GlTable } from '@gitlab/ui';
+import { GlSingleStat } from '@gitlab/ui/dist/charts';
+import { sprintf, s__ } from '~/locale';
+
+const defaultHeaderAttrs = {
+ thClass: 'gl-bg-white!',
+ thAttr: { 'data-testid': 'header' },
+};
+
+export default {
+ components: {
+ GlBadge,
+ GlTable,
+ GlSingleStat,
+ },
+ inject: {
+ devopsScoreMetrics: {
+ default: null,
+ },
+ },
+ computed: {
+ titleHelperText() {
+ return sprintf(
+ s__(
+ 'DevopsReport|DevOps score metrics are based on usage over the last 30 days. Last updated: %{timestamp}.',
+ ),
+ { timestamp: this.devopsScoreMetrics.createdAt },
+ );
+ },
+ },
+ tableHeaderFields: [
+ {
+ key: 'title',
+ label: '',
+ ...defaultHeaderAttrs,
+ },
+ {
+ key: 'usage',
+ label: s__('DevopsReport|Your usage'),
+ ...defaultHeaderAttrs,
+ },
+ {
+ key: 'leadInstance',
+ label: s__('DevopsReport|Leader usage'),
+ ...defaultHeaderAttrs,
+ },
+ {
+ key: 'score',
+ label: s__('DevopsReport|Score'),
+ ...defaultHeaderAttrs,
+ },
+ ],
+};
+</script>
+<template>
+ <div data-testid="devops-score-app">
+ <div class="gl-text-gray-400 gl-my-4" data-testid="devops-score-note-text">
+ {{ titleHelperText }}
+ </div>
+ <gl-single-stat
+ unit="%"
+ size="sm"
+ :title="s__('DevopsReport|Your score')"
+ :should-animate="true"
+ :value="devopsScoreMetrics.averageScore.value"
+ :meta-icon="devopsScoreMetrics.averageScore.scoreLevel.icon"
+ :meta-text="devopsScoreMetrics.averageScore.scoreLevel.label"
+ :variant="devopsScoreMetrics.averageScore.scoreLevel.variant"
+ />
+ <gl-table
+ :fields="$options.tableHeaderFields"
+ :items="devopsScoreMetrics.cards"
+ thead-class="gl-border-t-0 gl-border-b-solid gl-border-b-1 gl-border-b-gray-100"
+ stacked="sm"
+ >
+ <template #cell(usage)="{ item }">
+ <div data-testid="usageCol">
+ <span>{{ item.usage }}</span>
+ <gl-badge :variant="item.scoreLevel.variant" size="sm" class="gl-ml-1">{{
+ item.scoreLevel.label
+ }}</gl-badge>
+ </div>
+ </template>
+ </gl-table>
+ </div>
+</template>
diff --git a/app/assets/javascripts/analytics/devops_report/devops_score.js b/app/assets/javascripts/analytics/devops_report/devops_score.js
new file mode 100644
index 00000000000..46b27e0a340
--- /dev/null
+++ b/app/assets/javascripts/analytics/devops_report/devops_score.js
@@ -0,0 +1,20 @@
+import Vue from 'vue';
+import DevopsScore from './components/devops_score.vue';
+
+export default () => {
+ const el = document.getElementById('js-devops-score');
+
+ if (!el) return false;
+
+ const { devopsScoreMetrics } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: {
+ devopsScoreMetrics: JSON.parse(devopsScoreMetrics),
+ },
+ render(h) {
+ return h(DevopsScore);
+ },
+ });
+};
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
index 0050d64235f..5a41a01e167 100644
--- a/app/assets/javascripts/boards/components/board_content_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -4,13 +4,13 @@ import { mapState, mapActions, mapGetters } from 'vuex';
import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue';
-import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue';
import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants';
import { contentTop } from '~/lib/utils/common_utils';
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
+import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
@@ -23,7 +23,7 @@ export default {
BoardSidebarTimeTracker,
BoardSidebarLabelsSelect,
BoardSidebarDueDate,
- BoardSidebarSubscription,
+ SidebarSubscriptionsWidget,
BoardSidebarMilestoneSelect,
BoardSidebarEpicSelect: () =>
import('ee_component/boards/components/sidebar/board_sidebar_epic_select.vue'),
@@ -98,7 +98,12 @@ export default {
:issuable-type="issuableType"
@confidentialityUpdated="setActiveItemConfidential($event)"
/>
- <board-sidebar-subscription class="subscriptions" />
+ <sidebar-subscriptions-widget
+ :iid="activeBoardItem.iid"
+ :full-path="fullPath"
+ :issuable-type="issuableType"
+ data-testid="sidebar-notifications"
+ />
</template>
</gl-drawer>
</template>
diff --git a/app/assets/javascripts/issuable/components/status_box.vue b/app/assets/javascripts/issuable/components/status_box.vue
new file mode 100644
index 00000000000..cb768f2bc5b
--- /dev/null
+++ b/app/assets/javascripts/issuable/components/status_box.vue
@@ -0,0 +1,103 @@
+<script>
+import { GlIcon } from '@gitlab/ui';
+import Vue from 'vue';
+import { fetchPolicies } from '~/lib/graphql';
+import { __ } from '~/locale';
+
+export const statusBoxState = Vue.observable({
+ state: '',
+ updateStatus: null,
+});
+
+const CLASSES = {
+ opened: 'status-box-open',
+ locked: 'status-box-open',
+ closed: 'status-box-mr-closed',
+ merged: 'status-box-mr-merged',
+};
+
+const STATUS = {
+ opened: [__('Open'), 'issue-open-m'],
+ locked: [__('Open'), 'issue-open-m'],
+ closed: [__('Closed'), 'issue-close'],
+ merged: [__('Merged'), 'git-merge'],
+};
+
+export default {
+ components: {
+ GlIcon,
+ },
+ inject: {
+ query: { default: null },
+ projectPath: { default: null },
+ iid: { default: null },
+ },
+ props: {
+ initialState: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ if (this.initialState) {
+ statusBoxState.state = this.initialState;
+ }
+
+ return statusBoxState;
+ },
+ computed: {
+ statusBoxClass() {
+ return CLASSES[`${this.issuableType}_${this.state}`] || CLASSES[this.state];
+ },
+ statusHumanName() {
+ return (STATUS[`${this.issuableType}_${this.state}`] || STATUS[this.state])[0];
+ },
+ statusIconName() {
+ return (STATUS[`${this.issuableType}_${this.state}`] || STATUS[this.state])[1];
+ },
+ },
+ created() {
+ if (!statusBoxState.updateStatus) {
+ statusBoxState.updateStatus = this.fetchState;
+ }
+ },
+ beforeDestroy() {
+ if (statusBoxState.updateStatus && this.query) {
+ statusBoxState.updateStatus = null;
+ }
+ },
+ methods: {
+ async fetchState() {
+ const { data } = await this.$apollo.query({
+ query: this.query,
+ variables: {
+ projectPath: this.projectPath,
+ iid: this.iid,
+ },
+ fetchPolicy: fetchPolicies.NO_CACHE,
+ });
+
+ statusBoxState.state = data?.workspace?.issuable?.state;
+ },
+ },
+};
+</script>
+
+<template>
+ <div :class="statusBoxClass" class="issuable-status-box status-box">
+ <gl-icon
+ :name="statusIconName"
+ class="gl-display-block gl-sm-display-none!"
+ data-testid="status-icon"
+ />
+ <span class="gl-display-none gl-sm-display-block">
+ {{ statusHumanName }}
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/merge_request/components/status_box.vue b/app/assets/javascripts/merge_request/components/status_box.vue
deleted file mode 100644
index 526aafc1def..00000000000
--- a/app/assets/javascripts/merge_request/components/status_box.vue
+++ /dev/null
@@ -1,71 +0,0 @@
-<script>
-import { GlIcon } from '@gitlab/ui';
-import { __ } from '~/locale';
-import mrEventHub from '../eventhub';
-
-const CLASSES = {
- opened: 'status-box-open',
- locked: 'status-box-open',
- closed: 'status-box-mr-closed',
- merged: 'status-box-mr-merged',
-};
-
-const STATUS = {
- opened: [__('Open'), 'issue-open-m'],
- locked: [__('Open'), 'issue-open-m'],
- closed: [__('Closed'), 'issue-close'],
- merged: [__('Merged'), 'git-merge'],
-};
-
-export default {
- components: {
- GlIcon,
- },
- props: {
- initialState: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- state: this.initialState,
- };
- },
- computed: {
- statusBoxClass() {
- return CLASSES[this.state];
- },
- statusHumanName() {
- return STATUS[this.state][0];
- },
- statusIconName() {
- return STATUS[this.state][1];
- },
- },
- created() {
- mrEventHub.$on('mr.state.updated', this.updateState);
- },
- beforeDestroy() {
- mrEventHub.$off('mr.state.updated', this.updateState);
- },
- methods: {
- updateState({ state }) {
- this.state = state;
- },
- },
-};
-</script>
-
-<template>
- <div :class="statusBoxClass" class="issuable-status-box status-box">
- <gl-icon
- :name="statusIconName"
- class="gl-display-block gl-sm-display-none!"
- data-testid="status-icon"
- />
- <span class="gl-display-none gl-sm-display-block">
- {{ statusHumanName }}
- </span>
- </div>
-</template>
diff --git a/app/assets/javascripts/merge_request/eventhub.js b/app/assets/javascripts/merge_request/eventhub.js
deleted file mode 100644
index e31806ad199..00000000000
--- a/app/assets/javascripts/merge_request/eventhub.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import createEventHub from '~/helpers/event_hub_factory';
-
-export default createEventHub();
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 79d8ce78329..90be5b3e470 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -15,6 +15,7 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import Autosave from '~/autosave';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import { deprecatedCreateFlash as Flash } from '~/flash';
+import { statusBoxState } from '~/issuable/components/status_box.vue';
import httpStatusCodes from '~/lib/utils/http_status';
import {
capitalizeFirstCharacter,
@@ -162,7 +163,7 @@ export default {
canToggleIssueState() {
return (
this.getNoteableData.current_user.can_update &&
- this.getNoteableData.state !== constants.MERGED &&
+ this.openState !== constants.MERGED &&
!this.closedAndLocked
);
},
@@ -283,6 +284,7 @@ export default {
const toggleState = this.isOpen ? this.closeIssuable : this.reopenIssuable;
toggleState()
+ .then(() => statusBoxState.updateStatus && statusBoxState.updateStatus())
.then(refreshUserMergeRequestCounts)
.catch(() => Flash(constants.toggleStateErrorMessage[this.noteableType][this.openState]));
},
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 39f66063cfb..b04b1d28ffa 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -1,4 +1,6 @@
import { flattenDeep, clone } from 'lodash';
+import { statusBoxState } from '~/issuable/components/status_box.vue';
+import { isInMRPage } from '~/lib/utils/common_utils';
import * as constants from '../constants';
import { collapseSystemNotes } from './collapse_utils';
@@ -82,7 +84,8 @@ export const getBlockedByIssues = (state) => state.noteableData.blocked_by_issue
export const userCanReply = (state) => Boolean(state.noteableData.current_user.can_create_note);
-export const openState = (state) => state.noteableData.state;
+export const openState = (state) =>
+ isInMRPage() ? statusBoxState.state : state.noteableData.state;
export const getUserData = (state) => state.userData || {};
diff --git a/app/assets/javascripts/pages/admin/dev_ops_report/index.js b/app/assets/javascripts/pages/admin/dev_ops_report/index.js
index cf06ee2c22a..23b53315680 100644
--- a/app/assets/javascripts/pages/admin/dev_ops_report/index.js
+++ b/app/assets/javascripts/pages/admin/dev_ops_report/index.js
@@ -1,3 +1,5 @@
+import initDevOpsScore from '~/analytics/devops_report/devops_score';
import initDevOpsScoreEmptyState from '~/analytics/devops_report/devops_score_empty_state';
initDevOpsScoreEmptyState();
+initDevOpsScore();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index 021122d7637..6cd3202815b 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -1,14 +1,17 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import loadAwardsHandler from '~/awards_handler';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initIssuableSidebar from '~/init_issuable_sidebar';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
+import StatusBox from '~/issuable/components/status_box.vue';
+import createDefaultClient from '~/lib/graphql';
import { handleLocationHash } from '~/lib/utils/common_utils';
-import StatusBox from '~/merge_request/components/status_box.vue';
import initSourcegraph from '~/sourcegraph';
import ZenMode from '~/zen_mode';
+import getStateQuery from './queries/get_state.query.graphql';
export default function initMergeRequestShow() {
const awardEmojiEl = document.getElementById('js-vue-awards-block');
@@ -30,9 +33,16 @@ export default function initMergeRequestShow() {
initInviteMembersTrigger();
const el = document.querySelector('.js-mr-status-box');
+ const apolloProvider = new VueApollo({ defaultClient: createDefaultClient() });
// eslint-disable-next-line no-new
new Vue({
el,
+ apolloProvider,
+ provide: {
+ query: getStateQuery,
+ projectPath: el.dataset.projectPath,
+ iid: el.dataset.iid,
+ },
render(h) {
return h(StatusBox, {
props: {
diff --git a/app/assets/javascripts/pages/projects/merge_requests/queries/get_state.query.graphql b/app/assets/javascripts/pages/projects/merge_requests/queries/get_state.query.graphql
new file mode 100644
index 00000000000..b5a82b9428e
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/merge_requests/queries/get_state.query.graphql
@@ -0,0 +1,7 @@
+query getMergeRequestState($projectPath: ID!, $iid: String!) {
+ workspace: project(fullPath: $projectPath) {
+ issuable: mergeRequest(iid: $iid) {
+ state
+ }
+ }
+}
diff --git a/app/assets/javascripts/projects/compare/components/app.vue b/app/assets/javascripts/projects/compare/components/app.vue
index d2fb524489e..f7cfc82db11 100644
--- a/app/assets/javascripts/projects/compare/components/app.vue
+++ b/app/assets/javascripts/projects/compare/components/app.vue
@@ -1,6 +1,7 @@
<script>
import { GlButton } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
+import { joinPaths } from '~/lib/utils/url_utility';
import RevisionCard from './revision_card.vue';
export default {
@@ -36,11 +37,46 @@ export default {
type: String,
required: true,
},
+ defaultProject: {
+ type: Object,
+ required: true,
+ },
+ projects: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ from: {
+ projects: this.projects,
+ selectedProject: this.defaultProject,
+ revision: this.paramsFrom,
+ refsProjectPath: this.refsProjectPath,
+ },
+ to: {
+ selectedProject: this.defaultProject,
+ revision: this.paramsTo,
+ refsProjectPath: this.refsProjectPath,
+ },
+ };
},
methods: {
onSubmit() {
this.$refs.form.submit();
},
+ onSelectProject({ direction, project }) {
+ const refsPath = joinPaths(gon.relative_url_root || '', `/${project.name}`, '/refs');
+ // direction is either 'from' or 'to'
+ this[direction].refsProjectPath = refsPath;
+ this[direction].selectedProject = project;
+ },
+ onSelectRevision({ direction, revision }) {
+ this[direction].revision = revision; // direction is either 'from' or 'to'
+ },
+ onSwapRevision() {
+ [this.from, this.to] = [this.to, this.from]; // swaps 'from' and 'to'
+ },
},
};
</script>
@@ -57,10 +93,15 @@ export default {
class="gl-lg-flex-direction-row gl-lg-display-flex gl-align-items-center compare-revision-cards"
>
<revision-card
- :refs-project-path="refsProjectPath"
+ data-testid="sourceRevisionCard"
+ :refs-project-path="to.refsProjectPath"
revision-text="Source"
params-name="to"
- :params-branch="paramsTo"
+ :params-branch="to.revision"
+ :projects="to.projects"
+ :selected-project="to.selectedProject"
+ @selectProject="onSelectProject"
+ @selectRevision="onSelectRevision"
/>
<div
class="compare-ellipsis gl-display-flex gl-justify-content-center gl-align-items-center gl-my-4 gl-md-my-0"
@@ -69,16 +110,24 @@ export default {
...
</div>
<revision-card
- :refs-project-path="refsProjectPath"
+ data-testid="targetRevisionCard"
+ :refs-project-path="from.refsProjectPath"
revision-text="Target"
params-name="from"
- :params-branch="paramsFrom"
+ :params-branch="from.revision"
+ :projects="from.projects"
+ :selected-project="from.selectedProject"
+ @selectProject="onSelectProject"
+ @selectRevision="onSelectRevision"
/>
</div>
<div class="gl-mt-4">
<gl-button category="primary" variant="success" @click="onSubmit">
{{ s__('CompareRevisions|Compare') }}
</gl-button>
+ <gl-button data-testid="swapRevisionsButton" class="btn btn-default" @click="onSwapRevision">
+ {{ s__('CompareRevisions|Swap revisions') }}
+ </gl-button>
<gl-button
v-if="projectMergeRequestPath"
:href="projectMergeRequestPath"
diff --git a/app/assets/javascripts/projects/compare/components/repo_dropdown.vue b/app/assets/javascripts/projects/compare/components/repo_dropdown.vue
index cb9d8b64b33..ba1e00a2b36 100644
--- a/app/assets/javascripts/projects/compare/components/repo_dropdown.vue
+++ b/app/assets/javascripts/projects/compare/components/repo_dropdown.vue
@@ -1,57 +1,51 @@
<script>
import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
-const SOURCE_PARAM_NAME = 'to';
-
export default {
components: {
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
},
- inject: ['projectTo', 'projectsFrom'],
props: {
paramsName: {
type: String,
required: true,
},
+ projects: {
+ type: Array,
+ required: false,
+ default: null,
+ },
+ selectedProject: {
+ type: Object,
+ required: true,
+ },
},
data() {
return {
searchTerm: '',
- selectedRepo: {},
};
},
computed: {
+ disableRepoDropdown() {
+ return this.projects === null;
+ },
filteredRepos() {
const lowerCaseSearchTerm = this.searchTerm.toLowerCase();
- return this?.projectsFrom.filter(({ name }) =>
- name.toLowerCase().includes(lowerCaseSearchTerm),
- );
- },
- isSourceRevision() {
- return this.paramsName === SOURCE_PARAM_NAME;
+ return this?.projects.filter(({ name }) => name.toLowerCase().includes(lowerCaseSearchTerm));
},
inputName() {
return `${this.paramsName}_project_id`;
},
},
- mounted() {
- this.setDefaultRepo();
- },
methods: {
- onClick(repo) {
- this.selectedRepo = repo;
- this.emitTargetProject(repo.name);
- },
- setDefaultRepo() {
- this.selectedRepo = this.projectTo;
+ onClick(project) {
+ this.emitTargetProject(project);
},
- emitTargetProject(name) {
- if (!this.isSourceRevision) {
- this.$emit('changeTargetProject', name);
- }
+ emitTargetProject(project) {
+ this.$emit('selectProject', { direction: this.paramsName, project });
},
},
};
@@ -59,23 +53,23 @@ export default {
<template>
<div>
- <input type="hidden" :name="inputName" :value="selectedRepo.id" />
+ <input type="hidden" :name="inputName" :value="selectedProject.id" />
<gl-dropdown
- :text="selectedRepo.name"
+ :text="selectedProject.name"
:header-text="s__(`CompareRevisions|Select target project`)"
class="gl-w-full gl-font-monospace gl-sm-pr-3"
toggle-class="gl-min-w-0"
- :disabled="isSourceRevision"
+ :disabled="disableRepoDropdown"
>
<template #header>
- <gl-search-box-by-type v-if="!isSourceRevision" v-model.trim="searchTerm" />
+ <gl-search-box-by-type v-if="!disableRepoDropdown" v-model.trim="searchTerm" />
</template>
- <template v-if="!isSourceRevision">
+ <template v-if="!disableRepoDropdown">
<gl-dropdown-item
v-for="repo in filteredRepos"
:key="repo.id"
is-check-item
- :is-checked="selectedRepo.id === repo.id"
+ :is-checked="selectedProject.id === repo.id"
@click="onClick(repo)"
>
{{ repo.name }}
diff --git a/app/assets/javascripts/projects/compare/components/revision_card.vue b/app/assets/javascripts/projects/compare/components/revision_card.vue
index 15d24792310..02a329221cc 100644
--- a/app/assets/javascripts/projects/compare/components/revision_card.vue
+++ b/app/assets/javascripts/projects/compare/components/revision_card.vue
@@ -27,17 +27,14 @@ export default {
required: false,
default: null,
},
- },
- data() {
- return {
- selectedRefsProjectPath: this.refsProjectPath,
- };
- },
- methods: {
- onChangeTargetProject(targetProjectName) {
- if (this.paramsName === 'from') {
- this.selectedRefsProjectPath = `/${targetProjectName}/refs`;
- }
+ projects: {
+ type: Array,
+ required: false,
+ default: null,
+ },
+ selectedProject: {
+ type: Object,
+ required: true,
},
},
};
@@ -52,13 +49,16 @@ export default {
<repo-dropdown
class="gl-sm-w-half"
:params-name="paramsName"
- @changeTargetProject="onChangeTargetProject"
+ :projects="projects"
+ :selected-project="selectedProject"
+ v-on="$listeners"
/>
<revision-dropdown
class="gl-sm-w-half gl-mt-3 gl-sm-mt-0"
- :refs-project-path="selectedRefsProjectPath"
+ :refs-project-path="refsProjectPath"
:params-name="paramsName"
:params-branch="paramsBranch"
+ v-on="$listeners"
/>
</div>
</gl-card>
diff --git a/app/assets/javascripts/projects/compare/components/revision_dropdown.vue b/app/assets/javascripts/projects/compare/components/revision_dropdown.vue
index d0b69344c12..3e7e20adb34 100644
--- a/app/assets/javascripts/projects/compare/components/revision_dropdown.vue
+++ b/app/assets/javascripts/projects/compare/components/revision_dropdown.vue
@@ -56,6 +56,9 @@ export default {
searchTerm: debounce(function debounceSearch() {
this.searchBranchesAndTags();
}, SEARCH_DEBOUNCE_MS),
+ paramsBranch(newBranch) {
+ this.setSelectedRevision(newBranch);
+ },
},
mounted() {
this.fetchBranchesAndTags();
@@ -84,7 +87,7 @@ export default {
this.loading = true;
if (reset) {
- this.selectedRevision = this.getDefaultBranch();
+ this.setSelectedRevision(this.paramsBranch);
}
return axios
@@ -108,10 +111,14 @@ export default {
return this.paramsBranch || EMPTY_DROPDOWN_TEXT;
},
onClick(revision) {
- this.selectedRevision = revision;
+ this.setSelectedRevision(revision);
+ this.$emit('selectRevision', { direction: this.paramsName, revision });
},
onSearchEnter() {
- this.selectedRevision = this.searchTerm;
+ this.setSelectedRevision(this.searchTerm);
+ },
+ setSelectedRevision(revision) {
+ this.selectedRevision = revision || EMPTY_DROPDOWN_TEXT;
},
},
};
diff --git a/app/assets/javascripts/projects/compare/index.js b/app/assets/javascripts/projects/compare/index.js
index 4ba4e308cd4..322dff773b8 100644
--- a/app/assets/javascripts/projects/compare/index.js
+++ b/app/assets/javascripts/projects/compare/index.js
@@ -22,10 +22,6 @@ export default function init() {
components: {
CompareApp,
},
- provide: {
- projectTo: JSON.parse(projectTo),
- projectsFrom: JSON.parse(projectsFrom),
- },
render(createElement) {
return createElement(CompareApp, {
props: {
@@ -35,6 +31,8 @@ export default function init() {
projectCompareIndexPath,
projectMergeRequestPath,
createMrPath,
+ defaultProject: JSON.parse(projectTo),
+ projects: JSON.parse(projectsFrom),
},
});
},
diff --git a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
index 148ecae0785..0fb8d762c7c 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
@@ -43,6 +43,11 @@ export default {
property: null,
}),
},
+ canEdit: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -113,8 +118,9 @@ export default {
inline
class="gl-mx-auto gl-my-0 hide-expanded"
/>
+ <slot name="collapsed-right"></slot>
<gl-button
- v-if="canUpdate && !initialLoading"
+ v-if="canUpdate && !initialLoading && canEdit"
variant="link"
class="gl-text-gray-900! gl-hover-text-blue-800! gl-ml-auto hide-collapsed"
data-testid="edit-button"
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
deleted file mode 100644
index 3ad097138a3..00000000000
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-<script>
-import { deprecatedCreateFlash as Flash } from '../../../flash';
-import { __ } from '../../../locale';
-import Store from '../../stores/sidebar_store';
-import subscriptions from './subscriptions.vue';
-
-export default {
- components: {
- subscriptions,
- },
- props: {
- mediator: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- store: new Store(),
- };
- },
- methods: {
- onToggleSubscription() {
- this.mediator.toggleSubscription().catch(() => {
- Flash(__('Error occurred when toggling the notification subscription'));
- });
- },
- },
-};
-</script>
-
-<template>
- <div class="block subscriptions">
- <subscriptions
- :loading="store.isFetching.subscriptions"
- :project-emails-disabled="store.projectEmailsDisabled"
- :subscribe-disabled-description="store.subscribeDisabledDescription"
- :subscribed="store.subscribed"
- @toggleSubscription="onToggleSubscription"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
new file mode 100644
index 00000000000..ee7502e3457
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
@@ -0,0 +1,202 @@
+<script>
+import { GlIcon, GlLoadingIcon, GlToggle, GlTooltipDirective } from '@gitlab/ui';
+import createFlash from '~/flash';
+import { IssuableType } from '~/issue_show/constants';
+import { __, sprintf } from '~/locale';
+import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
+import { subscribedQueries } from '~/sidebar/constants';
+
+const ICON_ON = 'notifications';
+const ICON_OFF = 'notifications-off';
+
+export default {
+ tracking: {
+ event: 'click_edit_button',
+ label: 'right_sidebar',
+ property: 'subscriptions',
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ components: {
+ GlIcon,
+ GlLoadingIcon,
+ GlToggle,
+ SidebarEditableItem,
+ },
+ inject: ['canUpdate'],
+ props: {
+ iid: {
+ type: String,
+ required: true,
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ issuableType: {
+ required: true,
+ type: String,
+ },
+ },
+ data() {
+ return {
+ subscribed: false,
+ loading: false,
+ emailsDisabled: false,
+ };
+ },
+ apollo: {
+ subscribed: {
+ query() {
+ return subscribedQueries[this.issuableType].query;
+ },
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ iid: String(this.iid),
+ };
+ },
+ update(data) {
+ return data.workspace?.issuable?.subscribed || false;
+ },
+ result({ data }) {
+ this.emailsDisabled = this.parentIsGroup
+ ? data.workspace?.emailsDisabled
+ : data.workspace?.issuable?.emailsDisabled;
+ this.$emit('subscribedUpdated', data.workspace?.issuable?.subscribed);
+ },
+ error() {
+ createFlash({
+ message: sprintf(
+ __('Something went wrong while setting %{issuableType} notifications.'),
+ {
+ issuableType: this.issuableType,
+ },
+ ),
+ });
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries?.subscribed?.loading || this.loading;
+ },
+ notificationTooltip() {
+ if (this.emailsDisabled) {
+ return this.subscribeDisabledDescription;
+ }
+ return this.subscribed ? this.$options.i18n.labelOn : this.$options.i18n.labelOff;
+ },
+ notificationIcon() {
+ if (this.emailsDisabled || !this.subscribed) {
+ return ICON_OFF;
+ }
+ return ICON_ON;
+ },
+ parentIsGroup() {
+ return this.issuableType === IssuableType.Epic;
+ },
+ subscribeDisabledDescription() {
+ return sprintf(__('Disabled by %{parent} owner'), {
+ parent: this.parentIsGroup ? 'group' : 'project',
+ });
+ },
+ },
+ methods: {
+ setSubscribed(subscribed) {
+ this.loading = true;
+ this.$apollo
+ .mutate({
+ mutation: subscribedQueries[this.issuableType].mutation,
+ variables: {
+ fullPath: this.fullPath,
+ iid: this.iid,
+ subscribedState: subscribed,
+ },
+ })
+ .then(
+ ({
+ data: {
+ updateIssuableSubscription: { errors },
+ },
+ }) => {
+ if (errors.length) {
+ createFlash({
+ message: errors[0],
+ });
+ }
+ },
+ )
+ .catch(() => {
+ createFlash({
+ message: sprintf(
+ __('Something went wrong while setting %{issuableType} notifications.'),
+ {
+ issuableType: this.issuableType,
+ },
+ ),
+ });
+ })
+ .finally(() => {
+ this.loading = false;
+ });
+ },
+ toggleSubscribed() {
+ if (this.emailsDisabled) {
+ this.expandSidebar();
+ } else {
+ this.setSubscribed(!this.subscribed);
+ }
+ },
+ expandSidebar() {
+ this.$emit('expandSidebar');
+ },
+ },
+ i18n: {
+ notifications: __('Notifications'),
+ labelOn: __('Notifications on'),
+ labelOff: __('Notifications off'),
+ },
+};
+</script>
+
+<template>
+ <sidebar-editable-item
+ ref="editable"
+ :title="$options.i18n.notifications"
+ :tracking="$options.tracking"
+ :loading="isLoading"
+ :can-edit="false"
+ class="block subscriptions"
+ >
+ <template #collapsed-right>
+ <gl-toggle
+ :value="subscribed"
+ :is-loading="isLoading"
+ :disabled="emailsDisabled || !canUpdate"
+ class="hide-collapsed gl-ml-auto"
+ data-testid="subscription-toggle"
+ :label="$options.i18n.notifications"
+ label-position="hidden"
+ @change="setSubscribed"
+ />
+ </template>
+ <template #collapsed>
+ <span
+ ref="tooltip"
+ v-gl-tooltip.viewport.left
+ :title="notificationTooltip"
+ class="sidebar-collapsed-icon"
+ @click="toggleSubscribed"
+ >
+ <gl-loading-icon v-if="isLoading" class="sidebar-item-icon is-active" />
+ <gl-icon v-else :name="notificationIcon" :size="16" class="sidebar-item-icon is-active" />
+ </span>
+ <div v-show="emailsDisabled" class="gl-mt-3 hide-collapsed gl-text-gray-500">
+ {{ subscribeDisabledDescription }}
+ </div>
+ </template>
+ <template #default> </template>
+ </sidebar-editable-item>
+</template>
diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js
index 21876ee8dab..93469760ce7 100644
--- a/app/assets/javascripts/sidebar/constants.js
+++ b/app/assets/javascripts/sidebar/constants.js
@@ -2,16 +2,22 @@ import { IssuableType } from '~/issue_show/constants';
import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql';
import epicDueDateQuery from '~/sidebar/queries/epic_due_date.query.graphql';
import epicStartDateQuery from '~/sidebar/queries/epic_start_date.query.graphql';
+import epicSubscribedQuery from '~/sidebar/queries/epic_subscribed.query.graphql';
import issuableAssigneesSubscription from '~/sidebar/queries/issuable_assignees.subscription.graphql';
import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql';
import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
+import issueSubscribedQuery from '~/sidebar/queries/issue_subscribed.query.graphql';
import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql';
+import mergeRequestSubscribed from '~/sidebar/queries/merge_request_subscribed.query.graphql';
import updateEpicConfidentialMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql';
import updateEpicDueDateMutation from '~/sidebar/queries/update_epic_due_date.mutation.graphql';
import updateEpicStartDateMutation from '~/sidebar/queries/update_epic_start_date.mutation.graphql';
+import updateEpicSubscriptionMutation from '~/sidebar/queries/update_epic_subscription.mutation.graphql';
import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_confidential.mutation.graphql';
import updateIssueDueDateMutation from '~/sidebar/queries/update_issue_due_date.mutation.graphql';
+import updateIssueSubscriptionMutation from '~/sidebar/queries/update_issue_subscription.mutation.graphql';
+import updateMergeRequestSubscriptionMutation from '~/sidebar/queries/update_merge_request_subscription.mutation.graphql';
import getIssueAssignees from '~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql';
import issueParticipantsQuery from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql';
import getMergeRequestAssignees from '~/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql';
@@ -80,6 +86,21 @@ export const dateFields = {
},
};
+export const subscribedQueries = {
+ [IssuableType.Issue]: {
+ query: issueSubscribedQuery,
+ mutation: updateIssueSubscriptionMutation,
+ },
+ [IssuableType.Epic]: {
+ query: epicSubscribedQuery,
+ mutation: updateEpicSubscriptionMutation,
+ },
+ [IssuableType.MergeRequest]: {
+ query: mergeRequestSubscribed,
+ mutation: updateMergeRequestSubscriptionMutation,
+ },
+};
+
export const dueDateQueries = {
[IssuableType.Issue]: {
query: issueDueDateQuery,
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index a214dcd4479..d3bfa7c4b58 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -24,7 +24,7 @@ import IssuableLockForm from './components/lock/issuable_lock_form.vue';
import sidebarParticipants from './components/participants/sidebar_participants.vue';
import SidebarReviewers from './components/reviewers/sidebar_reviewers.vue';
import SidebarSeverity from './components/severity/sidebar_severity.vue';
-import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue';
+import SidebarSubscriptionsWidget from './components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue';
@@ -334,21 +334,32 @@ function mountParticipantsComponent(mediator) {
});
}
-function mountSubscriptionsComponent(mediator) {
+function mountSubscriptionsComponent() {
const el = document.querySelector('.js-sidebar-subscriptions-entry-point');
if (!el) return;
+ const { fullPath, iid, editable } = getSidebarOptions();
+
// eslint-disable-next-line no-new
new Vue({
el,
+ apolloProvider,
components: {
- sidebarSubscriptions,
+ SidebarSubscriptionsWidget,
+ },
+ provide: {
+ canUpdate: editable,
},
render: (createElement) =>
- createElement('sidebar-subscriptions', {
+ createElement('sidebar-subscriptions-widget', {
props: {
- mediator,
+ iid: String(iid),
+ fullPath,
+ issuableType:
+ isInIssuePage() || isInIncidentPage() || isInDesignPage()
+ ? IssuableType.Issue
+ : IssuableType.MergeRequest,
},
}),
});
@@ -425,7 +436,7 @@ export function mountSidebar(mediator) {
mountReferenceComponent(mediator);
mountLockComponent();
mountParticipantsComponent(mediator);
- mountSubscriptionsComponent(mediator);
+ mountSubscriptionsComponent();
mountCopyEmailComponent();
new SidebarMoveIssue(
diff --git a/app/assets/javascripts/sidebar/queries/epic_subscribed.query.graphql b/app/assets/javascripts/sidebar/queries/epic_subscribed.query.graphql
new file mode 100644
index 00000000000..9f1967e1685
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/epic_subscribed.query.graphql
@@ -0,0 +1,11 @@
+query epicSubscribed($fullPath: ID!, $iid: ID) {
+ workspace: group(fullPath: $fullPath) {
+ __typename
+ emailsDisabled
+ issuable: epic(iid: $iid) {
+ __typename
+ id
+ subscribed
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/issue_subscribed.query.graphql b/app/assets/javascripts/sidebar/queries/issue_subscribed.query.graphql
new file mode 100644
index 00000000000..7d38b5d3bd8
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/issue_subscribed.query.graphql
@@ -0,0 +1,11 @@
+query issueSubscribed($fullPath: ID!, $iid: String) {
+ workspace: project(fullPath: $fullPath) {
+ __typename
+ issuable: issue(iid: $iid) {
+ __typename
+ id
+ subscribed
+ emailsDisabled
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/merge_request_subscribed.query.graphql b/app/assets/javascripts/sidebar/queries/merge_request_subscribed.query.graphql
new file mode 100644
index 00000000000..3b54a2e529b
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/merge_request_subscribed.query.graphql
@@ -0,0 +1,10 @@
+query mergeRequestSubscribed($fullPath: ID!, $iid: String!) {
+ workspace: project(fullPath: $fullPath) {
+ __typename
+ issuable: mergeRequest(iid: $iid) {
+ __typename
+ id
+ subscribed
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/update_epic_subscription.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_epic_subscription.mutation.graphql
index f2b806102f4..af43766aed5 100644
--- a/app/assets/javascripts/sidebar/queries/update_epic_subscription.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_epic_subscription.mutation.graphql
@@ -1,6 +1,9 @@
-mutation epicSetSubscription($input: EpicSetSubscriptionInput!) {
- updateIssuableSubscription: epicSetSubscription(input: $input) {
- epic {
+mutation epicSetSubscription($fullPath: ID!, $iid: ID!, $subscribedState: Boolean!) {
+ updateIssuableSubscription: epicSetSubscription(
+ input: { groupPath: $fullPath, iid: $iid, subscribedState: $subscribedState }
+ ) {
+ issuable: epic {
+ id
subscribed
}
errors
diff --git a/app/assets/javascripts/sidebar/queries/update_issue_subscription.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_issue_subscription.mutation.graphql
new file mode 100644
index 00000000000..81891fb601f
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/update_issue_subscription.mutation.graphql
@@ -0,0 +1,11 @@
+mutation issueSetSubscription($fullPath: ID!, $iid: String!, $subscribedState: Boolean!) {
+ updateIssuableSubscription: issueSetSubscription(
+ input: { projectPath: $fullPath, iid: $iid, subscribedState: $subscribedState }
+ ) {
+ issuable: issue {
+ id
+ subscribed
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/update_merge_request_subscription.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_merge_request_subscription.mutation.graphql
new file mode 100644
index 00000000000..69944ff9a13
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/update_merge_request_subscription.mutation.graphql
@@ -0,0 +1,11 @@
+mutation mergeRequestSetSubscription($fullPath: ID!, $iid: String!, $subscribedState: Boolean!) {
+ updateIssuableSubscription: mergeRequestSetSubscription(
+ input: { projectPath: $fullPath, iid: $iid, subscribedState: $subscribedState }
+ ) {
+ issuable: mergeRequest {
+ id
+ subscribed
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index f57b638dd81..9f85140bab8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -1,6 +1,6 @@
import { format } from 'timeago.js';
import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key';
-import mrEventHub from '~/merge_request/eventhub';
+import { statusBoxState } from '~/issuable/components/status_box.vue';
import { formatDate } from '../../lib/utils/datetime_utility';
import { MTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY, MWPS_MERGE_STRATEGY } from '../constants';
import { stateKey } from './state_maps';
@@ -23,6 +23,8 @@ export default class MergeRequestStore {
setData(data, isRebased) {
this.initApprovals();
+ this.updateStatusState(data.state);
+
if (isRebased) {
this.sha = data.diff_head_sha;
}
@@ -156,16 +158,14 @@ export default class MergeRequestStore {
this.canRevertInCurrentMR = currentUser.can_revert_on_current_merge_request || false;
this.setState();
-
- if (!window.gon?.features?.mergeRequestWidgetGraphql) {
- this.emitUpdatedState();
- }
}
setGraphqlData(project) {
const { mergeRequest } = project;
const pipeline = mergeRequest.headPipeline;
+ this.updateStatusState(mergeRequest.state);
+
this.projectArchived = project.archived;
this.onlyAllowMergeIfPipelineSucceeds = project.onlyAllowMergeIfPipelineSucceeds;
@@ -190,10 +190,15 @@ export default class MergeRequestStore {
this.workInProgress = mergeRequest.workInProgress;
this.mergeRequestState = mergeRequest.state;
- this.emitUpdatedState();
this.setState();
}
+ updateStatusState(state) {
+ if (this.mergeRequestState !== state && statusBoxState.updateStatus) {
+ statusBoxState.updateStatus();
+ }
+ }
+
setState() {
if (this.mergeOngoing) {
this.state = 'merging';
@@ -216,12 +221,6 @@ export default class MergeRequestStore {
}
}
- emitUpdatedState() {
- mrEventHub.$emit('mr.state.updated', {
- state: this.mergeRequestState,
- });
- }
-
setPaths(data) {
// Paths are set on the first load of the page and not auto-refreshed
this.squashBeforeMergeHelpPath = data.squash_before_merge_help_path;
diff --git a/app/assets/stylesheets/page_bundles/dev_ops_report.scss b/app/assets/stylesheets/page_bundles/dev_ops_report.scss
index 5c6019efce6..690d2f6f41e 100644
--- a/app/assets/stylesheets/page_bundles/dev_ops_report.scss
+++ b/app/assets/stylesheets/page_bundles/dev_ops_report.scss
@@ -1,261 +1,6 @@
@import 'mixins_and_variables_and_functions';
-$space-between-cards: 8px;
-
.devops-empty svg {
margin: 64px auto 32px;
max-width: 420px;
}
-
-.devops-header {
- margin-top: $gl-padding;
- margin-bottom: $gl-padding;
- padding: 0 4px;
- display: flex;
- align-items: center;
-
- .devops-header-title {
- font-size: 48px;
- line-height: 1;
- margin: 0;
- }
-
- .devops-header-subtitle {
- font-size: 22px;
- line-height: 1;
- color: var(--gl-text-color-secondary, $gl-text-color-secondary);
- margin-left: 8px;
- font-weight: $gl-font-weight-normal;
-
- .devops-header-icon {
- vertical-align: px-to-rem(-$gl-spacing-scale-1);
- }
-
- a {
- font-size: 18px;
- color: var(--gl-text-color-secondary, $gl-text-color-secondary);
-
- &:hover {
- color: var(--blue-500, $blue-500);
- }
- }
- }
-}
-
-.devops-cards {
- display: flex;
- justify-content: center;
- flex-wrap: wrap;
-}
-
-.devops-card-wrapper {
- display: flex;
- flex-direction: column;
- align-items: stretch;
- text-align: center;
- width: 50%;
- border-color: var(--border-color, $border-color);
- margin: 0 0 32px;
- padding: $space-between-cards / 2;
- position: relative;
-
- @include media-breakpoint-up(xs) {
- width: percentage(1 / 4);
- }
-
- @include media-breakpoint-up(sm) {
- width: percentage(1 / 5);
- }
-
- @include media-breakpoint-up(md) {
- width: percentage(1 / 6);
- }
-
- @include media-breakpoint-up(lg) {
- width: percentage(1 / 10);
- }
-}
-
-.devops-card {
- border: solid 1px var(--border-color, $border-color);
- border-radius: 3px;
- border-top-width: 3px;
- display: flex;
- flex-direction: column;
- flex-grow: 1;
-}
-
-.devops-card-low {
- border-top-color: var(--red-400, $red-400);
-
- .board-card-score-big {
- background-color: var(--red-50, $red-50);
- }
-}
-
-.devops-card-average {
- border-top-color: var(--orange-200, $orange-200);
-
- .board-card-score-big {
- background-color: var(--orange-50, $orange-50);
- }
-}
-
-.devops-card-high {
- border-top-color: var(--green-400, $green-400);
-
- .board-card-score-big {
- background-color: var(--green-50, $green-50);
- }
-}
-
-.devops-card-title {
- margin: $gl-padding auto auto;
- max-width: 100px;
-
- h3 {
- font-size: 14px;
- margin: 0 0 2px;
- }
-
- .light-text {
- font-size: 13px;
- line-height: 1.25;
- color: var(--gl-text-color-secondary, $gl-text-color-secondary);
- }
-}
-
-.board-card-scores {
- display: flex;
- justify-content: space-around;
- align-items: center;
- margin: $gl-padding $gl-btn-padding;
- line-height: 1;
-}
-
-.board-card-score {
- color: var(--gl-text-color-secondary, $gl-text-color-secondary);
-
- .board-card-score-name {
- font-size: 13px;
- margin-top: 4px;
- }
-}
-
-.board-card-score-value {
- font-size: 16px;
- color: var(--gl-text-color, $gl-text-color);
- font-weight: $gl-font-weight-normal;
-}
-
-.board-card-score-big {
- border-top: 2px solid var(--border-color, $border-color);
- border-bottom: 1px solid var(--border-color, $border-color);
- font-size: 22px;
- padding: 10px 0;
- font-weight: $gl-font-weight-normal;
-}
-
-.board-card-buttons {
- display: flex;
-
- > * {
- font-size: 16px;
- color: var(--gl-text-color-secondary, $gl-text-color-secondary);
- padding: 10px;
- flex-grow: 1;
-
- &:hover {
- background-color: var(--border-color, $border-color);
- color: var(--border-color, $border-color);
- }
-
- + * {
- border-left: solid 1px var(--border-color, $border-color);
- }
- }
-}
-
-.devops-steps {
- margin-top: $gl-padding;
- height: 1px;
- min-width: 100%;
- justify-content: space-around;
- position: relative;
- background: var(--border-color, $border-color);
-}
-
-.devops-step {
- $step-positions: 5% 10% 30% 42% 48% 55% 60% 70% 75% 90%;
- @each $pos in $step-positions {
- $i: index($step-positions, $pos);
-
- &:nth-child(#{$i}) {
- left: $pos;
- }
- }
-
- position: absolute;
- transform-origin: 75% 50%;
- padding: 8px;
- height: 50px;
- width: 50px;
- border-radius: 3px;
- display: flex;
- flex-direction: column;
- align-items: center;
- border: solid 1px var(--border-color, $border-color);
- background: var(--white, $white);
- transform: translate(-50%, -50%);
- color: var(--gl-text-color-secondary, $gl-text-color-secondary);
- fill: var(--gl-text-color-secondary, $gl-text-color-secondary);
- box-shadow: 0 2px 4px var(--dropdown-shadow-color, $dropdown-shadow-color);
-
- &:hover {
- padding: 8px 10px;
- fill: currentColor;
- z-index: 100;
- height: auto;
- width: auto;
-
- .devops-step-title {
- max-height: 2em;
- opacity: 1;
- transition: opacity 0.2s;
- }
-
- svg {
- transform: scale(1.5);
- margin: $gl-btn-padding;
- }
- }
-
- svg {
- transition: transform 0.1s;
- width: 30px;
- height: 30px;
- min-height: 30px;
- min-width: 30px;
- }
-}
-
-.devops-step-title {
- max-height: 0;
- opacity: 0;
- text-transform: uppercase;
- margin: $gl-vert-padding 0 0;
- text-align: center;
- font-size: 12px;
-}
-
-.devops-high-score {
- color: var(--green-400, $green-400);
-}
-
-.devops-average-score {
- color: var(--orange-500, $orange-500);
-}
-
-.devops-low-score {
- color: var(--red-400, $red-400);
-}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 607f3435394..8a53a9b9555 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -207,13 +207,13 @@ class ApplicationController < ActionController::Base
end
respond_to do |format|
- format.any { head status }
format.html do
render template,
layout: "errors",
status: status,
locals: { message: message }
end
+ format.any { head status }
end
end
@@ -223,8 +223,8 @@ class ApplicationController < ActionController::Base
def render_403
respond_to do |format|
- format.any { head :forbidden }
format.html { render "errors/access_denied", layout: "errors", status: :forbidden }
+ format.any { head :forbidden }
end
end
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 014f348d630..c67d7f53faf 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -144,7 +144,7 @@ module Projects
end
def define_badges_variables
- @ref = params[:ref] || @project.default_branch || 'master'
+ @ref = params[:ref] || @project.default_branch_or_main
@badges = [Gitlab::Ci::Badge::Pipeline::Status,
Gitlab::Ci::Badge::Coverage::Report]
diff --git a/app/graphql/resolvers/ci/runner_resolver.rb b/app/graphql/resolvers/ci/runner_resolver.rb
new file mode 100644
index 00000000000..ca94e28b2e9
--- /dev/null
+++ b/app/graphql/resolvers/ci/runner_resolver.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Ci
+ class RunnerResolver < BaseResolver
+ include LooksAhead
+
+ type Types::Ci::RunnerType, null: true
+ extras [:lookahead]
+ description 'Runner information.'
+
+ argument :id,
+ type: ::Types::GlobalIDType[::Ci::Runner],
+ required: true,
+ description: 'Runner ID.'
+
+ def resolve_with_lookahead(id:)
+ find_runner(id: id)
+ end
+
+ private
+
+ def find_runner(id:)
+ runner_id = GitlabSchema.parse_gid(id, expected_type: ::Ci::Runner).model_id.to_i
+ preload_tag_list = lookahead.selects?(:tag_list)
+
+ BatchLoader::GraphQL.for(runner_id).batch(key: { preload_tag_list: preload_tag_list }) do |ids, loader, batch|
+ results = ::Ci::Runner.id_in(ids)
+ results = results.with_tags if batch[:key][:preload_tag_list]
+
+ results.each { |record| loader.call(record.id, record) }
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb
index 07e05a736cd..21df25865d4 100644
--- a/app/graphql/types/ci/job_type.rb
+++ b/app/graphql/types/ci/job_type.rb
@@ -8,6 +8,8 @@ module Types
connection_type_class(Types::CountableConnectionType)
+ expose_permissions Types::PermissionTypes::Ci::Job
+
field :id, ::Types::GlobalIDType[::CommitStatus].as('JobID'), null: true,
description: 'ID of the job.'
field :pipeline, Types::Ci::PipelineType, null: true,
diff --git a/app/graphql/types/ci/runner_access_level_enum.rb b/app/graphql/types/ci/runner_access_level_enum.rb
new file mode 100644
index 00000000000..e98f80336f1
--- /dev/null
+++ b/app/graphql/types/ci/runner_access_level_enum.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class RunnerAccessLevelEnum < BaseEnum
+ graphql_name 'CiRunnerAccessLevel'
+
+ ::Ci::Runner.access_levels.keys.each do |type|
+ value type.upcase,
+ description: "A runner that is #{type.tr('_', ' ')}.",
+ value: type
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/runner_status_enum.rb b/app/graphql/types/ci/runner_status_enum.rb
new file mode 100644
index 00000000000..ad69175e44a
--- /dev/null
+++ b/app/graphql/types/ci/runner_status_enum.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class RunnerStatusEnum < BaseEnum
+ graphql_name 'CiRunnerStatus'
+
+ ::Ci::Runner::AVAILABLE_STATUSES.each do |status|
+ value status.to_s.upcase,
+ description: "A runner that is #{status.to_s.tr('_', ' ')}.",
+ value: status.to_sym
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/runner_type.rb b/app/graphql/types/ci/runner_type.rb
new file mode 100644
index 00000000000..3abed7289d5
--- /dev/null
+++ b/app/graphql/types/ci/runner_type.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class RunnerType < BaseObject
+ graphql_name 'CiRunner'
+ authorize :read_runner
+
+ field :id, ::Types::GlobalIDType[::Ci::Runner], null: false,
+ description: 'ID of the runner.'
+ field :description, GraphQL::STRING_TYPE, null: true,
+ description: 'Description of the runner.'
+ field :contacted_at, Types::TimeType, null: true,
+ description: 'Last contact from the runner.',
+ method: :contacted_at
+ field :maximum_timeout, GraphQL::INT_TYPE, null: true,
+ description: 'Maximum timeout (in seconds) for jobs processed by the runner.'
+ field :access_level, ::Types::Ci::RunnerAccessLevelEnum, null: false,
+ description: 'Access level of the runner.'
+ field :active, GraphQL::BOOLEAN_TYPE, null: false,
+ description: 'Indicates the runner is allowed to receive jobs.'
+ field :status, ::Types::Ci::RunnerStatusEnum, null: false,
+ description: 'Status of the runner.'
+ field :version, GraphQL::STRING_TYPE, null: false,
+ description: 'Version of the runner.'
+ field :short_sha, GraphQL::STRING_TYPE, null: true,
+ description: %q(First eight characters of the runner's token used to authenticate new job requests. Used as the runner's unique ID.)
+ field :revision, GraphQL::STRING_TYPE, null: false,
+ description: 'Revision of the runner.'
+ field :locked, GraphQL::BOOLEAN_TYPE, null: true,
+ description: 'Indicates the runner is locked.'
+ field :run_untagged, GraphQL::BOOLEAN_TYPE, null: false,
+ description: 'Indicates the runner is able to run untagged jobs.'
+ field :ip_address, GraphQL::STRING_TYPE, null: false,
+ description: 'IP address of the runner.'
+ field :runner_type, ::Types::Ci::RunnerTypeEnum, null: false,
+ description: 'Type of the runner.'
+ field :tag_list, [GraphQL::STRING_TYPE], null: true,
+ description: 'Tags associated with the runner.'
+ end
+ end
+end
diff --git a/app/graphql/types/ci/runner_type_enum.rb b/app/graphql/types/ci/runner_type_enum.rb
new file mode 100644
index 00000000000..f771635f4ed
--- /dev/null
+++ b/app/graphql/types/ci/runner_type_enum.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class RunnerTypeEnum < BaseEnum
+ graphql_name 'CiRunnerType'
+
+ ::Ci::Runner.runner_types.keys.each do |type|
+ value type.upcase,
+ description: "A runner that is #{type.tr('_', ' ')}.",
+ value: type
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/permission_types/ci/job.rb b/app/graphql/types/permission_types/ci/job.rb
new file mode 100644
index 00000000000..c9a85317e67
--- /dev/null
+++ b/app/graphql/types/permission_types/ci/job.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Types
+ module PermissionTypes
+ module Ci
+ class Job < BasePermissionType
+ graphql_name 'JobPermissions'
+
+ abilities :read_job_artifacts, :read_build
+ ability_field :update_build, calls_gitaly: true
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 545d82b8f36..6cb20129ad6 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -112,6 +112,13 @@ module Types
field :runner_platforms, resolver: Resolvers::Ci::RunnerPlatformsResolver
field :runner_setup, resolver: Resolvers::Ci::RunnerSetupResolver
+ field :runner, Types::Ci::RunnerType,
+ null: true,
+ resolver: Resolvers::Ci::RunnerResolver,
+ extras: [:lookahead],
+ description: "Find a runner.",
+ feature_flag: :runner_graphql_query
+
field :ci_config, resolver: Resolvers::Ci::ConfigResolver, complexity: 126 # AUTHENTICATED_COMPLEXITY / 2 + 1
def design_management
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index cacf9c7ad0b..7cfe8ffb266 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -176,6 +176,18 @@ module AuthHelper
!current_user
end
+ def auth_app_owner_text(owner)
+ return unless owner
+
+ if owner.is_a?(Group)
+ group_link = link_to(owner.name, group_path(owner))
+ _("This application was created for group %{group_link}.").html_safe % { group_link: group_link }
+ else
+ user_link = link_to(owner.name, user_path(owner))
+ _("This application was created by %{user_link}.").html_safe % { user_link: user_link }
+ end
+ end
+
extend self
end
diff --git a/app/helpers/dev_ops_report_helper.rb b/app/helpers/dev_ops_report_helper.rb
index ab7e56fc1a2..6137cb1f648 100644
--- a/app/helpers/dev_ops_report_helper.rb
+++ b/app/helpers/dev_ops_report_helper.rb
@@ -1,18 +1,78 @@
# frozen_string_literal: true
module DevOpsReportHelper
+ def devops_score_metrics(metric)
+ {
+ averageScore: average_score_data(metric),
+ cards: devops_score_card_data(metric),
+ createdAt: metric.created_at.strftime('%Y-%m-%d %H:%M')
+ }
+ end
+
+ private
+
+ def format_score(score)
+ precision = score < 1 ? 2 : 1
+ number_with_precision(score, precision: precision)
+ end
+
def score_level(score)
if score < 33.33
- 'low'
+ {
+ label: s_('DevopsReport|Low'),
+ variant: 'muted'
+ }
elsif score < 66.66
- 'average'
+ {
+ label: s_('DevopsReport|Moderate'),
+ variant: 'neutral'
+ }
else
- 'high'
+ {
+ label: s_('DevopsReport|High'),
+ variant: 'success'
+ }
end
end
- def format_score(score)
- precision = score < 1 ? 2 : 1
- number_with_precision(score, precision: precision)
+ def average_score_level(score)
+ if score < 33.33
+ {
+ label: s_('DevopsReport|Low'),
+ variant: 'danger',
+ icon: 'status-failed'
+ }
+ elsif score < 66.66
+ {
+ label: s_('DevopsReport|Moderate'),
+ variant: 'warning',
+ icon: 'status-alert'
+ }
+ else
+ {
+ label: s_('DevopsReport|High'),
+ variant: 'success',
+ icon: 'status_success_solid'
+ }
+ end
+ end
+
+ def average_score_data(metric)
+ {
+ value: format_score(metric.average_percentage_score),
+ scoreLevel: average_score_level(metric.average_percentage_score)
+ }
+ end
+
+ def devops_score_card_data(metric)
+ metric.cards.map do |card|
+ {
+ title: "#{card.title} #{card.description}",
+ usage: format_score(card.instance_score),
+ leadInstance: format_score(card.leader_score),
+ score: format_score(card.percentage_score),
+ scoreLevel: score_level(card.percentage_score)
+ }
+ end
end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index d149389b31f..cb54437f941 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -332,6 +332,18 @@ module IssuablesHelper
end
end
+ def state_name_with_icon(issuable)
+ if issuable.is_a?(MergeRequest) && issuable.merged?
+ [_("Merged"), "git-merge"]
+ elsif issuable.is_a?(MergeRequest) && issuable.closed?
+ [_("Closed"), "close"]
+ elsif issuable.closed?
+ [_("Closed"), "mobile-issue-close"]
+ else
+ [_("Open"), "issue-open-m"]
+ end
+ end
+
private
def sidebar_gutter_collapsed?
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index df7fcb0f3da..af8d6da4d84 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -29,16 +29,6 @@ module MergeRequestsHelper
classes.join(' ')
end
- def state_name_with_icon(merge_request)
- if merge_request.merged?
- [_("Merged"), "git-merge"]
- elsif merge_request.closed?
- [_("Closed"), "close"]
- else
- [_("Open"), "issue-open-m"]
- end
- end
-
def merge_path_description(merge_request, separator)
if merge_request.for_fork?
"Project:Branches: #{@merge_request.source_project_path}:#{@merge_request.source_branch} #{separator} #{@merge_request.target_project.full_path}:#{@merge_request.target_branch}"
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 1979426f844..16222b1c863 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -100,7 +100,7 @@ module UsersHelper
badges << blocked_user_badge(user) if user.blocked?
badges << { text: s_('AdminUsers|Admin'), variant: 'success' } if user.admin?
badges << { text: s_('AdminUsers|External'), variant: 'secondary' } if user.external?
- badges << { text: s_("AdminUsers|It's you!"), variant: nil } if current_user == user
+ badges << { text: s_("AdminUsers|It's you!"), variant: 'muted' } if current_user == user
end
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 728393a6eca..346b32ae510 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -107,6 +107,7 @@ class Note < ApplicationRecord
scope :fresh, -> { order_created_asc.with_order_id_asc }
scope :updated_after, ->(time) { where('updated_at > ?', time) }
scope :with_updated_at, ->(time) { where(updated_at: time) }
+ scope :with_suggestions, -> { joins(:suggestions) }
scope :inc_author_project, -> { includes(:project, :author) }
scope :inc_author, -> { includes(:author) }
scope :with_api_entity_associations, -> { preload(:note_diff_file, :author) }
diff --git a/app/models/project.rb b/app/models/project.rb
index 4aa094fed29..4e995c1fff2 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -2532,8 +2532,10 @@ class Project < ApplicationRecord
.exists?
end
- def default_branch_or_master
- default_branch || 'master'
+ def default_branch_or_main
+ return default_branch if default_branch
+
+ Feature.enabled?(:main_branch_over_master, self, default_enabled: :yaml) ? 'main' : 'master'
end
def ci_config_path_or_default
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 681c5cfca74..5e53dbf33d8 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -108,7 +108,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def add_license_ide_path
- ide_edit_path(project, default_branch_or_master, 'LICENSE')
+ ide_edit_path(project, default_branch_or_main, 'LICENSE')
end
def add_changelog_path
@@ -116,7 +116,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def add_changelog_ide_path
- ide_edit_path(project, default_branch_or_master, 'CHANGELOG')
+ ide_edit_path(project, default_branch_or_main, 'CHANGELOG')
end
def add_contribution_guide_path
@@ -124,7 +124,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def add_contribution_guide_ide_path
- ide_edit_path(project, default_branch_or_master, 'CONTRIBUTING.md')
+ ide_edit_path(project, default_branch_or_main, 'CONTRIBUTING.md')
end
def add_readme_path
@@ -132,7 +132,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def add_readme_ide_path
- ide_edit_path(project, default_branch_or_master, 'README.md')
+ ide_edit_path(project, default_branch_or_main, 'README.md')
end
def add_ci_yml_path
@@ -249,10 +249,10 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
nil,
nil,
{
- 'target_branch' => default_branch_or_master,
- 'original_branch' => default_branch_or_master,
+ 'target_branch' => default_branch_or_main,
+ 'original_branch' => default_branch_or_main,
'can_push_code' => 'true',
- 'path' => project_create_blob_path(project, default_branch_or_master),
+ 'path' => project_create_blob_path(project, default_branch_or_main),
'project_path' => project.path
}
)
@@ -268,7 +268,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def new_file_anchor_data
if can_current_user_push_to_default_branch?
- new_file_path = empty_repo? ? ide_edit_path(project, default_branch_or_master) : project_new_blob_path(project, default_branch_or_master)
+ new_file_path = empty_repo? ? ide_edit_path(project, default_branch_or_main) : project_new_blob_path(project, default_branch_or_main)
AnchorData.new(false,
statistic_icon + _('New file'),
@@ -472,7 +472,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
commit_message ||= s_("CommitMessage|Add %{file_name}") % { file_name: file_name }
project_new_blob_path(
project,
- default_branch_or_master,
+ default_branch_or_main,
file_name: file_name,
commit_message: commit_message,
branch_name: branch_name
diff --git a/app/services/ci/test_failure_history_service.rb b/app/services/ci/test_failure_history_service.rb
index 58bbc716ff0..a3f45c1b9cd 100644
--- a/app/services/ci/test_failure_history_service.rb
+++ b/app/services/ci/test_failure_history_service.rb
@@ -30,7 +30,7 @@ module Ci
end
def should_track_failures?
- return false unless project.default_branch_or_master == pipeline.ref
+ return false unless project.default_branch_or_main == pipeline.ref
# We fetch for up to MAX_TRACKABLE_FAILURES + 1 builds. So if ever we get
# 201 total number of builds with the assumption that each job has at least
diff --git a/app/services/merge_requests/create_from_issue_service.rb b/app/services/merge_requests/create_from_issue_service.rb
index b43e697d3ab..e03043ef6a2 100644
--- a/app/services/merge_requests/create_from_issue_service.rb
+++ b/app/services/merge_requests/create_from_issue_service.rb
@@ -73,7 +73,7 @@ module MergeRequests
end
def default_branch
- target_project.default_branch || 'master'
+ target_project.default_branch_or_main
end
def merge_request
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index a8481f40604..3a1fadc68c3 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -149,7 +149,7 @@ module Projects
def create_readme
commit_attrs = {
- branch_name: @project.default_branch || 'master',
+ branch_name: @project.default_branch_or_main,
commit_message: 'Initial commit',
file_path: 'README.md',
file_content: "# #{@project.name}\n\n#{@project.description}"
diff --git a/app/services/repositories/changelog_service.rb b/app/services/repositories/changelog_service.rb
index 0122bfb154d..bac3fdf36da 100644
--- a/app/services/repositories/changelog_service.rb
+++ b/app/services/repositories/changelog_service.rb
@@ -39,7 +39,7 @@ module Repositories
project,
user,
version:,
- branch: project.default_branch_or_master,
+ branch: project.default_branch_or_main,
from: nil,
to: branch,
date: DateTime.now,
diff --git a/app/views/admin/dev_ops_report/_card.html.haml b/app/views/admin/dev_ops_report/_card.html.haml
deleted file mode 100644
index dd6e5c0f108..00000000000
--- a/app/views/admin/dev_ops_report/_card.html.haml
+++ /dev/null
@@ -1,25 +0,0 @@
-.devops-card-wrapper
- .devops-card{ class: "devops-card-#{score_level(card.percentage_score)}" }
- .devops-card-title
- %h3
- = card.title
- .light-text
- = card.description
- .board-card-scores
- .board-card-score
- .board-card-score-value
- = format_score(card.instance_score)
- .board-card-score-name= _('You')
- .board-card-score
- .board-card-score-value
- = format_score(card.leader_score)
- .board-card-score-name= _('Lead')
- .board-card-score-big
- = number_to_percentage(card.percentage_score, precision: 1)
- .board-card-buttons
- - if card.blog
- %a.btn-svg{ href: card.blog }
- = sprite_icon('information-o')
- - if card.docs
- %a.btn-svg{ href: card.docs }
- = sprite_icon('question-o')
diff --git a/app/views/admin/dev_ops_report/_report.html.haml b/app/views/admin/dev_ops_report/_report.html.haml
index 95ef1298d03..db68a7a2291 100644
--- a/app/views/admin/dev_ops_report/_report.html.haml
+++ b/app/views/admin/dev_ops_report/_report.html.haml
@@ -8,25 +8,5 @@
- elsif @metric.blank?
= render 'no_data'
- else
- .devops
- .gl-my-3.gl-text-gray-400{ data: { testid: 'devops-score-note-text' } }
- = s_('DevopsReport|DevOps score metrics are based on usage over the last 30 days. Last updated: %{timestamp}.').html_safe % { timestamp: @metric.created_at.strftime('%Y-%m-%d %H:%M') }
- .devops-header
- %h2.devops-header-title{ class: "devops-#{score_level(@metric.average_percentage_score)}-score" }
- = number_to_percentage(@metric.average_percentage_score, precision: 1)
- .devops-header-subtitle
- = s_('DevopsReport|DevOps')
- %br
- = s_('DevopsReport|Score')
- = link_to sprite_icon('question-o', css_class: 'devops-header-icon'), help_page_path('user/admin_area/analytics/dev_ops_report')
+ #js-devops-score{ data: { devops_score_metrics: devops_score_metrics(@metric).to_json } }
- .devops-cards.board-card-container
- - @metric.cards.each do |card|
- = render 'card', card: card
-
- .devops-steps.d-none.d-lg-block
- - @metric.idea_to_production_steps.each_with_index do |step, index|
- .devops-step{ class: "devops-#{score_level(step.percentage_score)}-score" }
- = custom_icon("i2p_step_#{index + 1}")
- %h4.devops-step-title
- = step.title
diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml
index 5e93b1d89eb..f0e7a96f69f 100644
--- a/app/views/doorkeeper/authorizations/new.html.haml
+++ b/app/views/doorkeeper/authorizations/new.html.haml
@@ -17,10 +17,8 @@
= _("An application called %{link_to_client} is requesting access to your GitLab account.").html_safe % { link_to_client: link_to_client }
- auth_app_owner = @pre_auth.client.application.owner
- - if auth_app_owner
- - link_to_owner = link_to(auth_app_owner.name, user_path(auth_app_owner))
- = _("This application was created by %{link_to_owner}.").html_safe % { link_to_owner: link_to_owner }
+ = auth_app_owner_text(auth_app_owner)
= _("Please note that this application is not provided by GitLab and you should verify its authenticity before allowing access.")
- if @pre_auth.scopes
%p
diff --git a/app/views/layouts/header/_new_dropdown.html.haml b/app/views/layouts/header/_new_dropdown.html.haml
index 7b49e6f716e..ca90d2e02fa 100644
--- a/app/views/layouts/header/_new_dropdown.html.haml
+++ b/app/views/layouts/header/_new_dropdown.html.haml
@@ -1,3 +1,4 @@
+- new_repo_experiment_text = content_for(:new_repo_experiment)
%li.header-new.dropdown{ data: { track_label: "new_dropdown", track_event: "click_dropdown", track_experiment: "new_repo" } }
= link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", id: "js-onboarding-new-project-link", title: _("New..."), ref: 'tooltip', aria: { label: _("New...") }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body', display: 'static' } do
= sprite_icon('plus-square')
@@ -12,9 +13,9 @@
%li.dropdown-bold-header
= _('This group')
- if create_group_project
- %li= link_to _('New project'), new_project_path(namespace_id: @group.id)
+ %li= link_to new_repo_experiment_text, new_project_path(namespace_id: @group.id), data: { track_experiment: 'new_repo', track_event: 'click_link_new_project_group', track_label: 'plus_menu_dropdown' }
- if create_group_subgroup
- %li= link_to _('New subgroup'), new_group_path(parent_id: @group.id)
+ %li= link_to _('New subgroup'), new_group_path(parent_id: @group.id), data: { track_event: 'click_link_new_subgroup', track_label: 'plus_menu_dropdown' }
= render_if_exists 'layouts/header/create_epic_new_dropdown_item'
= render 'layouts/header/group_invite_members_new_dropdown_item'
%li.divider
@@ -29,16 +30,18 @@
%li.dropdown-bold-header
= _('This project')
- if create_project_issue
- %li= link_to _('New issue'), new_project_issue_path(@project)
+ %li= link_to _('New issue'), new_project_issue_path(@project), data: { track_event: 'click_link_new_issue', track_label: 'plus_menu_dropdown' }
- if merge_project
- %li= link_to _('New merge request'), project_new_merge_request_path(merge_project)
+ %li= link_to _('New merge request'), project_new_merge_request_path(merge_project), data: { track_event: 'click_link_new_mr', track_label: 'plus_menu_dropdown' }
+
- if create_project_snippet
- %li= link_to _('New snippet'), new_project_snippet_path(@project)
+ %li= link_to _('New snippet'), new_project_snippet_path(@project), data: { track_event: 'click_link_new_snippet_project', track_label: 'plus_menu_dropdown' }
= render 'layouts/header/project_invite_members_new_dropdown_item'
%li.divider
%li.dropdown-bold-header GitLab
- = content_for :new_repo_experiment
+ - if current_user.can_create_project?
+ %li= link_to new_repo_experiment_text, new_project_path, class: 'qa-global-new-project-link', data: { track_experiment: 'new_repo', track_event: 'click_link_new_project', track_label: 'plus_menu_dropdown' }
- if current_user.can_create_group?
- %li= link_to _('New group'), new_group_path
+ %li= link_to _('New group'), new_group_path, data: { track_event: 'click_link_new_group', track_label: 'plus_menu_dropdown' }
- if current_user.can?(:create_snippet)
- %li= link_to _('New snippet'), new_snippet_path, class: 'qa-global-new-snippet-link'
+ %li= link_to _('New snippet'), new_snippet_path, data: { track_event: 'click_link_new_snippet_parent', track_label: 'plus_menu_dropdown' }, class: 'qa-global-new-snippet-link'
diff --git a/app/views/layouts/header/_new_repo_experiment.html.haml b/app/views/layouts/header/_new_repo_experiment.html.haml
index 73f960844cb..aaa13d593cd 100644
--- a/app/views/layouts/header/_new_repo_experiment.html.haml
+++ b/app/views/layouts/header/_new_repo_experiment.html.haml
@@ -1,7 +1,6 @@
- content_for :new_repo_experiment do
- - if current_user&.can_create_project?
- - experiment(:new_repo, user: current_user) do |e|
- - e.use do
- %li= link_to _('New project'), new_project_path, class: 'qa-global-new-project-link', data: { track_experiment: 'new_repo', track_event: 'click_link', track_label: 'plus_menu_dropdown' }
- - e.try do
- %li= link_to _('New project/repository'), new_project_path, class: 'qa-global-new-project-link', data: { track_experiment: 'new_repo', track_event: 'click_link', track_label: 'plus_menu_dropdown' }
+ - experiment(:new_repo, user: current_user) do |e|
+ - e.use do
+ = _('New project')
+ - e.try do
+ = _('New project/repository')
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index 5effa5a9e92..12ce4667e1a 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -28,14 +28,14 @@
%li.dropdown-header= _('This repository')
- if can_push_code
- %li.qa-new-file-option= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master')
+ %li.qa-new-file-option= link_to _('New file'), project_new_blob_path(@project, @project.default_branch_or_main)
- unless @project.empty_repo?
%li= link_to _('New branch'), new_project_branch_path(@project)
%li= link_to _('New tag'), new_project_tag_path(@project)
- elsif can_collaborate_with_project?(@project)
- %li= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master')
+ %li= link_to _('New file'), project_new_blob_path(@project, @project.default_branch_or_main)
- elsif create_mr_from_new_fork
- - continue_params = { to: project_new_blob_path(@project, @project.default_branch || 'master'),
+ - continue_params = { to: project_new_blob_path(@project, @project.default_branch_or_main),
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now }
- fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params)
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 171222368d6..b76f6b27aa8 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -1,5 +1,5 @@
- @content_class = "limit-container-width" unless fluid_layout
-- default_branch_name = @project.default_branch_or_master
+- default_branch_name = @project.default_branch_or_main
- @skip_current_level_breadcrumb = true
= render partial: 'flash_messages', locals: { project: @project }
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index 06a2ed46805..0136184f80d 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -7,4 +7,4 @@
"new-environment-path" => new_project_environment_path(@project),
"help-page-path" => help_page_path("ci/environments/index.md"),
"project-path" => @project.full_path,
- "default-branch-name" => @project.default_branch_or_master } }
+ "default-branch-name" => @project.default_branch_or_main } }
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index 26d8e571973..6ecf78f0460 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -1,7 +1,6 @@
- @no_breadcrumb_border = true
- can_update_merge_request = can?(current_user, :update_merge_request, @merge_request)
- can_reopen_merge_request = can?(current_user, :reopen_merge_request, @merge_request)
-- state_human_name, state_icon_name = state_name_with_icon(@merge_request)
- are_close_and_open_buttons_hidden = merge_request_button_hidden?(@merge_request, true) && merge_request_button_hidden?(@merge_request, false)
- if @merge_request.closed_or_merged_without_fork?
@@ -12,10 +11,7 @@
.detail-page-header.border-bottom-0.pt-0.pb-0
.detail-page-header-body
- .issuable-status-box.status-box.js-mr-status-box{ class: status_box_class(@merge_request), data: { state: @merge_request.state } }
- = sprite_icon(state_icon_name, css_class: 'gl-display-block gl-sm-display-none!')
- %span.gl-display-none.gl-sm-display-block
- = state_human_name
+ = render "shared/issuable/status_box", issuable: @merge_request
.issuable-meta
#js-issuable-header-warnings
diff --git a/app/views/shared/issuable/_status_box.html.haml b/app/views/shared/issuable/_status_box.html.haml
new file mode 100644
index 00000000000..c0e972684d2
--- /dev/null
+++ b/app/views/shared/issuable/_status_box.html.haml
@@ -0,0 +1,6 @@
+- state_human_name, state_icon_name = state_name_with_icon(issuable)
+
+.issuable-status-box.status-box.js-mr-status-box{ class: status_box_class(issuable), data: { project_path: issuable.project.path_with_namespace, iid: issuable.iid, state: issuable.state } }
+ = sprite_icon(state_icon_name, css_class: 'gl-display-block gl-sm-display-none!')
+ %span.gl-display-none.gl-sm-display-block
+ = state_human_name