diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-04 15:10:04 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-04 15:10:04 +0300 |
commit | 998adcc422d4161515bf2960ef4dce71258f69a3 (patch) | |
tree | 34ffbf53b8587d3367457d4175fc8dfd71477dca /app | |
parent | e2c471038ab2b4f09487c1feb74520f74f834986 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
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 |