diff options
Diffstat (limited to 'app/assets/javascripts/ide/components')
34 files changed, 712 insertions, 230 deletions
diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue index 186d4b6d7d2..a65af55fcac 100644 --- a/app/assets/javascripts/ide/components/activity_bar.vue +++ b/app/assets/javascripts/ide/components/activity_bar.vue @@ -1,6 +1,6 @@ <script> import $ from 'jquery'; -import { mapActions, mapGetters, mapState } from 'vuex'; +import { mapActions, mapState } from 'vuex'; import Icon from '~/vue_shared/components/icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; import { leftSidebarViews } from '../constants'; @@ -13,7 +13,6 @@ export default { tooltip, }, computed: { - ...mapGetters(['hasChanges']), ...mapState(['currentActivityView']), }, methods: { @@ -23,6 +22,8 @@ export default { this.updateActivityBarView(view); + // TODO: We must use JQuery here to interact with the Bootstrap tooltip API + // https://gitlab.com/gitlab-org/gitlab/-/issues/217577 $(e.currentTarget).tooltip('hide'); }, }, @@ -67,7 +68,7 @@ export default { <icon name="file-modified" /> </button> </li> - <li v-show="hasChanges"> + <li> <button v-tooltip :class="{ diff --git a/app/assets/javascripts/ide/components/branches/item.vue b/app/assets/javascripts/ide/components/branches/item.vue index 58a0631ee0d..e7f4cd796b5 100644 --- a/app/assets/javascripts/ide/components/branches/item.vue +++ b/app/assets/javascripts/ide/components/branches/item.vue @@ -2,7 +2,6 @@ /* eslint-disable @gitlab/vue-require-i18n-strings */ import Icon from '~/vue_shared/components/icon.vue'; import Timeago from '~/vue_shared/components/time_ago_tooltip.vue'; -import router from '../../ide_router'; export default { components: { @@ -26,7 +25,7 @@ export default { }, computed: { branchHref() { - return router.resolve(`/project/${this.projectId}/edit/${this.item.name}`).href; + return this.$router.resolve(`/project/${this.projectId}/edit/${this.item.name}`).href; }, }, }; diff --git a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue index 24499fb9f6d..59a32dd477e 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue @@ -29,7 +29,7 @@ export default { }, }, methods: { - ...mapActions(['stageChange', 'unstageChange', 'discardFileChanges']), + ...mapActions(['unstageChange', 'discardFileChanges']), showDiscardModal() { this.$refs.discardModal.show(); }, @@ -56,7 +56,7 @@ export default { v-if="canDiscard" ref="discardButton" type="button" - class="btn btn-remove btn-inverted append-right-8" + class="btn btn-remove btn-inverted gl-mr-3" @click="showDiscardModal" > {{ __('Discard changes') }} diff --git a/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue b/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue index a23bae8e4c7..a13ca0cd138 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue @@ -9,10 +9,7 @@ export default { </script> <template> - <div - v-if="!lastCommitMsg" - class="multi-file-commit-panel-section ide-commit-empty-state js-empty-state" - > + <div v-if="!lastCommitMsg" class="multi-file-commit-panel-section ide-commit-empty-state"> <div class="ide-commit-empty-state-container"> <div class="svg-content svg-80"><img :src="noChangesStateSvgPath" /></div> <div class="append-right-default prepend-left-default"> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue index 4cbd33e6ed6..3bba4fbc906 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue @@ -26,7 +26,7 @@ export default { computed: { ...mapState(['changedFiles', 'stagedFiles', 'currentActivityView', 'lastCommitMsg']), ...mapState('commit', ['commitMessage', 'submitCommitLoading']), - ...mapGetters(['hasChanges']), + ...mapGetters(['someUncommittedChanges']), ...mapGetters('commit', ['discardDraftButtonDisabled', 'preBuiltCommitMessage']), overviewText() { return n__('%d changed file', '%d changed files', this.stagedFiles.length); @@ -40,20 +40,9 @@ export default { }, }, watch: { - currentActivityView() { - if (this.lastCommitMsg) { - this.isCompact = false; - } else { - this.isCompact = !( - this.currentViewIsCommitView && window.innerHeight >= MAX_WINDOW_HEIGHT_COMPACT - ); - } - }, - - lastCommitMsg() { - this.isCompact = - this.currentActivityView !== leftSidebarViews.commit.name && this.lastCommitMsg === ''; - }, + currentActivityView: 'handleCompactState', + someUncommittedChanges: 'handleCompactState', + lastCommitMsg: 'handleCompactState', }, methods: { ...mapActions(['updateActivityBarView']), @@ -71,19 +60,24 @@ export default { forceCreateNewBranch() { return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commit()); }, - toggleIsCompact() { - if (this.currentViewIsCommitView) { - this.isCompact = !this.isCompact; + handleCompactState() { + if (this.lastCommitMsg) { + this.isCompact = false; } else { - this.updateActivityBarView(leftSidebarViews.commit.name) - .then(() => { - this.isCompact = false; - }) - .catch(e => { - throw e; - }); + this.isCompact = + !this.someUncommittedChanges || + !this.currentViewIsCommitView || + window.innerHeight < MAX_WINDOW_HEIGHT_COMPACT; } }, + toggleIsCompact() { + this.isCompact = !this.isCompact; + }, + beginCommit() { + return this.updateActivityBarView(leftSidebarViews.commit.name).then(() => { + this.isCompact = false; + }); + }, beforeEnterTransition() { const elHeight = this.isCompact ? this.$refs.formEl && this.$refs.formEl.offsetHeight @@ -126,16 +120,17 @@ export default { > <div v-if="isCompact" ref="compactEl" class="commit-form-compact"> <button - :disabled="!hasChanges" + :disabled="!someUncommittedChanges" type="button" class="btn btn-primary btn-sm btn-block qa-begin-commit-button" - @click="toggleIsCompact" + data-testid="begin-commit-button" + @click="beginCommit" > {{ __('Commit…') }} </button> <p class="text-center bold">{{ overviewText }}</p> </div> - <form v-if="!isCompact" ref="formEl" @submit.prevent.stop="commit"> + <form v-else ref="formEl" @submit.prevent.stop="commit"> <transition name="fade"> <success-message v-show="lastCommitMsg" /> </transition> <commit-message-field :text="commitMessage" diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue index e6a1a1ba73c..5cff1079eb0 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue @@ -55,7 +55,7 @@ export default { }, }, methods: { - ...mapActions(['stageAllChanges', 'unstageAllChanges', 'discardAllChanges']), + ...mapActions(['unstageAllChanges', 'discardAllChanges']), openDiscardModal() { this.$refs.discardAllModal.show(); }, @@ -74,7 +74,7 @@ export default { <div class="ide-commit-list-container"> <header class="multi-file-commit-panel-header d-flex mb-0"> <div class="d-flex align-items-center flex-fill"> - <icon v-once :name="iconName" :size="18" class="append-right-8" /> + <icon v-once :name="iconName" :size="18" class="gl-mr-3" /> <strong> {{ titleText }} </strong> <div class="d-flex ml-auto"> <button @@ -98,7 +98,7 @@ export default { </div> </div> </header> - <ul v-if="filesLength" class="multi-file-commit-list list-unstyled append-bottom-0"> + <ul v-if="filesLength" class="multi-file-commit-list list-unstyled gl-mb-0"> <li v-for="file in fileList" :key="file.key"> <list-item :file="file" diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue index e70e251c117..c65169f5d31 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue @@ -4,7 +4,7 @@ import tooltip from '~/vue_shared/directives/tooltip'; import Icon from '~/vue_shared/components/icon.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue'; import { viewerTypes } from '../../constants'; -import { getCommitIconMap } from '../../utils'; +import getCommitIconMap from '../../commit_icon'; export default { components: { @@ -87,7 +87,7 @@ export default { @click="openFileInEditor" > <span class="multi-file-commit-list-file-path d-flex align-items-center"> - <file-icon :file-name="file.name" class="append-right-8" /> + <file-icon :file-name="file.name" class="gl-mr-3" /> <template v-if="file.prevName && file.prevName !== file.name"> {{ file.prevName }} → </template> diff --git a/app/assets/javascripts/ide/components/file_row_extra.vue b/app/assets/javascripts/ide/components/file_row_extra.vue index 32822a75772..51509cd5fe6 100644 --- a/app/assets/javascripts/ide/components/file_row_extra.vue +++ b/app/assets/javascripts/ide/components/file_row_extra.vue @@ -89,7 +89,7 @@ export default { :type="file.type" :path="file.path" :is-open="dropdownOpen" - class="prepend-left-8" + class="gl-ml-3" v-on="$listeners" /> </div> diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 36c8b18e205..e9f84eb8648 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -1,5 +1,4 @@ <script> -import Vue from 'vue'; import { mapActions, mapGetters, mapState } from 'vuex'; import { GlDeprecatedButton, GlLoadingIcon } from '@gitlab/ui'; import { __ } from '~/locale'; @@ -27,20 +26,13 @@ export default { CommitEditorHeader, GlDeprecatedButton, GlLoadingIcon, + RightPane, }, mixins: [glFeatureFlagsMixin()], - props: { - rightPaneComponent: { - type: Vue.Component, - required: false, - default: () => RightPane, - }, - }, computed: { ...mapState([ 'openFiles', 'viewer', - 'currentMergeRequestId', 'fileFindVisible', 'emptyStateSvgPath', 'currentProjectId', @@ -49,7 +41,6 @@ export default { ]), ...mapGetters([ 'activeFile', - 'hasChanges', 'someUncommittedChanges', 'isCommitModeActive', 'allBlobs', @@ -108,14 +99,7 @@ export default { <div class="multi-file-edit-pane"> <template v-if="activeFile"> <commit-editor-header v-if="isCommitModeActive" :active-file="activeFile" /> - <repo-tabs - v-else - :active-file="activeFile" - :files="openFiles" - :viewer="viewer" - :has-changes="hasChanges" - :merge-request-id="currentMergeRequestId" - /> + <repo-tabs v-else :active-file="activeFile" :files="openFiles" :viewer="viewer" /> <repo-editor :file="activeFile" class="multi-file-edit-pane-content" /> </template> <template v-else> @@ -141,6 +125,7 @@ export default { variant="success" :title="__('New file')" :aria-label="__('New file')" + data-qa-selector="first_file_button" @click="createNewFile()" > {{ __('New file') }} @@ -160,7 +145,7 @@ export default { </div> </template> </div> - <component :is="rightPaneComponent" v-if="currentProjectId" /> + <right-pane v-if="currentProjectId" /> </div> <ide-status-bar /> <new-modal ref="newModal" /> diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue index 7cb31df85ce..1eb89b41495 100644 --- a/app/assets/javascripts/ide/components/ide_side_bar.vue +++ b/app/assets/javascripts/ide/components/ide_side_bar.vue @@ -9,7 +9,7 @@ import CommitForm from './commit_sidebar/form.vue'; import IdeReview from './ide_review.vue'; import SuccessMessage from './commit_sidebar/success_message.vue'; import IdeProjectHeader from './ide_project_header.vue'; -import { leftSidebarViews } from '../constants'; +import { leftSidebarViews, SIDEBAR_INIT_WIDTH } from '../constants'; export default { components: { @@ -33,11 +33,16 @@ export default { ); }, }, + SIDEBAR_INIT_WIDTH, }; </script> <template> - <resizable-panel :initial-width="340" side="left" class="flex-column"> + <resizable-panel + :initial-width="$options.SIDEBAR_INIT_WIDTH" + side="left" + class="multi-file-commit-panel flex-column" + > <template v-if="loading"> <div class="multi-file-commit-panel-inner"> <div v-for="n in 3" :key="n" class="multi-file-loading-container"> diff --git a/app/assets/javascripts/ide/components/ide_sidebar_nav.vue b/app/assets/javascripts/ide/components/ide_sidebar_nav.vue new file mode 100644 index 00000000000..966c36d6e71 --- /dev/null +++ b/app/assets/javascripts/ide/components/ide_sidebar_nav.vue @@ -0,0 +1,83 @@ +<script> +import { GlTooltipDirective, GlIcon } from '@gitlab/ui'; +import { otherSide } from '../utils'; +import { SIDE_RIGHT } from '../constants'; + +export default { + directives: { + tooltip: GlTooltipDirective, + }, + components: { + GlIcon, + }, + props: { + tabs: { + type: Array, + required: true, + }, + side: { + type: String, + required: true, + }, + currentView: { + type: String, + required: false, + default: '', + }, + isOpen: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + otherSide() { + return otherSide(this.side); + }, + }, + methods: { + isActiveTab(tab) { + return this.isOpen && tab.views.some(view => view.name === this.currentView); + }, + buttonClasses(tab) { + return [ + { + 'is-right': this.side === SIDE_RIGHT, + active: this.isActiveTab(tab), + }, + ...(tab.buttonClasses || []), + ]; + }, + clickTab(e, tab) { + e.currentTarget.blur(); + this.$root.$emit('bv::hide::tooltip'); + + if (this.isActiveTab(tab)) { + this.$emit('close'); + } else { + this.$emit('open', tab.views[0]); + } + }, + }, +}; +</script> +<template> + <nav class="ide-activity-bar"> + <ul class="list-unstyled"> + <li v-for="tab of tabs" :key="tab.title"> + <button + v-tooltip="{ container: 'body', placement: otherSide }" + :title="tab.title" + :aria-label="tab.title" + :class="buttonClasses(tab)" + :data-qa-selector="`${tab.title.toLowerCase()}_tab_button`" + class="ide-sidebar-link" + type="button" + @click="clickTab($event, tab)" + > + <gl-icon :size="16" :name="tab.icon" /> + </button> + </li> + </ul> + </nav> +</template> diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue index 5585343f367..ddc126c3d77 100644 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -1,7 +1,7 @@ <script> /* eslint-disable @gitlab/vue-require-i18n-strings */ import { mapActions, mapState, mapGetters } from 'vuex'; -import IdeStatusList from 'ee_else_ce/ide/components/ide_status_list.vue'; +import IdeStatusList from './ide_status_list.vue'; import IdeStatusMr from './ide_status_mr.vue'; import icon from '~/vue_shared/components/icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; diff --git a/app/assets/javascripts/ide/components/ide_status_list.vue b/app/assets/javascripts/ide/components/ide_status_list.vue index 364e3f081a1..92d25709bd5 100644 --- a/app/assets/javascripts/ide/components/ide_status_list.vue +++ b/app/assets/javascripts/ide/components/ide_status_list.vue @@ -1,9 +1,17 @@ <script> import { mapGetters } from 'vuex'; +import TerminalSyncStatusSafe from './terminal_sync/terminal_sync_status_safe.vue'; +import { getFileEOL } from '../utils'; export default { + components: { + TerminalSyncStatusSafe, + }, computed: { ...mapGetters(['activeFile']), + activeFileEOL() { + return getFileEOL(this.activeFile.content); + }, }, }; </script> @@ -12,12 +20,12 @@ export default { <div class="ide-status-list d-flex"> <template v-if="activeFile"> <div class="ide-status-file">{{ activeFile.name }}</div> - <div class="ide-status-file">{{ activeFile.eol }}</div> + <div class="ide-status-file">{{ activeFileEOL }}</div> <div v-if="!activeFile.binary" class="ide-status-file"> {{ activeFile.editorRow }}:{{ activeFile.editorColumn }} </div> <div class="ide-status-file">{{ activeFile.fileLanguage }}</div> </template> - <slot></slot> + <terminal-sync-status-safe /> </div> </template> diff --git a/app/assets/javascripts/ide/components/jobs/detail/description.vue b/app/assets/javascripts/ide/components/jobs/detail/description.vue index 9c0c97bc5ae..f1ba102fffe 100644 --- a/app/assets/javascripts/ide/components/jobs/detail/description.vue +++ b/app/assets/javascripts/ide/components/jobs/detail/description.vue @@ -24,7 +24,7 @@ export default { <template> <div class="d-flex align-items-center"> <ci-icon :status="job.status" :borderless="true" :size="24" class="d-flex" /> - <span class="prepend-left-8"> + <span class="gl-ml-3"> {{ job.name }} <a :href="job.path" target="_blank" class="ide-external-link position-relative"> {{ jobId }} <icon :size="12" name="external-link" /> diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue index ba8407382f4..169a948c2da 100644 --- a/app/assets/javascripts/ide/components/jobs/stage.vue +++ b/app/assets/javascripts/ide/components/jobs/stage.vue @@ -71,11 +71,11 @@ export default { v-tooltip="showTooltip" :title="showTooltip ? stage.name : null" data-container="body" - class="prepend-left-8 text-truncate" + class="gl-ml-3 text-truncate" > {{ stage.name }} </strong> - <div v-if="!stage.isLoading || stage.jobs.length" class="append-right-8 prepend-left-4"> + <div v-if="!stage.isLoading || stage.jobs.length" class="gl-mr-3 gl-ml-2"> <span class="badge badge-pill"> {{ jobsCount }} </span> </div> <icon :name="collapseIcon" class="ide-stage-collapse-icon" /> diff --git a/app/assets/javascripts/ide/components/merge_requests/item.vue b/app/assets/javascripts/ide/components/merge_requests/item.vue index 60889c893cf..3f060392686 100644 --- a/app/assets/javascripts/ide/components/merge_requests/item.vue +++ b/app/assets/javascripts/ide/components/merge_requests/item.vue @@ -1,6 +1,5 @@ <script> import Icon from '../../../vue_shared/components/icon.vue'; -import router from '../../ide_router'; export default { components: { @@ -33,7 +32,7 @@ export default { mergeRequestHref() { const path = `/project/${this.item.projectPathWithNamespace}/merge_requests/${this.item.iid}`; - return router.resolve(path).href; + return this.$router.resolve(path).href; }, }, }; diff --git a/app/assets/javascripts/ide/components/mr_file_icon.vue b/app/assets/javascripts/ide/components/mr_file_icon.vue index cf8a1abbde4..4fab57b6f3e 100644 --- a/app/assets/javascripts/ide/components/mr_file_icon.vue +++ b/app/assets/javascripts/ide/components/mr_file_icon.vue @@ -18,6 +18,6 @@ export default { :title="__('Part of merge request changes')" :size="12" name="git-merge" - class="append-right-8" + class="gl-mr-3" /> </template> diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index 4766a2fe6ae..586d6867ab4 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -4,7 +4,7 @@ import flash from '~/flash'; import { __, sprintf, s__ } from '~/locale'; import { GlModal } from '@gitlab/ui'; import { modalTypes } from '../../constants'; -import { trimPathComponents } from '../../utils'; +import { trimPathComponents, getPathParent } from '../../utils'; export default { components: { @@ -85,8 +85,10 @@ export default { } }, createFromTemplate(template) { + const parent = getPathParent(this.entryName); + const name = parent ? `${parent}/${template.name}` : template.name; this.createTempEntry({ - name: template.name, + name, type: this.modalType, }); @@ -133,7 +135,7 @@ export default { <gl-modal ref="modal" modal-id="ide-new-entry" - modal-class="qa-new-file-modal" + data-qa-selector="new_file_modal" :title="modalTitle" :ok-title="buttonLabel" ok-variant="success" @@ -148,7 +150,8 @@ export default { ref="fieldName" v-model.trim="entryName" type="text" - class="form-control qa-full-file-path" + class="form-control" + data-qa-selector="file_name_field" :placeholder="placeholder" /> <ul diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue index 7261e0590c8..b2141c13d9f 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue @@ -35,7 +35,6 @@ export default { name: `${this.path ? `${this.path}/` : ''}${name}`, type: 'blob', content, - base64: !isText, binary: !isText, rawPath: !isText ? target.result : '', }); diff --git a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue index 91e80be7d18..4e8e1e3a470 100644 --- a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue +++ b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue @@ -2,8 +2,7 @@ import { mapActions, mapState } from 'vuex'; import tooltip from '~/vue_shared/directives/tooltip'; import Icon from '~/vue_shared/components/icon.vue'; -import ResizablePanel from '../resizable_panel.vue'; -import { GlSkeletonLoading } from '@gitlab/ui'; +import IdeSidebarNav from '../ide_sidebar_nav.vue'; export default { name: 'CollapsibleSidebar', @@ -12,8 +11,7 @@ export default { }, components: { Icon, - ResizablePanel, - GlSkeletonLoading, + IdeSidebarNav, }, props: { extensionTabs: { @@ -25,13 +23,8 @@ export default { type: String, required: true, }, - width: { - type: Number, - required: true, - }, }, computed: { - ...mapState(['loading']), ...mapState({ isOpen(state) { return state[this.namespace].isOpen; @@ -39,9 +32,6 @@ export default { currentView(state) { return state[this.namespace].currentView; }, - isActiveView(state, getters) { - return getters[`${this.namespace}/isActiveView`]; - }, isAliveView(_state, getters) { return getters[`${this.namespace}/isAliveView`]; }, @@ -59,9 +49,6 @@ export default { aliveTabViews() { return this.tabViews.filter(view => this.isAliveView(view.name)); }, - otherSide() { - return this.side === 'right' ? 'left' : 'right'; - }, }, methods: { ...mapActions({ @@ -72,25 +59,6 @@ export default { return dispatch(`${this.namespace}/open`, view); }, }), - clickTab(e, tab) { - e.target.blur(); - - if (this.isActiveTab(tab)) { - this.toggleOpen(); - } else { - this.open(tab.views[0]); - } - }, - isActiveTab(tab) { - return tab.views.some(view => this.isActiveView(view.name)); - }, - buttonClasses(tab) { - return [ - this.side === 'right' ? 'is-right' : '', - this.isActiveTab(tab) && this.isOpen ? 'active' : '', - ...(tab.buttonClasses || []), - ]; - }, }, }; </script> @@ -101,49 +69,27 @@ export default { :data-qa-selector="`ide_${side}_sidebar`" class="multi-file-commit-panel ide-sidebar" > - <resizable-panel + <div v-show="isOpen" - :initial-width="width" - :min-size="width" :class="`ide-${side}-sidebar-${currentView}`" - :side="side" class="multi-file-commit-panel-inner" > - <div class="h-100 d-flex flex-column align-items-stretch"> - <slot v-if="isOpen" name="header"></slot> - <div - v-for="tabView in aliveTabViews" - v-show="isActiveView(tabView.name)" - :key="tabView.name" - class="flex-fill gl-overflow-hidden js-tab-view" - > - <component :is="tabView.component" /> - </div> - <slot name="footer"></slot> + <div + v-for="tabView in aliveTabViews" + v-show="tabView.name === currentView" + :key="tabView.name" + class="flex-fill gl-overflow-hidden js-tab-view gl-h-full" + > + <component :is="tabView.component" /> </div> - </resizable-panel> - <nav class="ide-activity-bar"> - <ul class="list-unstyled"> - <li> - <slot name="header-icon"></slot> - </li> - <li v-for="tab of tabs" :key="tab.title"> - <button - v-tooltip - :title="tab.title" - :aria-label="tab.title" - :class="buttonClasses(tab)" - data-container="body" - :data-placement="otherSide" - :data-qa-selector="`${tab.title.toLowerCase()}_tab_button`" - class="ide-sidebar-link" - type="button" - @click="clickTab($event, tab)" - > - <icon :size="16" :name="tab.icon" /> - </button> - </li> - </ul> - </nav> + </div> + <ide-sidebar-nav + :tabs="tabs" + :side="side" + :current-view="currentView" + :is-open="isOpen" + @open="open" + @close="toggleOpen" + /> </div> </template> diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue index 4a9de9e0c03..46ef08a45a9 100644 --- a/app/assets/javascripts/ide/components/panes/right.vue +++ b/app/assets/javascripts/ide/components/panes/right.vue @@ -2,26 +2,27 @@ import { mapGetters, mapState } from 'vuex'; import { __ } from '~/locale'; import CollapsibleSidebar from './collapsible_sidebar.vue'; -import { rightSidebarViews } from '../../constants'; +import ResizablePanel from '../resizable_panel.vue'; +import { rightSidebarViews, SIDEBAR_INIT_WIDTH, SIDEBAR_NAV_WIDTH } from '../../constants'; import PipelinesList from '../pipelines/list.vue'; import JobsDetail from '../jobs/detail.vue'; import Clientside from '../preview/clientside.vue'; +import TerminalView from '../terminal/view.vue'; + +// Need to add the width of the nav buttons since the resizable container contains those as well +const WIDTH = SIDEBAR_INIT_WIDTH + SIDEBAR_NAV_WIDTH; export default { name: 'RightPane', components: { CollapsibleSidebar, - }, - props: { - extensionTabs: { - type: Array, - required: false, - default: () => [], - }, + ResizablePanel, }, computed: { + ...mapState('terminal', { isTerminalVisible: 'isVisible' }), ...mapState(['currentMergeRequestId', 'clientsidePreviewEnabled']), ...mapGetters(['packageJson']), + ...mapState('rightPane', ['isOpen']), showLivePreview() { return this.packageJson && this.clientsidePreviewEnabled; }, @@ -42,13 +43,27 @@ export default { views: [{ component: Clientside, ...rightSidebarViews.clientSidePreview }], icon: 'live-preview', }, - ...this.extensionTabs, + { + show: this.isTerminalVisible, + title: __('Terminal'), + views: [{ component: TerminalView, ...rightSidebarViews.terminal }], + icon: 'terminal', + }, ]; }, }, + WIDTH, }; </script> <template> - <collapsible-sidebar :extension-tabs="rightExtensionTabs" side="right" :width="350" /> + <resizable-panel + class="gl-display-flex gl-overflow-hidden" + side="right" + :initial-width="$options.WIDTH" + :min-size="$options.WIDTH" + :resizable="isOpen" + > + <collapsible-sidebar class="gl-w-full" :extension-tabs="rightExtensionTabs" side="right" /> + </resizable-panel> </template> diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue index cf6d01b6351..6958a5d2526 100644 --- a/app/assets/javascripts/ide/components/pipelines/list.vue +++ b/app/assets/javascripts/ide/components/pipelines/list.vue @@ -63,7 +63,7 @@ export default { <template v-else-if="hasLoadedPipeline"> <header v-if="latestPipeline" class="ide-tree-header ide-pipeline-header"> <ci-icon :status="latestPipeline.details.status" :size="24" class="d-flex" /> - <span class="prepend-left-8"> + <span class="gl-ml-3"> <strong> {{ __('Pipeline') }} </strong> <a :href="latestPipeline.path" @@ -82,9 +82,9 @@ export default { class="mb-auto mt-auto" /> <div v-else-if="latestPipeline.yamlError" class="bs-callout bs-callout-danger"> - <p class="append-bottom-0">{{ __('Found errors in your .gitlab-ci.yml:') }}</p> - <p class="append-bottom-0 break-word">{{ latestPipeline.yamlError }}</p> - <p class="append-bottom-0" v-html="ciLintText"></p> + <p class="gl-mb-0">{{ __('Found errors in your .gitlab-ci.yml:') }}</p> + <p class="gl-mb-0 break-word">{{ latestPipeline.yamlError }}</p> + <p class="gl-mb-0" v-html="ciLintText"></p> </div> <tabs v-else class="ide-pipeline-list"> <tab :active="!pipelineFailed"> diff --git a/app/assets/javascripts/ide/components/preview/navigator.vue b/app/assets/javascripts/ide/components/preview/navigator.vue index ff23485f0f0..0de9dfd8827 100644 --- a/app/assets/javascripts/ide/components/preview/navigator.vue +++ b/app/assets/javascripts/ide/components/preview/navigator.vue @@ -119,7 +119,7 @@ export default { > <icon :size="18" name="retry" class="m-auto" /> </button> - <div class="position-relative w-100 prepend-left-4"> + <div class="position-relative w-100 gl-ml-2"> <input :value="path || '/'" type="text" diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index 530fba49df2..5eed57bb6c5 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -3,7 +3,7 @@ import { mapState, mapActions, mapGetters } from 'vuex'; import tooltip from '~/vue_shared/directives/tooltip'; import CommitFilesList from './commit_sidebar/list.vue'; import EmptyState from './commit_sidebar/empty_state.vue'; -import { leftSidebarViews, stageKeys } from '../constants'; +import { stageKeys } from '../constants'; export default { components: { @@ -14,39 +14,37 @@ export default { tooltip, }, computed: { - ...mapState(['changedFiles', 'stagedFiles', 'lastCommitMsg', 'unusedSeal']), + ...mapState(['changedFiles', 'stagedFiles', 'lastCommitMsg']), ...mapState('commit', ['commitMessage', 'submitCommitLoading']), - ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommittedChanges', 'activeFile']), + ...mapGetters(['lastOpenedFile', 'someUncommittedChanges', 'activeFile']), ...mapGetters('commit', ['discardDraftButtonDisabled']), showStageUnstageArea() { - return Boolean(this.someUncommittedChanges || this.lastCommitMsg || !this.unusedSeal); + return Boolean(this.someUncommittedChanges || this.lastCommitMsg); }, activeFileKey() { return this.activeFile ? this.activeFile.key : null; }, }, - watch: { - hasChanges() { - if (!this.hasChanges) { - this.updateActivityBarView(leftSidebarViews.edit.name); - } - }, - }, mounted() { - if (this.lastOpenedFile && this.lastOpenedFile.type !== 'tree') { - this.openPendingTab({ - file: this.lastOpenedFile, - keyPrefix: this.lastOpenedFile.staged ? stageKeys.staged : stageKeys.unstaged, + const file = + this.lastOpenedFile && this.lastOpenedFile.type !== 'tree' + ? this.lastOpenedFile + : this.activeFile; + + if (!file) return; + + this.openPendingTab({ + file, + keyPrefix: file.staged ? stageKeys.staged : stageKeys.unstaged, + }) + .then(changeViewer => { + if (changeViewer) { + this.updateViewer('diff'); + } }) - .then(changeViewer => { - if (changeViewer) { - this.updateViewer('diff'); - } - }) - .catch(e => { - throw e; - }); - } + .catch(e => { + throw e; + }); }, methods: { ...mapActions(['openPendingTab', 'updateViewer', 'updateActivityBarView']), @@ -67,6 +65,6 @@ export default { icon-name="unstaged" /> </template> - <empty-state v-if="unusedSeal" /> + <empty-state v-else /> </div> </template> diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index c72a8b2b0d0..a7646083428 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -14,6 +14,9 @@ import Editor from '../lib/editor'; import FileTemplatesBar from './file_templates/bar.vue'; import { __ } from '~/locale'; import { extractMarkdownImagesFromEntries } from '../stores/utils'; +import { getPathParent, readFileAsDataURL } from '../utils'; +import { getRulesWithTraversal } from '../lib/editorconfig/parser'; +import mapRulesToMonaco from '../lib/editorconfig/rules_mapper'; export default { components: { @@ -31,6 +34,7 @@ export default { return { content: '', images: {}, + rules: {}, }; }, computed: { @@ -50,7 +54,6 @@ export default { 'getStagedFile', 'isEditModeActive', 'isCommitModeActive', - 'isReviewModeActive', 'currentBranch', ]), ...mapGetters('fileTemplates', ['showFileTemplatesBar']), @@ -82,10 +85,6 @@ export default { active: this.isPreviewViewMode, }; }, - fileType() { - const info = viewerInformationForPath(this.file.path); - return (info && info.id) || ''; - }, showEditor() { return !this.shouldHideEditor && this.isEditorViewMode; }, @@ -98,6 +97,12 @@ export default { currentBranchCommit() { return this.currentBranch?.commit.id; }, + previewMode() { + return viewerInformationForPath(this.file.path); + }, + fileType() { + return this.previewMode?.id || ''; + }, }, watch: { file(newVal, oldVal) { @@ -165,6 +170,12 @@ export default { this.editor = Editor.create(this.editorOptions); } this.initEditor(); + + // listen in capture phase to be able to override Monaco's behaviour. + window.addEventListener('paste', this.onPaste, true); + }, + destroyed() { + window.removeEventListener('paste', this.onPaste, true); }, methods: { ...mapActions([ @@ -174,10 +185,10 @@ export default { 'setFileLanguage', 'setEditorPosition', 'setFileViewMode', - 'setFileEOL', 'updateViewer', 'removePendingTab', 'triggerFilesChange', + 'addTempImage', ]), initEditor() { if (this.shouldHideEditor && (this.file.content || this.file.raw)) { @@ -186,7 +197,7 @@ export default { this.editor.clearEditor(); - this.fetchFileData() + Promise.all([this.fetchFileData(), this.fetchEditorconfigRules()]) .then(() => { this.createEditorInstance(); }) @@ -223,7 +234,7 @@ export default { if (this.viewer === viewerTypes.edit) { this.editor.createInstance(this.$refs.editor); } else { - this.editor.createDiffInstance(this.$refs.editor, !this.isReviewModeActive); + this.editor.createDiffInstance(this.$refs.editor); } this.setupEditor(); @@ -245,15 +256,15 @@ export default { this.editor.attachModel(this.model); } + this.model.updateOptions(this.rules); + this.model.onChange(model => { const { file } = model; + if (!file.active) return; - if (file.active) { - this.changeFileContent({ - path: file.path, - content: model.getModel().getValue(), - }); - } + const monacoModel = model.getModel(); + const content = monacoModel.getValue(); + this.changeFileContent({ path: file.path, content }); }); // Handle Cursor Position @@ -274,16 +285,51 @@ export default { fileLanguage: this.model.language, }); - // Get File eol - this.setFileEOL({ - eol: this.model.eol, - }); + this.$emit('editorSetup'); }, refreshEditorDimensions() { if (this.showEditor) { this.editor.updateDimensions(); } }, + fetchEditorconfigRules() { + return getRulesWithTraversal(this.file.path, path => { + const entry = this.entries[path]; + if (!entry) return Promise.resolve(null); + + const content = entry.content || entry.raw; + if (content) return Promise.resolve(content); + + return this.getFileData({ path: entry.path, makeFileActive: false }).then(() => + this.getRawFileData({ path: entry.path }), + ); + }).then(rules => { + this.rules = mapRulesToMonaco(rules); + }); + }, + onPaste(event) { + const editor = this.editor.instance; + const reImage = /^image\/(png|jpg|jpeg|gif)$/; + const file = event.clipboardData.files[0]; + + if (editor.hasTextFocus() && this.fileType === 'markdown' && reImage.test(file?.type)) { + // don't let the event be passed on to Monaco. + event.preventDefault(); + event.stopImmediatePropagation(); + + return readFileAsDataURL(file).then(content => { + const parentPath = getPathParent(this.file.path); + const path = `${parentPath ? `${parentPath}/` : ''}${file.name}`; + + return this.addTempImage({ name: path, rawPath: content }).then(({ name: fileName }) => { + this.editor.replaceSelectedText(`![${fileName}](./${fileName})`); + }); + }); + } + + // do nothing if no image is found in the clipboard + return Promise.resolve(); + }, }, viewerTypes, FILE_VIEW_MODE_EDITOR, @@ -301,16 +347,15 @@ export default { role="button" @click.prevent="setFileViewMode({ file, viewMode: $options.FILE_VIEW_MODE_EDITOR })" > - <template v-if="viewer === $options.viewerTypes.edit">{{ __('Edit') }}</template> - <template v-else>{{ __('Review') }}</template> + {{ __('Edit') }} </a> </li> - <li v-if="file.previewMode" :class="previewTabCSS"> + <li v-if="previewMode" :class="previewTabCSS"> <a href="javascript:void(0);" role="button" @click.prevent="setFileViewMode({ file, viewMode: $options.FILE_VIEW_MODE_PREVIEW })" - >{{ file.previewMode.previewTitle }}</a + >{{ previewMode.previewTitle }}</a > </li> </ul> diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue index 1b7f149097b..47c75be3f7c 100644 --- a/app/assets/javascripts/ide/components/repo_tabs.vue +++ b/app/assets/javascripts/ide/components/repo_tabs.vue @@ -1,7 +1,6 @@ <script> import { mapActions } from 'vuex'; import RepoTab from './repo_tab.vue'; -import router from '../ide_router'; export default { components: { @@ -20,15 +19,6 @@ export default { type: String, required: true, }, - hasChanges: { - type: Boolean, - required: true, - }, - mergeRequestId: { - type: String, - required: false, - default: '', - }, }, methods: { ...mapActions(['updateViewer', 'removePendingTab']), @@ -37,7 +27,7 @@ export default { if (this.activeFile.pending) { return this.removePendingTab(this.activeFile).then(() => { - router.push(`/project${this.activeFile.url}`); + this.$router.push(`/project${this.activeFile.url}`); }); } @@ -49,7 +39,7 @@ export default { <template> <div class="multi-file-tabs"> - <ul ref="tabsScroller" class="list-unstyled append-bottom-0"> + <ul ref="tabsScroller" class="list-unstyled gl-mb-0"> <repo-tab v-for="tab in files" :key="tab.key" :tab="tab" /> </ul> </div> diff --git a/app/assets/javascripts/ide/components/resizable_panel.vue b/app/assets/javascripts/ide/components/resizable_panel.vue index 86a4622401c..b49d743d877 100644 --- a/app/assets/javascripts/ide/components/resizable_panel.vue +++ b/app/assets/javascripts/ide/components/resizable_panel.vue @@ -1,6 +1,7 @@ <script> import { mapActions } from 'vuex'; import PanelResizer from '~/vue_shared/components/panel_resizer.vue'; +import { SIDEBAR_MIN_WIDTH } from '../constants'; export default { components: { @@ -14,12 +15,17 @@ export default { minSize: { type: Number, required: false, - default: 340, + default: SIDEBAR_MIN_WIDTH, }, side: { type: String, required: true, }, + resizable: { + type: Boolean, + required: false, + default: true, + }, }, data() { return { @@ -28,7 +34,7 @@ export default { }, computed: { panelStyle() { - if (!this.collapsed) { + if (this.resizable) { return { width: `${this.width}px`, }; @@ -45,9 +51,10 @@ export default { </script> <template> - <div :style="panelStyle" class="multi-file-commit-panel"> + <div class="gl-relative" :style="panelStyle"> <slot></slot> <panel-resizer + v-show="resizable" :size.sync="width" :start-size="initialWidth" :min-size="minSize" diff --git a/app/assets/javascripts/ide/components/terminal/empty_state.vue b/app/assets/javascripts/ide/components/terminal/empty_state.vue new file mode 100644 index 00000000000..9841f1ece48 --- /dev/null +++ b/app/assets/javascripts/ide/components/terminal/empty_state.vue @@ -0,0 +1,71 @@ +<script> +import { GlLoadingIcon } from '@gitlab/ui'; + +export default { + components: { + GlLoadingIcon, + }, + props: { + isLoading: { + type: Boolean, + required: false, + default: true, + }, + isValid: { + type: Boolean, + required: false, + default: false, + }, + message: { + type: String, + required: false, + default: '', + }, + helpPath: { + type: String, + required: false, + default: '', + }, + illustrationPath: { + type: String, + required: false, + default: '', + }, + }, + methods: { + onStart() { + this.$emit('start'); + }, + }, +}; +</script> +<template> + <div class="text-center p-3"> + <div v-if="illustrationPath" class="svg-content svg-130"><img :src="illustrationPath" /></div> + <h4>{{ __('Web Terminal') }}</h4> + <gl-loading-icon v-if="isLoading" size="lg" class="prepend-top-default" /> + <template v-else> + <p>{{ __('Run tests against your code live using the Web Terminal') }}</p> + <p> + <button + :disabled="!isValid" + class="btn btn-info" + type="button" + data-qa-selector="start_web_terminal_button" + @click="onStart" + > + {{ __('Start Web Terminal') }} + </button> + </p> + <div v-if="!isValid && message" class="bs-callout text-left" v-html="message"></div> + <p v-else> + <a + v-if="helpPath" + :href="helpPath" + target="_blank" + v-text="__('Learn more about Web Terminal')" + ></a> + </p> + </template> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/terminal/session.vue b/app/assets/javascripts/ide/components/terminal/session.vue new file mode 100644 index 00000000000..a8fe9ea6866 --- /dev/null +++ b/app/assets/javascripts/ide/components/terminal/session.vue @@ -0,0 +1,53 @@ +<script> +import { mapActions, mapState } from 'vuex'; +import { __ } from '~/locale'; +import Terminal from './terminal.vue'; +import { isEndingStatus } from '../../stores/modules/terminal/utils'; + +export default { + components: { + Terminal, + }, + computed: { + ...mapState('terminal', ['session']), + actionButton() { + if (isEndingStatus(this.session.status)) { + return { + action: () => this.restartSession(), + text: __('Restart Terminal'), + class: 'btn-primary', + }; + } + + return { + action: () => this.stopSession(), + text: __('Stop Terminal'), + class: 'btn-inverted btn-remove', + }; + }, + }, + methods: { + ...mapActions('terminal', ['restartSession', 'stopSession']), + }, +}; +</script> + +<template> + <div v-if="session" class="ide-terminal d-flex flex-column"> + <header class="ide-job-header d-flex align-items-center"> + <h5>{{ __('Web Terminal') }}</h5> + <div class="ml-auto align-self-center"> + <button + v-if="actionButton" + type="button" + class="btn btn-sm" + :class="actionButton.class" + @click="actionButton.action" + > + {{ actionButton.text }} + </button> + </div> + </header> + <terminal :terminal-path="session.terminalPath" :status="session.status" /> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/terminal/terminal.vue b/app/assets/javascripts/ide/components/terminal/terminal.vue new file mode 100644 index 00000000000..0ee4107f9ab --- /dev/null +++ b/app/assets/javascripts/ide/components/terminal/terminal.vue @@ -0,0 +1,117 @@ +<script> +import { mapState } from 'vuex'; +import { GlLoadingIcon } from '@gitlab/ui'; +import { __ } from '~/locale'; +import GLTerminal from '~/terminal/terminal'; +import TerminalControls from './terminal_controls.vue'; +import { RUNNING, STOPPING } from '../../stores/modules/terminal/constants'; +import { isStartingStatus } from '../../stores/modules/terminal/utils'; + +export default { + components: { + GlLoadingIcon, + TerminalControls, + }, + props: { + terminalPath: { + type: String, + required: false, + default: '', + }, + status: { + type: String, + required: true, + }, + }, + data() { + return { + glterminal: null, + canScrollUp: false, + canScrollDown: false, + }; + }, + computed: { + ...mapState(['panelResizing']), + loadingText() { + if (isStartingStatus(this.status)) { + return __('Starting...'); + } else if (this.status === STOPPING) { + return __('Stopping...'); + } + + return ''; + }, + }, + watch: { + panelResizing() { + if (!this.panelResizing && this.glterminal) { + this.glterminal.fit(); + } + }, + status() { + this.refresh(); + }, + terminalPath() { + this.refresh(); + }, + }, + beforeDestroy() { + this.destroyTerminal(); + }, + methods: { + refresh() { + if (this.status === RUNNING && this.terminalPath) { + this.createTerminal(); + } else if (this.status === STOPPING) { + this.stopTerminal(); + } + }, + createTerminal() { + this.destroyTerminal(); + this.glterminal = new GLTerminal(this.$refs.terminal); + this.glterminal.addScrollListener(({ canScrollUp, canScrollDown }) => { + this.canScrollUp = canScrollUp; + this.canScrollDown = canScrollDown; + }); + }, + destroyTerminal() { + if (this.glterminal) { + this.glterminal.dispose(); + this.glterminal = null; + } + }, + stopTerminal() { + if (this.glterminal) { + this.glterminal.disable(); + } + }, + }, +}; +</script> + +<template> + <div class="d-flex flex-column flex-fill min-height-0 pr-3"> + <div class="top-bar d-flex border-left-0 align-items-center"> + <div v-if="loadingText" data-qa-selector="loading_container"> + <gl-loading-icon :inline="true" /> + <span>{{ loadingText }}</span> + </div> + <terminal-controls + v-if="glterminal" + class="ml-auto" + :can-scroll-up="canScrollUp" + :can-scroll-down="canScrollDown" + @scroll-up="glterminal.scrollToTop()" + @scroll-down="glterminal.scrollToBottom()" + /> + </div> + <div class="terminal-wrapper d-flex flex-fill min-height-0"> + <div + ref="terminal" + class="ide-terminal-trace flex-fill min-height-0 w-100" + :data-project-path="terminalPath" + data-qa-selector="terminal_screen" + ></div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/terminal/terminal_controls.vue b/app/assets/javascripts/ide/components/terminal/terminal_controls.vue new file mode 100644 index 00000000000..4c13b4ef103 --- /dev/null +++ b/app/assets/javascripts/ide/components/terminal/terminal_controls.vue @@ -0,0 +1,27 @@ +<script> +import ScrollButton from '~/ide/components/jobs/detail/scroll_button.vue'; + +export default { + components: { + ScrollButton, + }, + props: { + canScrollUp: { + type: Boolean, + required: false, + default: false, + }, + canScrollDown: { + type: Boolean, + required: false, + default: false, + }, + }, +}; +</script> +<template> + <div class="controllers"> + <scroll-button :disabled="!canScrollUp" direction="up" @click="$emit('scroll-up')" /> + <scroll-button :disabled="!canScrollDown" direction="down" @click="$emit('scroll-down')" /> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/terminal/view.vue b/app/assets/javascripts/ide/components/terminal/view.vue new file mode 100644 index 00000000000..db97e95eed9 --- /dev/null +++ b/app/assets/javascripts/ide/components/terminal/view.vue @@ -0,0 +1,41 @@ +<script> +import { mapActions, mapGetters, mapState } from 'vuex'; +import EmptyState from './empty_state.vue'; +import TerminalSession from './session.vue'; + +export default { + components: { + EmptyState, + TerminalSession, + }, + computed: { + ...mapState('terminal', ['isShowSplash', 'paths']), + ...mapGetters('terminal', ['allCheck']), + }, + methods: { + ...mapActions('terminal', ['startSession', 'hideSplash']), + start() { + this.startSession(); + this.hideSplash(); + }, + }, +}; +</script> + +<template> + <div class="h-100"> + <div v-if="isShowSplash" class="h-100 d-flex flex-column justify-content-center"> + <empty-state + :is-loading="allCheck.isLoading" + :is-valid="allCheck.isValid" + :message="allCheck.message" + :help-path="paths.webTerminalHelpPath" + :illustration-path="paths.webTerminalSvgPath" + @start="start()" + /> + </div> + <template v-else> + <terminal-session /> + </template> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue b/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue new file mode 100644 index 00000000000..deb13b5615e --- /dev/null +++ b/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue @@ -0,0 +1,76 @@ +<script> +import { throttle } from 'lodash'; +import { GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui'; +import { mapState } from 'vuex'; +import Icon from '~/vue_shared/components/icon.vue'; +import { + MSG_TERMINAL_SYNC_CONNECTING, + MSG_TERMINAL_SYNC_UPLOADING, + MSG_TERMINAL_SYNC_RUNNING, +} from '../../stores/modules/terminal_sync/messages'; + +export default { + components: { + Icon, + GlLoadingIcon, + }, + directives: { + 'gl-tooltip': GlTooltipDirective, + }, + data() { + return { isLoading: false }; + }, + computed: { + ...mapState('terminalSync', ['isError', 'isStarted', 'message']), + ...mapState('terminalSync', { + isLoadingState: 'isLoading', + }), + status() { + if (this.isLoading) { + return { + icon: '', + text: this.isStarted ? MSG_TERMINAL_SYNC_UPLOADING : MSG_TERMINAL_SYNC_CONNECTING, + }; + } else if (this.isError) { + return { + icon: 'warning', + text: this.message, + }; + } else if (this.isStarted) { + return { + icon: 'mobile-issue-close', + text: MSG_TERMINAL_SYNC_RUNNING, + }; + } + + return null; + }, + }, + watch: { + // We want to throttle the `isLoading` updates so that + // the user actually sees an indicator that changes are sent. + isLoadingState: throttle(function watchIsLoadingState(val) { + this.isLoading = val; + }, 150), + }, + created() { + this.isLoading = this.isLoadingState; + }, +}; +</script> + +<template> + <div + v-if="status" + v-gl-tooltip + :title="status.text" + role="note" + class="d-flex align-items-center" + > + <span>{{ __('Terminal') }}:</span> + <span class="square s16 d-flex-center ml-1" :aria-label="status.text"> + <gl-loading-icon v-if="isLoading" inline size="sm" class="d-flex-center" /> + <icon v-else-if="status.icon" :name="status.icon" :size="16" /> + </span> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status_safe.vue b/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status_safe.vue new file mode 100644 index 00000000000..afaf06f7f68 --- /dev/null +++ b/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status_safe.vue @@ -0,0 +1,22 @@ +<script> +import { mapState } from 'vuex'; +import TerminalSyncStatus from './terminal_sync_status.vue'; + +/** + * It is possible that the vuex module is not registered. + * + * This component will gracefully handle this so the actual one can simply use `mapState(moduleName, ...)`. + */ +export default { + components: { + TerminalSyncStatus, + }, + computed: { + ...mapState(['terminalSync']), + }, +}; +</script> + +<template> + <terminal-sync-status v-if="terminalSync" /> +</template> |