diff options
Diffstat (limited to 'app/assets/javascripts/projects')
35 files changed, 772 insertions, 425 deletions
diff --git a/app/assets/javascripts/projects/commit/components/branches_dropdown.vue b/app/assets/javascripts/projects/commit/components/branches_dropdown.vue index 3ecc3f1d1d3..3527bcb04c6 100644 --- a/app/assets/javascripts/projects/commit/components/branches_dropdown.vue +++ b/app/assets/javascripts/projects/commit/components/branches_dropdown.vue @@ -68,6 +68,7 @@ export default { autocomplete="off" :debounce="250" :placeholder="$options.i18n.searchPlaceholder" + data-testid="dropdown-search-box" @input="searchTermChanged" /> <gl-dropdown-item @@ -77,6 +78,7 @@ export default { :name="branch" :is-checked="isSelected(branch)" is-check-item + data-testid="dropdown-item" @click="selectBranch(branch)" > {{ branch }} diff --git a/app/assets/javascripts/projects/commit/components/form_modal.vue b/app/assets/javascripts/projects/commit/components/form_modal.vue index 6411b1ca921..ed216a91ca0 100644 --- a/app/assets/javascripts/projects/commit/components/form_modal.vue +++ b/app/assets/javascripts/projects/commit/components/form_modal.vue @@ -1,8 +1,9 @@ <script> import { GlModal, GlForm, GlFormCheckbox, GlSprintf, GlFormGroup } from '@gitlab/ui'; import { mapActions, mapState } from 'vuex'; -import eventHub from '../event_hub'; +import { BV_SHOW_MODAL } from '~/lib/utils/constants'; import csrf from '~/lib/utils/csrf'; +import eventHub from '../event_hub'; import BranchesDropdown from './branches_dropdown.vue'; export default { @@ -67,7 +68,7 @@ export default { methods: { ...mapActions(['clearModal', 'setBranch', 'setSelectedBranch']), show() { - this.$root.$emit('bv::show::modal', this.modalId); + this.$root.$emit(BV_SHOW_MODAL, this.modalId); }, handlePrimary() { this.$refs.form.$el.submit(); diff --git a/app/assets/javascripts/projects/commit/components/form_trigger.vue b/app/assets/javascripts/projects/commit/components/form_trigger.vue index e92854c1ac3..3561b5c2473 100644 --- a/app/assets/javascripts/projects/commit/components/form_trigger.vue +++ b/app/assets/javascripts/projects/commit/components/form_trigger.vue @@ -10,6 +10,9 @@ export default { displayText: { default: '', }, + testId: { + default: '', + }, }, props: { openModal: { @@ -26,7 +29,7 @@ export default { </script> <template> - <gl-link data-is-link="true" data-testid="revert-commit-link" @click="showModal"> + <gl-link data-is-link="true" :data-testid="testId" @click="showModal"> {{ displayText }} </gl-link> </template> diff --git a/app/assets/javascripts/projects/commit/constants.js b/app/assets/javascripts/projects/commit/constants.js index 233f43d56b9..b47c744e5fb 100644 --- a/app/assets/javascripts/projects/commit/constants.js +++ b/app/assets/javascripts/projects/commit/constants.js @@ -2,6 +2,10 @@ import { s__, __ } from '~/locale'; export const OPEN_REVERT_MODAL = 'openRevertModal'; export const REVERT_MODAL_ID = 'revert-commit-modal'; +export const REVERT_LINK_TEST_ID = 'revert-commit-link'; +export const OPEN_CHERRY_PICK_MODAL = 'openCherryPickModal'; +export const CHERRY_PICK_MODAL_ID = 'cherry-pick-commit-modal'; +export const CHERRY_PICK_LINK_TEST_ID = 'cherry-pick-commit-link'; export const I18N_MODAL = { startMergeRequest: s__('ChangeTypeAction|Start a %{newMergeRequest} with these changes'), @@ -20,6 +24,11 @@ export const I18N_REVERT_MODAL = { actionPrimaryText: s__('ChangeTypeAction|Revert'), }; +export const I18N_CHERRY_PICK_MODAL = { + branchLabel: s__('ChangeTypeAction|Pick into branch'), + actionPrimaryText: s__('ChangeTypeAction|Cherry-pick'), +}; + export const PREPENDED_MODAL_TEXT = s__( 'ChangeTypeAction|This will create a new commit in order to revert the existing changes.', ); diff --git a/app/assets/javascripts/projects/commit/index.js b/app/assets/javascripts/projects/commit/index.js new file mode 100644 index 00000000000..b5fdfc25236 --- /dev/null +++ b/app/assets/javascripts/projects/commit/index.js @@ -0,0 +1,11 @@ +import initCherryPickCommitModal from './init_cherry_pick_commit_modal'; +import initCherryPickCommitTrigger from './init_cherry_pick_commit_trigger'; +import initRevertCommitModal from './init_revert_commit_modal'; +import initRevertCommitTrigger from './init_revert_commit_trigger'; + +export default () => { + initRevertCommitModal(); + initRevertCommitTrigger(); + initCherryPickCommitModal(); + initCherryPickCommitTrigger(); +}; diff --git a/app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js b/app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js new file mode 100644 index 00000000000..24baa27ff70 --- /dev/null +++ b/app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js @@ -0,0 +1,51 @@ +import Vue from 'vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import CommitFormModal from './components/form_modal.vue'; +import { + I18N_MODAL, + I18N_CHERRY_PICK_MODAL, + OPEN_CHERRY_PICK_MODAL, + CHERRY_PICK_MODAL_ID, +} from './constants'; +import createStore from './store'; + +export default function initInviteMembersModal() { + const el = document.querySelector('.js-cherry-pick-commit-modal'); + if (!el) { + return false; + } + + const { + title, + endpoint, + branch, + pushCode, + branchCollaboration, + existingBranch, + branchesEndpoint, + } = el.dataset; + + const store = createStore({ + endpoint, + branchesEndpoint, + branch, + pushCode: parseBoolean(pushCode), + branchCollaboration: parseBoolean(branchCollaboration), + defaultBranch: branch, + modalTitle: title, + existingBranch, + }); + + return new Vue({ + el, + store, + render: (createElement) => + createElement(CommitFormModal, { + props: { + i18n: { ...I18N_CHERRY_PICK_MODAL, ...I18N_MODAL }, + openModal: OPEN_CHERRY_PICK_MODAL, + modalId: CHERRY_PICK_MODAL_ID, + }, + }), + }); +} diff --git a/app/assets/javascripts/projects/commit/init_cherry_pick_commit_trigger.js b/app/assets/javascripts/projects/commit/init_cherry_pick_commit_trigger.js new file mode 100644 index 00000000000..942451dc96a --- /dev/null +++ b/app/assets/javascripts/projects/commit/init_cherry_pick_commit_trigger.js @@ -0,0 +1,20 @@ +import Vue from 'vue'; +import CommitFormTrigger from './components/form_trigger.vue'; +import { OPEN_CHERRY_PICK_MODAL, CHERRY_PICK_LINK_TEST_ID } from './constants'; + +export default function initInviteMembersTrigger() { + const el = document.querySelector('.js-cherry-pick-commit-trigger'); + + if (!el) { + return false; + } + + const { displayText } = el.dataset; + + return new Vue({ + el, + provide: { displayText, testId: CHERRY_PICK_LINK_TEST_ID }, + render: (createElement) => + createElement(CommitFormTrigger, { props: { openModal: OPEN_CHERRY_PICK_MODAL } }), + }); +} diff --git a/app/assets/javascripts/projects/commit/init_revert_commit_modal.js b/app/assets/javascripts/projects/commit/init_revert_commit_modal.js index ec0600cd25a..df26aa3c830 100644 --- a/app/assets/javascripts/projects/commit/init_revert_commit_modal.js +++ b/app/assets/javascripts/projects/commit/init_revert_commit_modal.js @@ -1,7 +1,6 @@ import Vue from 'vue'; -import CommitFormModal from './components/form_modal.vue'; import { parseBoolean } from '~/lib/utils/common_utils'; -import createStore from './store'; +import CommitFormModal from './components/form_modal.vue'; import { I18N_MODAL, I18N_REVERT_MODAL, @@ -9,6 +8,7 @@ import { OPEN_REVERT_MODAL, REVERT_MODAL_ID, } from './constants'; +import createStore from './store'; export default function initInviteMembersModal() { const el = document.querySelector('.js-revert-commit-modal'); diff --git a/app/assets/javascripts/projects/commit/init_revert_commit_trigger.js b/app/assets/javascripts/projects/commit/init_revert_commit_trigger.js index 0bb57f22663..dc5168524ca 100644 --- a/app/assets/javascripts/projects/commit/init_revert_commit_trigger.js +++ b/app/assets/javascripts/projects/commit/init_revert_commit_trigger.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import RevertCommitTrigger from './components/form_trigger.vue'; -import { OPEN_REVERT_MODAL } from './constants'; +import CommitFormTrigger from './components/form_trigger.vue'; +import { OPEN_REVERT_MODAL, REVERT_LINK_TEST_ID } from './constants'; export default function initInviteMembersTrigger() { const el = document.querySelector('.js-revert-commit-trigger'); @@ -13,8 +13,8 @@ export default function initInviteMembersTrigger() { return new Vue({ el, - provide: { displayText }, + provide: { displayText, testId: REVERT_LINK_TEST_ID }, render: (createElement) => - createElement(RevertCommitTrigger, { props: { openModal: OPEN_REVERT_MODAL } }), + createElement(CommitFormTrigger, { props: { openModal: OPEN_REVERT_MODAL } }), }); } diff --git a/app/assets/javascripts/projects/commit/store/actions.js b/app/assets/javascripts/projects/commit/store/actions.js index 2ae0370d579..10135e55351 100644 --- a/app/assets/javascripts/projects/commit/store/actions.js +++ b/app/assets/javascripts/projects/commit/store/actions.js @@ -1,7 +1,7 @@ -import * as types from './mutation_types'; -import axios from '~/lib/utils/axios_utils'; import createFlash from '~/flash'; +import axios from '~/lib/utils/axios_utils'; import { PROJECT_BRANCHES_ERROR } from '../constants'; +import * as types from './mutation_types'; export const clearModal = ({ commit }) => { commit(types.CLEAR_MODAL); diff --git a/app/assets/javascripts/projects/commit_box/info/index.js b/app/assets/javascripts/projects/commit_box/info/index.js index 254d178f013..4bbdb5c2357 100644 --- a/app/assets/javascripts/projects/commit_box/info/index.js +++ b/app/assets/javascripts/projects/commit_box/info/index.js @@ -1,7 +1,7 @@ -import { loadBranches } from './load_branches'; -import { initDetailsButton } from './init_details_button'; import { fetchCommitMergeRequests } from '~/commit_merge_requests'; import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown'; +import { initDetailsButton } from './init_details_button'; +import { loadBranches } from './load_branches'; export const initCommitBoxInfo = (containerSelector = '.js-commit-box-info') => { const containerEl = document.querySelector(containerSelector); diff --git a/app/assets/javascripts/projects/commits/components/author_select.vue b/app/assets/javascripts/projects/commits/components/author_select.vue index 752bb594794..1566232751d 100644 --- a/app/assets/javascripts/projects/commits/components/author_select.vue +++ b/app/assets/javascripts/projects/commits/components/author_select.vue @@ -1,6 +1,4 @@ <script> -import { debounce } from 'lodash'; -import { mapState, mapActions } from 'vuex'; import { GlDropdown, GlDropdownSectionHeader, @@ -9,8 +7,10 @@ import { GlDropdownDivider, GlTooltipDirective, } from '@gitlab/ui'; -import { redirectTo } from '~/lib/utils/url_utility'; +import { debounce } from 'lodash'; +import { mapState, mapActions } from 'vuex'; import { urlParamsToObject } from '~/lib/utils/common_utils'; +import { redirectTo } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; const tooltipMessage = __('Searching by both author and message is currently not supported.'); diff --git a/app/assets/javascripts/projects/commits/store/actions.js b/app/assets/javascripts/projects/commits/store/actions.js index 359d81f32f7..72d4f0c31e5 100644 --- a/app/assets/javascripts/projects/commits/store/actions.js +++ b/app/assets/javascripts/projects/commits/store/actions.js @@ -1,9 +1,9 @@ -import * as Sentry from '~/sentry/wrapper'; -import * as types from './mutation_types'; -import axios from '~/lib/utils/axios_utils'; import { deprecatedCreateFlash as createFlash } from '~/flash'; -import { __ } from '~/locale'; +import axios from '~/lib/utils/axios_utils'; import { joinPaths } from '~/lib/utils/url_utility'; +import { __ } from '~/locale'; +import * as Sentry from '~/sentry/wrapper'; +import * as types from './mutation_types'; export default { setInitialData({ commit }, data) { diff --git a/app/assets/javascripts/projects/compare/components/app.vue b/app/assets/javascripts/projects/compare/components/app.vue new file mode 100644 index 00000000000..05bd0f1370b --- /dev/null +++ b/app/assets/javascripts/projects/compare/components/app.vue @@ -0,0 +1,89 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import csrf from '~/lib/utils/csrf'; +import RevisionDropdown from './revision_dropdown.vue'; + +export default { + csrf, + components: { + RevisionDropdown, + GlButton, + }, + props: { + projectCompareIndexPath: { + type: String, + required: true, + }, + refsProjectPath: { + type: String, + required: true, + }, + paramsFrom: { + type: String, + required: false, + default: null, + }, + paramsTo: { + type: String, + required: false, + default: null, + }, + projectMergeRequestPath: { + type: String, + required: true, + }, + createMrPath: { + type: String, + required: true, + }, + }, + methods: { + onSubmit() { + this.$refs.form.submit(); + }, + }, +}; +</script> + +<template> + <form + ref="form" + class="form-inline js-requires-input js-signature-container" + method="POST" + :action="projectCompareIndexPath" + > + <input :value="$options.csrf.token" type="hidden" name="authenticity_token" /> + <revision-dropdown + :refs-project-path="refsProjectPath" + revision-text="Source" + params-name="to" + :params-branch="paramsTo" + /> + <div class="compare-ellipsis gl-display-inline" data-testid="ellipsis">...</div> + <revision-dropdown + :refs-project-path="refsProjectPath" + revision-text="Target" + params-name="from" + :params-branch="paramsFrom" + /> + <gl-button category="primary" variant="success" class="gl-ml-3" @click="onSubmit"> + {{ s__('CompareRevisions|Compare') }} + </gl-button> + <a + v-if="projectMergeRequestPath" + :href="projectMergeRequestPath" + data-testid="projectMrButton" + class="btn btn-default gl-button gl-ml-3" + > + {{ s__('CompareRevisions|View open merge request') }} + </a> + <a + v-else-if="createMrPath" + :href="createMrPath" + data-testid="createMrButton" + class="btn btn-default gl-button gl-ml-3" + > + {{ s__('CompareRevisions|Create merge request') }} + </a> + </form> +</template> diff --git a/app/assets/javascripts/projects/compare/components/revision_dropdown.vue b/app/assets/javascripts/projects/compare/components/revision_dropdown.vue new file mode 100644 index 00000000000..13d80b5ae0b --- /dev/null +++ b/app/assets/javascripts/projects/compare/components/revision_dropdown.vue @@ -0,0 +1,145 @@ +<script> +import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlDropdownSectionHeader } from '@gitlab/ui'; +import createFlash from '~/flash'; +import axios from '~/lib/utils/axios_utils'; +import { s__ } from '~/locale'; + +export default { + components: { + GlDropdown, + GlDropdownItem, + GlDropdownSectionHeader, + GlSearchBoxByType, + }, + props: { + refsProjectPath: { + type: String, + required: true, + }, + revisionText: { + type: String, + required: true, + }, + paramsName: { + type: String, + required: true, + }, + paramsBranch: { + type: String, + required: false, + default: null, + }, + }, + data() { + return { + branches: [], + tags: [], + loading: true, + searchTerm: '', + selectedRevision: this.getDefaultBranch(), + }; + }, + computed: { + filteredBranches() { + return this.branches.filter((branch) => + branch.toLowerCase().includes(this.searchTerm.toLowerCase()), + ); + }, + hasFilteredBranches() { + return this.filteredBranches.length; + }, + filteredTags() { + return this.tags.filter((tag) => tag.toLowerCase().includes(this.searchTerm.toLowerCase())); + }, + hasFilteredTags() { + return this.filteredTags.length; + }, + }, + mounted() { + this.fetchBranchesAndTags(); + }, + methods: { + fetchBranchesAndTags() { + const endpoint = this.refsProjectPath; + + return axios + .get(endpoint) + .then(({ data }) => { + this.branches = data.Branches || []; + this.tags = data.Tags || []; + }) + .catch(() => { + createFlash({ + message: `${s__( + 'CompareRevisions|There was an error while updating the branch/tag list. Please try again.', + )}`, + }); + }) + .finally(() => { + this.loading = false; + }); + }, + getDefaultBranch() { + return this.paramsBranch || s__('CompareRevisions|Select branch/tag'); + }, + onClick(revision) { + this.selectedRevision = revision; + }, + onSearchEnter() { + this.selectedRevision = this.searchTerm; + }, + }, +}; +</script> + +<template> + <div class="form-group compare-form-group" :class="`js-compare-${paramsName}-dropdown`"> + <div class="input-group inline-input-group"> + <span class="input-group-prepend"> + <div class="input-group-text"> + {{ revisionText }} + </div> + </span> + <input type="hidden" :name="paramsName" :value="selectedRevision" /> + <gl-dropdown + class="gl-flex-grow-1 gl-flex-basis-0 gl-min-w-0 gl-font-monospace" + toggle-class="form-control compare-dropdown-toggle js-compare-dropdown gl-min-w-0 gl-rounded-top-left-none! gl-rounded-bottom-left-none!" + :text="selectedRevision" + header-text="Select Git revision" + :loading="loading" + > + <template #header> + <gl-search-box-by-type + v-model.trim="searchTerm" + :placeholder="s__('CompareRevisions|Filter by Git revision')" + @keyup.enter="onSearchEnter" + /> + </template> + <gl-dropdown-section-header v-if="hasFilteredBranches"> + {{ s__('CompareRevisions|Branches') }} + </gl-dropdown-section-header> + <gl-dropdown-item + v-for="(branch, index) in filteredBranches" + :key="`branch${index}`" + is-check-item + :is-checked="selectedRevision === branch" + @click="onClick(branch)" + > + {{ branch }} + </gl-dropdown-item> + <gl-dropdown-section-header v-if="hasFilteredTags"> + {{ s__('CompareRevisions|Tags') }} + </gl-dropdown-section-header> + <gl-dropdown-item + v-for="(tag, index) in filteredTags" + :key="`tag${index}`" + is-check-item + :is-checked="selectedRevision === tag" + @click="onClick(tag)" + > + {{ tag }} + </gl-dropdown-item> + </gl-dropdown> + </div> + </div> +</template> diff --git a/app/assets/javascripts/projects/compare/index.js b/app/assets/javascripts/projects/compare/index.js new file mode 100644 index 00000000000..4337eecb667 --- /dev/null +++ b/app/assets/javascripts/projects/compare/index.js @@ -0,0 +1,33 @@ +import Vue from 'vue'; +import CompareApp from './components/app.vue'; + +export default function init() { + const el = document.getElementById('js-compare-selector'); + const { + refsProjectPath, + paramsFrom, + paramsTo, + projectCompareIndexPath, + projectMergeRequestPath, + createMrPath, + } = el.dataset; + + return new Vue({ + el, + components: { + CompareApp, + }, + render(createElement) { + return createElement(CompareApp, { + props: { + refsProjectPath, + paramsFrom, + paramsTo, + projectCompareIndexPath, + projectMergeRequestPath, + createMrPath, + }, + }); + }, + }); +} diff --git a/app/assets/javascripts/projects/components/project_delete_button.vue b/app/assets/javascripts/projects/components/project_delete_button.vue index 5429d51dae0..81d23a563e2 100644 --- a/app/assets/javascripts/projects/components/project_delete_button.vue +++ b/app/assets/javascripts/projects/components/project_delete_button.vue @@ -22,10 +22,10 @@ export default { strings: { alertTitle: __('You are about to permanently delete this project'), alertBody: __( - 'Once a project is permanently deleted it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd} including issues, merge requests etc.', + 'Once a project is permanently deleted, it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd}, including issues, merge requests etc.', ), modalBody: __( - "This action cannot be undone. You will lose this project's repository and all content: issues, merge requests, etc.", + "This action cannot be undone. You will lose this project's repository and all related resources, including issues, merge requests, etc.", ), }, }; diff --git a/app/assets/javascripts/projects/components/shared/delete_button.vue b/app/assets/javascripts/projects/components/shared/delete_button.vue index 051bfcb732a..2e46f437ace 100644 --- a/app/assets/javascripts/projects/components/shared/delete_button.vue +++ b/app/assets/javascripts/projects/components/shared/delete_button.vue @@ -1,8 +1,8 @@ <script> -import { uniqueId } from 'lodash'; import { GlModal, GlModalDirective, GlFormInput, GlButton } from '@gitlab/ui'; -import { __ } from '~/locale'; +import { uniqueId } from 'lodash'; import csrf from '~/lib/utils/csrf'; +import { __ } from '~/locale'; export default { components: { diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue b/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue index b54f7051806..ef61fba88fe 100644 --- a/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue +++ b/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue @@ -1,14 +1,14 @@ <script> /* eslint-disable vue/no-v-html */ import { GlBreadcrumb, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; -import WelcomePage from './welcome.vue'; -import LegacyContainer from './legacy_container.vue'; import { __, s__ } from '~/locale'; import blankProjectIllustration from '../illustrations/blank-project.svg'; +import ciCdProjectIllustration from '../illustrations/ci-cd-project.svg'; import createFromTemplateIllustration from '../illustrations/create-from-template.svg'; import importProjectIllustration from '../illustrations/import-project.svg'; -import ciCdProjectIllustration from '../illustrations/ci-cd-project.svg'; +import LegacyContainer from './legacy_container.vue'; +import WelcomePage from './welcome.vue'; const BLANK_PANEL = 'blank_project'; const CI_CD_PANEL = 'cicd_for_external_repo'; diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/index.js b/app/assets/javascripts/projects/experiment_new_project_creation/index.js index 06920a5ab19..0414f7ef6a5 100644 --- a/app/assets/javascripts/projects/experiment_new_project_creation/index.js +++ b/app/assets/javascripts/projects/experiment_new_project_creation/index.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import NewProjectCreationApp from './components/app.vue'; -export default function (el, props) { +export default function initNewProjectCreation(el, props) { return new Vue({ el, components: { diff --git a/app/assets/javascripts/projects/members/constants.js b/app/assets/javascripts/projects/members/constants.js new file mode 100644 index 00000000000..a69a64fe882 --- /dev/null +++ b/app/assets/javascripts/projects/members/constants.js @@ -0,0 +1 @@ +export const PROJECT_MEMBER_BASE_PROPERTY_NAME = 'project_member'; diff --git a/app/assets/javascripts/projects/members/utils.js b/app/assets/javascripts/projects/members/utils.js new file mode 100644 index 00000000000..a8a635e3ce8 --- /dev/null +++ b/app/assets/javascripts/projects/members/utils.js @@ -0,0 +1,8 @@ +import { MEMBER_ACCESS_LEVEL_PROPERTY_NAME } from '~/members/constants'; +import { baseRequestFormatter } from '~/members/utils'; +import { PROJECT_MEMBER_BASE_PROPERTY_NAME } from './constants'; + +export const projectMemberRequestFormatter = baseRequestFormatter( + PROJECT_MEMBER_BASE_PROPERTY_NAME, + MEMBER_ACCESS_LEVEL_PROPERTY_NAME, +); diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue index 7bb62cf4a73..4a8e1424fa8 100644 --- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue +++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue @@ -1,44 +1,12 @@ <script> -import { GlAlert, GlTabs, GlTab } from '@gitlab/ui'; -import { s__ } from '~/locale'; -import getPipelineCountByStatus from '../graphql/queries/get_pipeline_count_by_status.query.graphql'; -import getProjectPipelineStatistics from '../graphql/queries/get_project_pipeline_statistics.query.graphql'; +import { GlTabs, GlTab } from '@gitlab/ui'; +import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility'; import PipelineCharts from './pipeline_charts.vue'; -import { - DEFAULT, - LOAD_ANALYTICS_FAILURE, - LOAD_PIPELINES_FAILURE, - PARSE_FAILURE, - UNSUPPORTED_DATA, -} from '../constants'; - -const defaultAnalyticsValues = { - weekPipelinesTotals: [], - weekPipelinesLabels: [], - weekPipelinesSuccessful: [], - monthPipelinesLabels: [], - monthPipelinesTotals: [], - monthPipelinesSuccessful: [], - yearPipelinesLabels: [], - yearPipelinesTotals: [], - yearPipelinesSuccessful: [], - pipelineTimesLabels: [], - pipelineTimesValues: [], -}; - -const defaultCountValues = { - totalPipelines: { - count: 0, - }, - successfulPipelines: { - count: 0, - }, -}; +const charts = ['pipelines', 'deployments']; export default { components: { - GlAlert, GlTabs, GlTab, PipelineCharts, @@ -50,171 +18,42 @@ export default { type: Boolean, default: false, }, - projectPath: { - type: String, - default: '', - }, }, data() { return { - showFailureAlert: false, - failureType: null, - analytics: { ...defaultAnalyticsValues }, - counts: { ...defaultCountValues }, + selectedTab: 0, }; }, - apollo: { - counts: { - query: getPipelineCountByStatus, - variables() { - return { - projectPath: this.projectPath, - }; - }, - update(data) { - return data?.project; - }, - error() { - this.reportFailure(LOAD_PIPELINES_FAILURE); - }, - }, - analytics: { - query: getProjectPipelineStatistics, - variables() { - return { - projectPath: this.projectPath, - }; - }, - update(data) { - return data?.project?.pipelineAnalytics; - }, - error() { - this.reportFailure(LOAD_ANALYTICS_FAILURE); - }, - }, - }, - computed: { - failure() { - switch (this.failureType) { - case LOAD_ANALYTICS_FAILURE: - return { - text: this.$options.errorTexts[LOAD_ANALYTICS_FAILURE], - variant: 'danger', - }; - case PARSE_FAILURE: - return { - text: this.$options.errorTexts[PARSE_FAILURE], - variant: 'danger', - }; - case UNSUPPORTED_DATA: - return { - text: this.$options.errorTexts[UNSUPPORTED_DATA], - variant: 'info', - }; - default: - return { - text: this.$options.errorTexts[DEFAULT], - variant: 'danger', - }; - } - }, - lastWeekChartData() { - return { - labels: this.analytics.weekPipelinesLabels, - totals: this.analytics.weekPipelinesTotals, - success: this.analytics.weekPipelinesSuccessful, - }; - }, - lastMonthChartData() { - return { - labels: this.analytics.monthPipelinesLabels, - totals: this.analytics.monthPipelinesTotals, - success: this.analytics.monthPipelinesSuccessful, - }; - }, - lastYearChartData() { - return { - labels: this.analytics.yearPipelinesLabels, - totals: this.analytics.yearPipelinesTotals, - success: this.analytics.yearPipelinesSuccessful, - }; - }, - timesChartData() { - return { - labels: this.analytics.pipelineTimesLabels, - values: this.analytics.pipelineTimesValues, - }; - }, - successRatio() { - const { successfulPipelines, failedPipelines } = this.counts; - const successfulCount = successfulPipelines?.count; - const failedCount = failedPipelines?.count; - const ratio = (successfulCount / (successfulCount + failedCount)) * 100; - - return failedCount === 0 ? 100 : ratio; - }, - formattedCounts() { - const { totalPipelines, successfulPipelines, failedPipelines } = this.counts; - - return { - total: totalPipelines?.count, - success: successfulPipelines?.count, - failed: failedPipelines?.count, - successRatio: this.successRatio, - }; - }, + created() { + this.selectTab(); + window.addEventListener('popstate', this.selectTab); }, methods: { - hideAlert() { - this.showFailureAlert = false; - }, - reportFailure(type) { - this.showFailureAlert = true; - this.failureType = type; + selectTab() { + const [chart] = getParameterValues('chart') || charts; + const tab = charts.indexOf(chart); + this.selectedTab = tab >= 0 ? tab : 0; + }, + onTabChange(index) { + if (index !== this.selectedTab) { + this.selectedTab = index; + const path = mergeUrlParams({ chart: charts[index] }, window.location.pathname); + updateHistory({ url: path, title: window.title }); + } }, }, - errorTexts: { - [LOAD_ANALYTICS_FAILURE]: s__( - 'PipelineCharts|An error has ocurred when retrieving the analytics data', - ), - [LOAD_PIPELINES_FAILURE]: s__( - 'PipelineCharts|An error has ocurred when retrieving the pipelines data', - ), - [PARSE_FAILURE]: s__('PipelineCharts|There was an error parsing the data for the charts.'), - [DEFAULT]: s__('PipelineCharts|An unknown error occurred while processing CI/CD analytics.'), - }, }; </script> <template> <div> - <gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="hideAlert">{{ - failure.text - }}</gl-alert> - <gl-tabs v-if="shouldRenderDeploymentFrequencyCharts"> + <gl-tabs v-if="shouldRenderDeploymentFrequencyCharts" :value="selectedTab" @input="onTabChange"> <gl-tab :title="__('Pipelines')"> - <pipeline-charts - :counts="formattedCounts" - :last-week="lastWeekChartData" - :last-month="lastMonthChartData" - :last-year="lastYearChartData" - :times-chart="timesChartData" - :loading="$apollo.queries.counts.loading" - @report-failure="reportFailure" - /> + <pipeline-charts /> </gl-tab> <gl-tab :title="__('Deployments')"> <deployment-frequency-charts /> </gl-tab> </gl-tabs> - <pipeline-charts - v-else - :counts="formattedCounts" - :last-week="lastWeekChartData" - :last-month="lastMonthChartData" - :last-year="lastYearChartData" - :times-chart="timesChartData" - :loading="$apollo.queries.counts.loading" - @report-failure="reportFailure" - /> + <pipeline-charts v-else /> </div> </template> diff --git a/app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_charts.vue b/app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_charts.vue new file mode 100644 index 00000000000..43b36da8b2c --- /dev/null +++ b/app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_charts.vue @@ -0,0 +1,50 @@ +<script> +import { GlSegmentedControl } from '@gitlab/ui'; +import { s__, sprintf } from '~/locale'; +import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue'; + +export default { + components: { + GlSegmentedControl, + CiCdAnalyticsAreaChart, + }, + props: { + charts: { + required: true, + type: Array, + }, + chartOptions: { + required: true, + type: Object, + }, + }, + data() { + return { + selectedChart: 0, + }; + }, + computed: { + chartRanges() { + return this.charts.map(({ title }, index) => ({ text: title, value: index })); + }, + chart() { + return this.charts[this.selectedChart]; + }, + dateRange() { + return sprintf(s__('CiCdAnalytics|Date range: %{range}'), { range: this.chart.range }); + }, + }, +}; +</script> +<template> + <div> + <gl-segmented-control v-model="selectedChart" :options="chartRanges" class="gl-mb-4" /> + <ci-cd-analytics-area-chart + v-if="chart" + :chart-data="chart.data" + :area-chart-options="chartOptions" + > + {{ dateRange }} + </ci-cd-analytics-area-chart> + </div> +</template> diff --git a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue index bec4ab407f0..733f833d51a 100644 --- a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue +++ b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue @@ -1,63 +1,184 @@ <script> -import dateFormat from 'dateformat'; +import { GlAlert, GlSkeletonLoader } from '@gitlab/ui'; import { GlColumnChart } from '@gitlab/ui/dist/charts'; -import { GlSkeletonLoader } from '@gitlab/ui'; -import { __, s__, sprintf } from '~/locale'; +import dateFormat from 'dateformat'; import { getDateInPast } from '~/lib/utils/datetime_utility'; +import { __, s__, sprintf } from '~/locale'; import { + DEFAULT, CHART_CONTAINER_HEIGHT, CHART_DATE_FORMAT, INNER_CHART_HEIGHT, ONE_WEEK_AGO_DAYS, ONE_MONTH_AGO_DAYS, + ONE_YEAR_AGO_DAYS, X_AXIS_LABEL_ROTATION, X_AXIS_TITLE_OFFSET, PARSE_FAILURE, + LOAD_ANALYTICS_FAILURE, + LOAD_PIPELINES_FAILURE, + UNSUPPORTED_DATA, } from '../constants'; +import getPipelineCountByStatus from '../graphql/queries/get_pipeline_count_by_status.query.graphql'; +import getProjectPipelineStatistics from '../graphql/queries/get_project_pipeline_statistics.query.graphql'; +import CiCdAnalyticsCharts from './ci_cd_analytics_charts.vue'; import StatisticsList from './statistics_list.vue'; -import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue'; + +const defaultAnalyticsValues = { + weekPipelinesTotals: [], + weekPipelinesLabels: [], + weekPipelinesSuccessful: [], + monthPipelinesLabels: [], + monthPipelinesTotals: [], + monthPipelinesSuccessful: [], + yearPipelinesLabels: [], + yearPipelinesTotals: [], + yearPipelinesSuccessful: [], + pipelineTimesLabels: [], + pipelineTimesValues: [], +}; + +const defaultCountValues = { + totalPipelines: { + count: 0, + }, + successfulPipelines: { + count: 0, + }, +}; export default { components: { + GlAlert, GlColumnChart, GlSkeletonLoader, StatisticsList, - CiCdAnalyticsAreaChart, + CiCdAnalyticsCharts, + }, + inject: { + projectPath: { + type: String, + default: '', + }, }, - props: { + data() { + return { + showFailureAlert: false, + failureType: null, + analytics: { ...defaultAnalyticsValues }, + counts: { ...defaultCountValues }, + }; + }, + apollo: { counts: { - required: true, - type: Object, + query: getPipelineCountByStatus, + variables() { + return { + projectPath: this.projectPath, + }; + }, + update(data) { + return data?.project; + }, + error() { + this.reportFailure(LOAD_PIPELINES_FAILURE); + }, + }, + analytics: { + query: getProjectPipelineStatistics, + variables() { + return { + projectPath: this.projectPath, + }; + }, + update(data) { + return data?.project?.pipelineAnalytics; + }, + error() { + this.reportFailure(LOAD_ANALYTICS_FAILURE); + }, + }, + }, + computed: { + loading() { + return this.$apollo.queries.counts.loading; }, - loading: { - required: false, - default: false, - type: Boolean, + failure() { + switch (this.failureType) { + case LOAD_ANALYTICS_FAILURE: + return { + text: this.$options.errorTexts[LOAD_ANALYTICS_FAILURE], + variant: 'danger', + }; + case PARSE_FAILURE: + return { + text: this.$options.errorTexts[PARSE_FAILURE], + variant: 'danger', + }; + case UNSUPPORTED_DATA: + return { + text: this.$options.errorTexts[UNSUPPORTED_DATA], + variant: 'info', + }; + default: + return { + text: this.$options.errorTexts[DEFAULT], + variant: 'danger', + }; + } }, - lastWeek: { - required: true, - type: Object, + lastWeekChartData() { + return { + labels: this.analytics.weekPipelinesLabels, + totals: this.analytics.weekPipelinesTotals, + success: this.analytics.weekPipelinesSuccessful, + }; }, - lastMonth: { - required: true, - type: Object, + lastMonthChartData() { + return { + labels: this.analytics.monthPipelinesLabels, + totals: this.analytics.monthPipelinesTotals, + success: this.analytics.monthPipelinesSuccessful, + }; }, - lastYear: { - required: true, - type: Object, + lastYearChartData() { + return { + labels: this.analytics.yearPipelinesLabels, + totals: this.analytics.yearPipelinesTotals, + success: this.analytics.yearPipelinesSuccessful, + }; }, - timesChart: { - required: true, - type: Object, + timesChartData() { + return { + labels: this.analytics.pipelineTimesLabels, + values: this.analytics.pipelineTimesValues, + }; + }, + successRatio() { + const { successfulPipelines, failedPipelines } = this.counts; + const successfulCount = successfulPipelines?.count; + const failedCount = failedPipelines?.count; + const ratio = (successfulCount / (successfulCount + failedCount)) * 100; + + return failedCount === 0 ? 100 : ratio; + }, + formattedCounts() { + const { totalPipelines, successfulPipelines, failedPipelines } = this.counts; + + return { + total: totalPipelines?.count, + success: successfulPipelines?.count, + failed: failedPipelines?.count, + successRatio: this.successRatio, + }; }, - }, - computed: { areaCharts() { const { lastWeek, lastMonth, lastYear } = this.$options.chartTitles; + const { lastWeekRange, lastMonthRange, lastYearRange } = this.$options.chartRanges; const charts = [ - { title: lastWeek, data: this.lastWeek }, - { title: lastMonth, data: this.lastMonth }, - { title: lastYear, data: this.lastYear }, + { title: lastWeek, range: lastWeekRange, data: this.lastWeekChartData }, + { title: lastMonth, range: lastMonthRange, data: this.lastMonthChartData }, + { title: lastYear, range: lastYearRange, data: this.lastYearChartData }, ]; let areaChartsData = []; @@ -65,7 +186,7 @@ export default { areaChartsData = charts.map(this.buildAreaChartData); } catch { areaChartsData = []; - this.vm.$emit('report-failure', PARSE_FAILURE); + this.reportFailure(PARSE_FAILURE); } return areaChartsData; @@ -74,20 +195,28 @@ export default { return [ { name: 'full', - data: this.mergeLabelsAndValues(this.timesChart.labels, this.timesChart.values), + data: this.mergeLabelsAndValues(this.timesChartData.labels, this.timesChartData.values), }, ]; }, }, methods: { + hideAlert() { + this.showFailureAlert = false; + }, + reportFailure(type) { + this.showFailureAlert = true; + this.failureType = type; + }, mergeLabelsAndValues(labels, values) { return labels.map((label, index) => [label, values[index]]); }, - buildAreaChartData({ title, data }) { + buildAreaChartData({ title, data, range }) { const { labels, totals, success } = data; return { title, + range, data: [ { name: 'all', @@ -118,28 +247,50 @@ export default { }, yAxis: { name: s__('Pipeline|Pipelines'), + minInterval: 1, }, }, - get chartTitles() { + errorTexts: { + [LOAD_ANALYTICS_FAILURE]: s__( + 'PipelineCharts|An error has ocurred when retrieving the analytics data', + ), + [LOAD_PIPELINES_FAILURE]: s__( + 'PipelineCharts|An error has ocurred when retrieving the pipelines data', + ), + [PARSE_FAILURE]: s__('PipelineCharts|There was an error parsing the data for the charts.'), + [DEFAULT]: s__('PipelineCharts|An unknown error occurred while processing CI/CD analytics.'), + }, + chartTitles: { + lastWeek: __('Last week'), + lastMonth: __('Last month'), + lastYear: __('Last year'), + }, + get chartRanges() { const today = dateFormat(new Date(), CHART_DATE_FORMAT); const pastDate = (timeScale) => dateFormat(getDateInPast(new Date(), timeScale), CHART_DATE_FORMAT); return { - lastWeek: sprintf(__('Pipelines for last week (%{oneWeekAgo} - %{today})'), { + lastWeekRange: sprintf(__('%{oneWeekAgo} - %{today}'), { oneWeekAgo: pastDate(ONE_WEEK_AGO_DAYS), today, }), - lastMonth: sprintf(__('Pipelines for last month (%{oneMonthAgo} - %{today})'), { + lastMonthRange: sprintf(__('%{oneMonthAgo} - %{today}'), { oneMonthAgo: pastDate(ONE_MONTH_AGO_DAYS), today, }), - lastYear: __('Pipelines for last year'), + lastYearRange: sprintf(__('%{oneYearAgo} - %{today}'), { + oneYearAgo: pastDate(ONE_YEAR_AGO_DAYS), + today, + }), }; }, }; </script> <template> <div> + <gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="hideAlert">{{ + failure.text + }}</gl-alert> <div class="gl-mb-3"> <h3>{{ s__('PipelineCharts|CI / CD Analytics') }}</h3> </div> @@ -147,7 +298,7 @@ export default { <div class="row"> <div class="col-md-6"> <gl-skeleton-loader v-if="loading" :lines="5" /> - <statistics-list v-else :counts="counts" /> + <statistics-list v-else :counts="formattedCounts" /> </div> <div v-if="!loading" class="col-md-6"> <strong>{{ __('Duration for the last 30 commits') }}</strong> @@ -164,13 +315,7 @@ export default { <template v-if="!loading"> <hr /> <h4 class="gl-my-4">{{ __('Pipelines charts') }}</h4> - <ci-cd-analytics-area-chart - v-for="(chart, index) in areaCharts" - :key="index" - :chart-data="chart.data" - :area-chart-options="$options.areaChartOptions" - >{{ chart.title }}</ci-cd-analytics-area-chart - > + <ci-cd-analytics-charts :charts="areaCharts" :chart-options="$options.areaChartOptions" /> </template> </div> </template> diff --git a/app/assets/javascripts/projects/pipelines/charts/constants.js b/app/assets/javascripts/projects/pipelines/charts/constants.js index 079e23943c1..41fe81f21ea 100644 --- a/app/assets/javascripts/projects/pipelines/charts/constants.js +++ b/app/assets/javascripts/projects/pipelines/charts/constants.js @@ -10,6 +10,8 @@ export const ONE_WEEK_AGO_DAYS = 7; export const ONE_MONTH_AGO_DAYS = 31; +export const ONE_YEAR_AGO_DAYS = 365; + export const CHART_DATE_FORMAT = 'dd mmm'; export const DEFAULT = 'default'; diff --git a/app/assets/javascripts/projects/settings/access_dropdown.js b/app/assets/javascripts/projects/settings/access_dropdown.js index a62b5d423de..a5e53ee3927 100644 --- a/app/assets/javascripts/projects/settings/access_dropdown.js +++ b/app/assets/javascripts/projects/settings/access_dropdown.js @@ -1,17 +1,16 @@ /* eslint-disable no-underscore-dangle, class-methods-use-this */ import { escape, find, countBy } from 'lodash'; -import axios from '~/lib/utils/axios_utils'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; import createFlash from '~/flash'; +import axios from '~/lib/utils/axios_utils'; import { n__, s__, __, sprintf } from '~/locale'; import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVELS, ACCESS_LEVEL_NONE } from './constants'; -import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default class AccessDropdown { constructor(options) { const { $dropdown, accessLevel, accessLevelsData, hasLicense = true } = options; this.options = options; this.hasLicense = hasLicense; - this.deployKeysOnProtectedBranchesEnabled = gon.features.deployKeysOnProtectedBranches; this.groups = []; this.accessLevel = accessLevel; this.accessLevelsData = accessLevelsData.roles; @@ -330,11 +329,7 @@ export default class AccessDropdown { ); }) .catch(() => { - if (this.deployKeysOnProtectedBranchesEnabled) { - createFlash({ message: __('Failed to load groups, users and deploy keys.') }); - } else { - createFlash({ message: __('Failed to load groups & users.') }); - } + createFlash({ message: __('Failed to load groups, users and deploy keys.') }); }); } else { this.getDeployKeys(query) @@ -445,35 +440,33 @@ export default class AccessDropdown { } } - if (this.deployKeysOnProtectedBranchesEnabled) { - const deployKeys = deployKeysResponse.map((response) => { - const { - id, - fingerprint, - title, - owner: { avatar_url, name, username }, - } = response; - - const shortFingerprint = `(${fingerprint.substring(0, 14)}...)`; - - return { - id, - title: title.concat(' ', shortFingerprint), - avatar_url, - fullname: name, - username, - type: LEVEL_TYPES.DEPLOY_KEY, - }; - }); + const deployKeys = deployKeysResponse.map((response) => { + const { + id, + fingerprint, + title, + owner: { avatar_url, name, username }, + } = response; + + const shortFingerprint = `(${fingerprint.substring(0, 14)}...)`; + + return { + id, + title: title.concat(' ', shortFingerprint), + avatar_url, + fullname: name, + username, + type: LEVEL_TYPES.DEPLOY_KEY, + }; + }); - if (this.accessLevel === ACCESS_LEVELS.PUSH) { - if (deployKeys.length) { - consolidatedData = consolidatedData.concat( - [{ type: 'divider' }], - [{ type: 'header', content: s__('AccessDropdown|Deploy Keys') }], - deployKeys, - ); - } + if (this.accessLevel === ACCESS_LEVELS.PUSH) { + if (deployKeys.length) { + consolidatedData = consolidatedData.concat( + [{ type: 'divider' }], + [{ type: 'header', content: s__('AccessDropdown|Deploy Keys') }], + deployKeys, + ); } } @@ -501,19 +494,15 @@ export default class AccessDropdown { } getDeployKeys(query) { - if (this.deployKeysOnProtectedBranchesEnabled) { - return axios.get(this.buildUrl(gon.relative_url_root, this.deployKeysPath), { - params: { - search: query, - per_page: 20, - active: true, - project_id: gon.current_project_id, - push_code: true, - }, - }); - } - - return Promise.resolve({ data: [] }); + return axios.get(this.buildUrl(gon.relative_url_root, this.deployKeysPath), { + params: { + search: query, + per_page: 20, + active: true, + project_id: gon.current_project_id, + push_code: true, + }, + }); } buildUrl(urlRoot, url) { diff --git a/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue b/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue index 51281def7d0..0786a74f6b1 100644 --- a/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue +++ b/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue @@ -1,7 +1,7 @@ <script> import { GlAlert, GlToggle, GlTooltip } from '@gitlab/ui'; -import { __ } from '~/locale'; import axios from '~/lib/utils/axios_utils'; +import { __ } from '~/locale'; const DEFAULT_ERROR_MESSAGE = __('An error occurred while updating the configuration.'); diff --git a/app/assets/javascripts/projects/settings/mount_shared_runners_toggle.js b/app/assets/javascripts/projects/settings/mount_shared_runners_toggle.js index c5d45fe6fed..eaeb5848b68 100644 --- a/app/assets/javascripts/projects/settings/mount_shared_runners_toggle.js +++ b/app/assets/javascripts/projects/settings/mount_shared_runners_toggle.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import SharedRunnersToggle from '~/projects/settings/components/shared_runners_toggle.vue'; import { parseBoolean } from '~/lib/utils/common_utils'; +import SharedRunnersToggle from '~/projects/settings/components/shared_runners_toggle.vue'; export default (containerId = 'toggle-shared-runners-form') => { const containerEl = document.getElementById(containerId); diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue index 909f1afd9f6..9b3c0dd2755 100644 --- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue +++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue @@ -1,61 +1,43 @@ <script> import { GlAlert } from '@gitlab/ui'; +import axios from '~/lib/utils/axios_utils'; import { __, sprintf } from '~/locale'; import ServiceDeskSetting from './service_desk_setting.vue'; -import ServiceDeskService from '../services/service_desk_service'; -import eventHub from '../event_hub'; export default { - name: 'ServiceDeskRoot', components: { GlAlert, ServiceDeskSetting, }, - props: { + inject: { initialIsEnabled: { - type: Boolean, - required: true, + default: false, }, endpoint: { - type: String, - required: true, + default: '', }, - incomingEmail: { - type: String, - required: false, + initialIncomingEmail: { default: '', }, customEmail: { - type: String, - required: false, default: '', }, customEmailEnabled: { - type: Boolean, - required: false, + default: false, }, selectedTemplate: { - type: String, - required: false, default: '', }, outgoingName: { - type: String, - required: false, default: '', }, projectKey: { - type: String, - required: false, default: '', }, templates: { - type: Array, - required: false, - default: () => [], + default: [], }, }, - data() { return { isEnabled: this.initialIsEnabled, @@ -63,28 +45,21 @@ export default { isAlertShowing: false, alertVariant: 'danger', alertMessage: '', + incomingEmail: this.initialIncomingEmail, updatedCustomEmail: this.customEmail, }; }, - - created() { - eventHub.$on('serviceDeskEnabledCheckboxToggled', this.onEnableToggled); - eventHub.$on('serviceDeskTemplateSave', this.onSaveTemplate); - this.service = new ServiceDeskService(this.endpoint); - }, - - beforeDestroy() { - eventHub.$off('serviceDeskEnabledCheckboxToggled', this.onEnableToggled); - eventHub.$off('serviceDeskTemplateSave', this.onSaveTemplate); - }, - methods: { onEnableToggled(isChecked) { this.isEnabled = isChecked; this.incomingEmail = ''; - this.service - .toggleServiceDesk(isChecked) + const body = { + service_desk_enabled: isChecked, + }; + + return axios + .put(this.endpoint, body) .then(({ data }) => { const email = data.service_desk_address; if (isChecked && !email) { @@ -104,8 +79,16 @@ export default { onSaveTemplate({ selectedTemplate, outgoingName, projectKey }) { this.isTemplateSaving = true; - this.service - .updateTemplate({ selectedTemplate, outgoingName, projectKey }, this.isEnabled) + + const body = { + issue_template_key: selectedTemplate, + outgoing_name: outgoingName, + project_key: projectKey, + service_desk_enabled: this.isEnabled, + }; + + return axios + .put(this.endpoint, body) .then(({ data }) => { this.updatedCustomEmail = data?.service_desk_address; this.showAlert(__('Changes saved.'), 'success'); @@ -150,6 +133,8 @@ export default { :initial-project-key="projectKey" :templates="templates" :is-template-saving="isTemplateSaving" + @save="onSaveTemplate" + @toggle="onEnableToggled" /> </div> </template> diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue index a850374fc88..39d9a6a4239 100644 --- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue +++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue @@ -1,12 +1,9 @@ <script> import { GlButton, GlFormSelect, GlToggle, GlLoadingIcon, GlSprintf } from '@gitlab/ui'; import { __ } from '~/locale'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; -import eventHub from '../event_hub'; export default { - name: 'ServiceDeskSetting', components: { ClipboardButton, GlButton, @@ -15,7 +12,6 @@ export default { GlLoadingIcon, GlSprintf, }, - mixins: [glFeatureFlagsMixin()], props: { isEnabled: { type: Boolean, @@ -84,10 +80,10 @@ export default { }, methods: { onCheckboxToggle(isChecked) { - eventHub.$emit('serviceDeskEnabledCheckboxToggled', isChecked); + this.$emit('toggle', isChecked); }, onSaveTemplate() { - eventHub.$emit('serviceDeskTemplateSave', { + this.$emit('save', { selectedTemplate: this.selectedTemplate, outgoingName: this.outgoingName, projectKey: this.projectKey, @@ -111,7 +107,11 @@ export default { </label> <div v-if="isEnabled" class="row mt-3"> <div class="col-md-9 mb-0"> - <strong id="incoming-email-describer" class="d-block mb-1"> + <strong + id="incoming-email-describer" + class="gl-display-block gl-mb-1" + data-testid="incoming-email-describer" + > {{ __('Email address to use for Support Desk') }} </strong> <template v-if="email"> @@ -128,11 +128,7 @@ export default { disabled="true" /> <div class="input-group-append"> - <clipboard-button - :title="__('Copy')" - :text="email" - css-class="input-group-text qa-clipboard-button" - /> + <clipboard-button :title="__('Copy')" :text="email" css-class="input-group-text" /> </div> </div> <span v-if="hasCustomEmail" class="form-text text-muted"> diff --git a/app/assets/javascripts/projects/settings_service_desk/event_hub.js b/app/assets/javascripts/projects/settings_service_desk/event_hub.js deleted file mode 100644 index e31806ad199..00000000000 --- a/app/assets/javascripts/projects/settings_service_desk/event_hub.js +++ /dev/null @@ -1,3 +0,0 @@ -import createEventHub from '~/helpers/event_hub_factory'; - -export default createEventHub(); diff --git a/app/assets/javascripts/projects/settings_service_desk/index.js b/app/assets/javascripts/projects/settings_service_desk/index.js index 8f9828dd73d..f842ffaaa2b 100644 --- a/app/assets/javascripts/projects/settings_service_desk/index.js +++ b/app/assets/javascripts/projects/settings_service_desk/index.js @@ -3,43 +3,37 @@ import { parseBoolean } from '~/lib/utils/common_utils'; import ServiceDeskRoot from './components/service_desk_root.vue'; export default () => { - const serviceDeskRootElement = document.querySelector('.js-service-desk-setting-root'); - if (serviceDeskRootElement) { - // eslint-disable-next-line no-new - new Vue({ - el: serviceDeskRootElement, - components: { - ServiceDeskRoot, - }, - data() { - const { dataset } = serviceDeskRootElement; - return { - initialIsEnabled: parseBoolean(dataset.enabled), - endpoint: dataset.endpoint, - incomingEmail: dataset.incomingEmail, - customEmail: dataset.customEmail, - customEmailEnabled: parseBoolean(dataset.customEmailEnabled), - selectedTemplate: dataset.selectedTemplate, - outgoingName: dataset.outgoingName, - projectKey: dataset.projectKey, - templates: JSON.parse(dataset.templates), - }; - }, - render(createElement) { - return createElement('service-desk-root', { - props: { - initialIsEnabled: this.initialIsEnabled, - endpoint: this.endpoint, - incomingEmail: this.incomingEmail, - customEmail: this.customEmail, - customEmailEnabled: this.customEmailEnabled, - selectedTemplate: this.selectedTemplate, - outgoingName: this.outgoingName, - projectKey: this.projectKey, - templates: this.templates, - }, - }); - }, - }); + const el = document.querySelector('.js-service-desk-setting-root'); + + if (!el) { + return false; } + + const { + customEmail, + customEmailEnabled, + enabled, + endpoint, + incomingEmail, + outgoingName, + projectKey, + selectedTemplate, + templates, + } = el.dataset; + + return new Vue({ + el, + provide: { + customEmail, + customEmailEnabled: parseBoolean(customEmailEnabled), + endpoint, + initialIncomingEmail: incomingEmail, + initialIsEnabled: parseBoolean(enabled), + outgoingName, + projectKey, + selectedTemplate, + templates: JSON.parse(templates), + }, + render: (createElement) => createElement(ServiceDeskRoot), + }); }; diff --git a/app/assets/javascripts/projects/settings_service_desk/services/service_desk_service.js b/app/assets/javascripts/projects/settings_service_desk/services/service_desk_service.js deleted file mode 100644 index b68c5bb876f..00000000000 --- a/app/assets/javascripts/projects/settings_service_desk/services/service_desk_service.js +++ /dev/null @@ -1,23 +0,0 @@ -import axios from '~/lib/utils/axios_utils'; - -class ServiceDeskService { - constructor(endpoint) { - this.endpoint = endpoint; - } - - toggleServiceDesk(enable) { - return axios.put(this.endpoint, { service_desk_enabled: enable }); - } - - updateTemplate({ selectedTemplate, outgoingName, projectKey = '' }, isEnabled) { - const body = { - issue_template_key: selectedTemplate, - outgoing_name: outgoingName, - project_key: projectKey, - service_desk_enabled: isEnabled, - }; - return axios.put(this.endpoint, body); - } -} - -export default ServiceDeskService; diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue index 0f01167988d..f3d12e0dd00 100644 --- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue +++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue @@ -1,10 +1,10 @@ <script> -import Visibility from 'visibilityjs'; import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; -import ciIcon from '~/vue_shared/components/ci_icon.vue'; -import Poll from '~/lib/utils/poll'; +import Visibility from 'visibilityjs'; import { deprecatedCreateFlash as Flash } from '~/flash'; +import Poll from '~/lib/utils/poll'; import { __, s__, sprintf } from '~/locale'; +import ciIcon from '~/vue_shared/components/ci_icon.vue'; import CommitPipelineService from '../services/commit_pipeline_service'; export default { |