This is an automated email from the ASF dual-hosted git repository. zjffdu pushed a commit to branch web_vue in repository https://gitbox.apache.org/repos/asf/zeppelin.git
The following commit(s) were added to refs/heads/web_vue by this push: new 76b4564 ZEPPELIN-4376, ZEPPELIN-4379 Zeppelin Web Vue - Design as per Mocks and Recycle Bin 76b4564 is described below commit 76b456450792221d5dd9809a36a1434c4955709d Author: Malay Majithia <malay.majit...@gmail.com> AuthorDate: Thu Oct 17 02:56:05 2019 +0530 ZEPPELIN-4376, ZEPPELIN-4379 Zeppelin Web Vue - Design as per Mocks and Recycle Bin ### What is this PR for? - ZEPPELIN-4376 Design change as per new mockups (except Notebook view) - Added Search button placeholder (actual functionality will be implemented with Foldering) - ZEPPELIN-4379 Recycle Bin: Restore Note, Delete Permanently - A different view for Deleted Note - Fixed the bug which used to move the open note to the error state post note operations Thanks, kmaynk for the mockups. Mockups: https://www.figma.com/file/F0m6EJLjoVmxFuYfM2FTUe/Mocks-V1 ### What type of PR is it? Feature ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-4376 * https://issues.apache.org/jira/browse/ZEPPELIN-4379 ### How should this be tested? * Start the zeppelin server * Go to the zeppelin-web-vue folder * Run npm install (first time) * Run npm run serve to run Zeppelin web using vue.js on localhost:8081 ( can be changed through .env file) ### Screenshots (if appropriate) <img width="1440" alt="Screen Shot 2019-10-15 at 3 00 57 AM" src="https://user-images.githubusercontent.com/1881135/66784862-40075300-eef9-11e9-850b-aa96a61e8c82.png"> <img width="633" alt="Screen Shot 2019-10-15 at 3 01 26 AM" src="https://user-images.githubusercontent.com/1881135/66784863-40075300-eef9-11e9-9d30-c832c76a6748.png"> <img width="1440" alt="Screen Shot 2019-10-15 at 3 01 48 AM" src="https://user-images.githubusercontent.com/1881135/66784864-40075300-eef9-11e9-8540-4eaa8b7ec4ae.png"> <img width="1440" alt="Screen Shot 2019-10-15 at 3 08 40 AM" src="https://user-images.githubusercontent.com/1881135/66784865-409fe980-eef9-11e9-95bf-1b8b1d23a90b.png"> ### Questions: * Does the licenses files need update? - No * Is there breaking changes for older versions? - No * Does this needs documentation? - No Author: Malay Majithia <malay.majit...@gmail.com> Closes #3487 from malayhm/ZEPPELIN-4138-1 and squashes the following commits: 7107c5d06 [Malay Majithia] TopMenu: Restore and Delete Permanently Renamed Delete Temporary to Move to Trash in the labels and in the codebase ed7574c9c [Malay Majithia] Rename RecycleBin component to Trash f25bb4778 [Malay Majithia] Rename: Recycle Bin -> Trash 76bb71fc0 [Malay Majithia] Don't close the current tab if it's deleted Removed unnecessary reload list c1836116c [Malay Majithia] Don't delete the open notebook rather use the param note Id 9baf1dae2 [Malay Majithia] Restore Notebook, Delete Permanently, Added Item Context menu options Moved Reload to the Top Menu 9c08a9915 [Malay Majithia] Array merge on note list reload to preserve the paragraph data 1de9593cb [Malay Majithia] Rename Notebook - name / path 30a19d4d2 [Malay Majithia] Removed commented jquery.menu.scss code 8f97e1fe2 [Malay Majithia] ZEPPELIN-4376 Design change as per new mockups - Recycle bin fixes - Delete Notebook permanently - WIP - Search Notebook (sidebar) - WIP --- zeppelin-web-vue/src/App.vue | 21 ++- zeppelin-web-vue/src/assets/jquery.menu.scss | 29 ++-- zeppelin-web-vue/src/classes/web-socket.js | 2 +- .../src/components/Layout/Connectivity.vue | 40 +++++ zeppelin-web-vue/src/components/Layout/Header.vue | 48 ++++-- .../src/components/Layout/LeftNavBar.vue | 2 +- .../src/components/Layout/LeftSideBar.vue | 6 +- zeppelin-web-vue/src/components/Layout/TopMenu.vue | 88 ++++++++-- .../src/components/Notebook/Controls.vue | 90 +++++++--- .../src/components/Notebook/NoteTree.vue | 107 +++++++++++- .../src/components/Notebook/RecycleBin.vue | 110 ------------ .../src/components/Notebook/Rename.vue | 91 ++++++++++ zeppelin-web-vue/src/components/Notebook/Trash.vue | 190 +++++++++++++++++++++ zeppelin-web-vue/src/i18n.js | 11 +- zeppelin-web-vue/src/mixins/array_utils.js | 8 + zeppelin-web-vue/src/services/command-manager.js | 15 +- zeppelin-web-vue/src/services/notebook-utils.js | 29 ++-- zeppelin-web-vue/src/stores/notebook_store.js | 5 +- zeppelin-web-vue/src/stores/tab_manager_store.js | 6 +- 19 files changed, 681 insertions(+), 217 deletions(-) diff --git a/zeppelin-web-vue/src/App.vue b/zeppelin-web-vue/src/App.vue index 31f5eb9..5069eba 100644 --- a/zeppelin-web-vue/src/App.vue +++ b/zeppelin-web-vue/src/App.vue @@ -13,7 +13,7 @@ </SplitArea> </Split> - <StatusBar /> + <!-- <StatusBar /> --> <GlobalEvents @keyup.ctrl.n="executeCommand('note', 'show-create')" @@ -21,8 +21,9 @@ @keyup.ctrl.r="executeCommand('note', 'run-all')" /> - <Create /> - <Import /> + <CreateNote /> + <ImportNote /> + <RenameNote /> </div> </template> @@ -34,14 +35,15 @@ import ws from '@/services/ws-helper' import Header from '@/components/Layout/Header.vue' import LeftSidebar from '@/components/Layout/LeftSideBar.vue' -import StatusBar from '@/components/Layout/StatusBar.vue' +// import StatusBar from '@/components/Layout/StatusBar.vue' -import Create from '@/components/Notebook/Create.vue' -import Import from '@/components/Notebook/Import.vue' +import CreateNote from '@/components/Notebook/Create.vue' +import ImportNote from '@/components/Notebook/Import.vue' +import RenameNote from '@/components/Notebook/Rename.vue' export default { name: 'App', - components: { GlobalEvents, Header, LeftSidebar, StatusBar, Create, Import }, + components: { GlobalEvents, Header, LeftSidebar, CreateNote, ImportNote, RenameNote }, created () { document.title = 'Zeppelin Notebook' }, @@ -80,6 +82,9 @@ export default { body { color: #2c3e50; + font-size: 14px; + line-height: 1.5; + a { text-decoration: none; color: #333; @@ -99,7 +104,7 @@ body { width: 100%; > .split { - height: calc(100% - 67px - 24px); + height: calc(100% - 40px); border-top: 1px solid #F1F1F1; } diff --git a/zeppelin-web-vue/src/assets/jquery.menu.scss b/zeppelin-web-vue/src/assets/jquery.menu.scss index 5d24d04..8e181d5 100644 --- a/zeppelin-web-vue/src/assets/jquery.menu.scss +++ b/zeppelin-web-vue/src/assets/jquery.menu.scss @@ -1,18 +1,18 @@ #menu-bar { * { - box-sizing: content-box; + box-sizing: border-box; } .menu-top-mask { height: 2px; - background-color: #fff; z-index:1001; } ul.main-menu { list-style-type: none; - margin: 0 0 0 3px; padding: 0; + margin: 0; + padding-top: 6px; font-size: 14px; font-weight: 400; @@ -20,21 +20,14 @@ margin: 0; display: inline-block; list-style-type: none; - padding: 0 8px; - line-height: 28px; - vertical-align: middle; + padding: 4px 8px; + height: 34px; cursor: pointer; border: 1px solid transparent; &.active-menu { - background-color: #fff; - border-color: #ccc; - border: 1px solid #BDBDBD; - border-bottom-color: transparent; - - &:hover{ - background-color: #fff; - } + background-color: rgba(211, 211, 211, 0.2); + border-radius: 2px 2px 0px 0px; } .separator { @@ -48,9 +41,6 @@ padding: 0; margin: 0; display: none; - border-width:1px; - border-style: solid; - border-color: #ccc; background-color: #fff; -webkit-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); -moz-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); @@ -60,7 +50,8 @@ li { &:hover{ - background-color: whiteSmoke; /*#fef7cb;*/ + background-color: rgba(211, 211, 211, 0.2); + border-radius: 2px 2px 0px 0px; } a { @@ -95,7 +86,7 @@ cursor:default; background-color: #fff; } - padding-right: 40px; + padding: 4px 40px 4px 0; span { font-size: 11px; diff --git a/zeppelin-web-vue/src/classes/web-socket.js b/zeppelin-web-vue/src/classes/web-socket.js index 32d26eb..b474950 100644 --- a/zeppelin-web-vue/src/classes/web-socket.js +++ b/zeppelin-web-vue/src/classes/web-socket.js @@ -96,7 +96,7 @@ export default class WsConnection { // Pending - open the note tab data.note break case 'NOTES_INFO': - this.store.dispatch('setNoteMenu', data) + this.store.dispatch('setNoteList', data) break case 'NOTE': this.store.dispatch('setNoteContent', data) diff --git a/zeppelin-web-vue/src/components/Layout/Connectivity.vue b/zeppelin-web-vue/src/components/Layout/Connectivity.vue new file mode 100644 index 0000000..bf95737 --- /dev/null +++ b/zeppelin-web-vue/src/components/Layout/Connectivity.vue @@ -0,0 +1,40 @@ +<template> + <div + id="connection-status" + class="status-bar-widget" + > + <a-tooltip placement="bottom"> + <template slot="title"> + <span>Server Connectivity</span> + </template> + <div + class="ConnectionIndicator" + :class="'ConnectionIndicator--' + connectivityStatus" + > + <div class="Status"> + <div class="Status__circle Status__circle--static"></div> + <div class="Status__circle Status__circle--animated Status__circle--pulse"></div> + </div> + <div + class="status-label" + > + {{ connectivityStatus.replace('trying', 'trying to connect') }} + </div> + </div> + </a-tooltip> + </div> +</template> + +<script> +export default { + name: 'Connectivity', + computed: { + connectivityStatus () { + return this.$store.state.webSocketStatus + } + } +} +</script> + +<style lang="scss" scoped> +</style> diff --git a/zeppelin-web-vue/src/components/Layout/Header.vue b/zeppelin-web-vue/src/components/Layout/Header.vue index a5d3fa4..45cdffd 100644 --- a/zeppelin-web-vue/src/components/Layout/Header.vue +++ b/zeppelin-web-vue/src/components/Layout/Header.vue @@ -3,47 +3,67 @@ <div style="display: flex"> <router-link to="/" - class="logo pt-3 pl-2" + class="logo pt-2 pl-2" > - <img alt="Zeppelin logo" src="@/assets/zepLogo.png"> + <img alt="Zeppelin logo" src="@/assets/zepLogoW.png"> </router-link> - <div> - <h5 class="pt-2 mb-1"> - Zeppelin Notebook - </h5> - <TopMenu /> - </div> + <h5> + Zeppelin Notebook + </h5> + + <TopMenu /> + + <Connectivity class="connectivity"/> </div> </div> </template> <script> import TopMenu from './TopMenu.vue' +import Connectivity from './Connectivity.vue' export default { name: 'Header', - components: { TopMenu } + components: { TopMenu, Connectivity } } </script> <style lang="scss" scoped> #header { - height: 67px; + height: 40px; margin: 0; padding: 0; + background: #2C2C2C; + color: #FFFFFF; + + h5 { + color: #FFFFFF; + font-style: normal; + font-weight: 600; + font-size: 16px; + border-right: 0.5px solid rgba(211, 211, 211, 0.2); + vertical-align: middle; + line-height: 20px; + padding-left: 12px; + padding-right: 20px; + margin-right: 20px; + margin-top: 10px; + } .logo { display: block; - width: 60px; + width: 50px; img { - width: 40px; + height: 20px; } } - h5 { - padding-left: 12px; + .connectivity { + position: absolute; + right: 0; + top: 7px; } } </style> diff --git a/zeppelin-web-vue/src/components/Layout/LeftNavBar.vue b/zeppelin-web-vue/src/components/Layout/LeftNavBar.vue index b536419..4ce2bb2 100644 --- a/zeppelin-web-vue/src/components/Layout/LeftNavBar.vue +++ b/zeppelin-web-vue/src/components/Layout/LeftNavBar.vue @@ -59,7 +59,7 @@ > <a-tooltip placement="right"> <template slot="title"> - <span>Recycle Bin</span> + <span>Trash</span> </template> <a-icon type="delete" /> </a-tooltip> diff --git a/zeppelin-web-vue/src/components/Layout/LeftSideBar.vue b/zeppelin-web-vue/src/components/Layout/LeftSideBar.vue index 8b57aeb..8287e7e 100644 --- a/zeppelin-web-vue/src/components/Layout/LeftSideBar.vue +++ b/zeppelin-web-vue/src/components/Layout/LeftSideBar.vue @@ -28,7 +28,7 @@ v-if="this.$store.state.selectedLeftNavTab === 'trash'" class="trash-content" > - <RecycleBin /> + <Trash /> </div> </div> </div> @@ -39,7 +39,7 @@ import LeftNavBar from './LeftNavBar.vue' import NoteTree from '@/components/Notebook/NoteTree.vue' import ActivityConsole from '@/components/ActivityConsole.vue' import PackageList from '@/components/Helium/PackageList.vue' -import RecycleBin from '@/components/Notebook/RecycleBin.vue' +import Trash from '@/components/Notebook/Trash.vue' export default { name: 'LeftSideBar', @@ -48,7 +48,7 @@ export default { NoteTree, ActivityConsole, PackageList, - RecycleBin + Trash } } </script> diff --git a/zeppelin-web-vue/src/components/Layout/TopMenu.vue b/zeppelin-web-vue/src/components/Layout/TopMenu.vue index 032a74d..1b83a1f 100644 --- a/zeppelin-web-vue/src/components/Layout/TopMenu.vue +++ b/zeppelin-web-vue/src/components/Layout/TopMenu.vue @@ -26,7 +26,7 @@ <li> <a - v-bind:class="{'disabled': !(isActiveNote)}" + v-bind:class="{'disabled': !(isActiveNote && !isDeleted)}" @click="executeNoteCommand('save')" href="javascript:void(0)" > @@ -36,7 +36,7 @@ <li> <a - v-bind:class="{'disabled': !(isActiveNote)}" + v-bind:class="{'disabled': !(isActiveNote && !isDeleted)}" @click="executeNoteCommand('manage-permissions')" href="javascript:void(0)" > @@ -47,7 +47,7 @@ <li class="separator"></li> <li> <a - v-bind:class="{'disabled': !(isActiveNote)}" + v-bind:class="{'disabled': !(isActiveNote && !isDeleted)}" @click="executeNoteCommand('export-json')" href="javascript:void(0)" > @@ -56,11 +56,30 @@ </li> <li> <a + v-if="!isDeleted" v-bind:class="{'disabled': !(isActiveNote)}" - @click="executeNoteCommand('delete-temporary')" + @click="showMoveToTrashConfirm" href="javascript:void(0)" > - Move To Recycle Bin + Move To Trash + </a> + </li> + <li> + <a + v-if="isDeleted" + @click="executeNoteCommand('restore-note')" + href="javascript:void(0)" + > + Restore + </a> + </li> + <li> + <a + v-if="isDeleted" + @click="showDeletePermConfirm" + href="javascript:void(0)" + > + Delete Permanently </a> </li> @@ -84,7 +103,7 @@ <li> <a @click="executeNoteCommand('toggle-code')" - v-bind:class="{'disabled': !(isActiveNote)}" + v-bind:class="{'disabled': !(isActiveNote && !isDeleted)}" href="javascript:void(0)" > Show/Hide Code @@ -93,7 +112,7 @@ <li> <a @click="executeNoteCommand('toggle-line-numbers')" - v-bind:class="{'disabled': !(isActiveNote)}" + v-bind:class="{'disabled': !(isActiveNote && !isDeleted)}" href="javascript:void(0)" > Show/Hide Line Numbers @@ -102,7 +121,7 @@ <li> <a @click="executeNoteCommand('toggle-output')" - v-bind:class="{'disabled': !(isActiveNote)}" + v-bind:class="{'disabled': !(isActiveNote && !isDeleted)}" href="javascript:void(0)" > Show/Hide Outputs @@ -111,7 +130,7 @@ <li class="separator"></li> <li> <a - v-bind:class="{'disabled': !(isActiveNote)}" + v-bind:class="{'disabled': !(isActiveNote && !isDeleted)}" @click="executeNoteCommand('find-and-replace')" href="javascript:void(0)" > @@ -122,7 +141,7 @@ <li> <a @click="showConfirmClearOutput" - v-bind:class="{'disabled': !(isActiveNote)}" + v-bind:class="{'disabled': !(isActiveNote && !isDeleted)}" href="javascript:void(0)" > Clear All Outputs @@ -161,6 +180,16 @@ Note Info </a> </li> + <li class="separator"></li> + <li> + <a + v-bind:class="{'disabled': !(isActiveNote)}" + @click="executeNoteCommand('reload')" + href="javascript:void(0)" + > + Reload + </a> + </li> </ul> </li> @@ -169,7 +198,7 @@ <ul> <li> <a - v-bind:class="{'disabled': !(isActiveNote)}" + v-bind:class="{'disabled': !(isActiveNote && !isDeleted)}" @click="executeNoteCommand('run-all')" href="javascript:void(0)" > @@ -178,7 +207,7 @@ </li> <li> <a - v-bind:class="{'disabled': !(isActiveNote)}" + v-bind:class="{'disabled': !(isActiveNote && !isDeleted)}" @click="executeNoteCommand('run-before')" href="javascript:void(0)" > @@ -187,7 +216,7 @@ </li> <li> <a - v-bind:class="{'disabled': !(isActiveNote)}" + v-bind:class="{'disabled': !(isActiveNote && !isDeleted)}" @click="executeNoteCommand('run-focused')" href="javascript:void(0)" > @@ -196,7 +225,7 @@ </li> <li> <a - v-bind:class="{'disabled': !(isActiveNote)}" + v-bind:class="{'disabled': !(isActiveNote && !isDeleted)}" @click="executeNoteCommand('run-after')" href="javascript:void(0)" > @@ -347,6 +376,11 @@ export default { isActiveNote () { return (this.$store.state.TabManagerStore.currentTab && this.$store.state.TabManagerStore.currentTab.type === 'note') + }, + isDeleted () { + if (!this.isActiveNote) return false + + return this.activeNote.path ? this.activeNote.path.split('/')[1] === this.$root.TRASH_FOLDER_ID : false } }, mounted () { @@ -382,6 +416,32 @@ export default { }, onCancel () {} }) + }, + showMoveToTrashConfirm () { + let that = this + this.$confirm({ + title: that.$i18n.t('message.note.move_to_trash_confirm'), + content: that.$i18n.t('message.note.move_to_trash_content'), + onOk () { + that.executeNoteCommand('move-to-trash') + + that.$message.success(that.$i18n.t('message.note.move_to_trash_success'), 4) + }, + onCancel () {} + }) + }, + showDeletePermConfirm () { + let that = this + this.$confirm({ + title: that.$i18n.t('message.note.delete_confirm'), + content: that.$i18n.t('message.note.delete_content'), + onOk () { + that.executeNoteCommand('delete-permanently') + + that.$message.success(that.$i18n.t('message.note.delete_success'), 4) + }, + onCancel () {} + }) } } } diff --git a/zeppelin-web-vue/src/components/Notebook/Controls.vue b/zeppelin-web-vue/src/components/Notebook/Controls.vue index f3f09a8..7b3b45f 100644 --- a/zeppelin-web-vue/src/components/Notebook/Controls.vue +++ b/zeppelin-web-vue/src/components/Notebook/Controls.vue @@ -4,6 +4,7 @@ <a href="javascript: void(0);" @click="executeNoteCommand('run-all')" + :disabled="isDeleted" > <a-tooltip placement="top"> <template slot="title"> @@ -16,6 +17,7 @@ <a href="javascript: void(0);" @click="executeNoteCommand('save')" + :disabled="isDeleted" > <a-tooltip placement="top"> <template slot="title"> @@ -28,6 +30,7 @@ <a href="javascript: void(0);" @click="executeNoteCommand('show-clone')" + :disabled="isDeleted" > <a-tooltip placement="top"> <template slot="title"> @@ -40,6 +43,7 @@ <a href="javascript: void(0);" @click="executeNoteCommand('export-json')" + :disabled="isDeleted" > <a-tooltip placement="top"> <template slot="title"> @@ -51,53 +55,74 @@ <a href="javascript: void(0);" - @click="showDeleteConfirm" + @click="showMoveToTrashConfirm" + v-if="!isDeleted" > <a-tooltip placement="top"> <template slot="title"> - <span>Delete</span> + <span>Move to Trash</span> </template> <a-icon type="delete" /> </a-tooltip> </a> - </div> - <div class="right-controls"> <a href="javascript: void(0);" + @click="executeNoteCommand('restore-note')" + v-if="isDeleted" > <a-tooltip placement="top"> <template slot="title"> - <span>Search Code</span> + <span>Restore</span> </template> - <a-icon type="file-search" /> + <a-icon type="reload" /> </a-tooltip> </a> <a href="javascript: void(0);" + @click="showDeletePermConfirm" + v-if="isDeleted" > <a-tooltip placement="top"> <template slot="title"> - <span>Version Control</span> + <span>Delete Permanently</span> </template> - <a-icon type="diff" /> + <a-icon type="delete" /> </a-tooltip> </a> + </div> + <div class="right-controls"> <a href="javascript: void(0);" - @click="executeNoteCommand('reload')" + :disabled="isDeleted" > <a-tooltip placement="top"> <template slot="title"> - <span>Reload</span> + <span>Search Code</span> </template> - <a-icon type="reload" /> + <a-icon type="file-search" /> </a-tooltip> </a> - <a-dropdown :trigger="['click']"> + <a + href="javascript: void(0);" + :disabled="isDeleted" + > + <a-tooltip placement="top"> + <template slot="title"> + <span>Version Control</span> + </template> + <a-icon type="diff" /> + </a-tooltip> + </a> + + <a-dropdown + :disabled="isDeleted" + :class="{disabled: isDeleted}" + :trigger="['click']" + > <a class="ant-dropdown-link" href="#"> <span> Default </span> <a-icon type="down" /> @@ -124,19 +149,38 @@ export default { props: { noteId: { required: true } }, + computed: { + isDeleted () { + let notePath = this.$store.getters.getNote(this.$props.noteId).path + return notePath ? notePath.split('/')[1] === this.$root.TRASH_FOLDER_ID : false + } + }, methods: { - executeNoteCommand (command) { - this.$root.executeCommand('note', command) + executeNoteCommand (command, args) { + this.$root.executeCommand('note', command, args) + }, + showMoveToTrashConfirm () { + let that = this + this.$confirm({ + title: that.$i18n.t('message.note.move_to_trash_confirm'), + content: that.$i18n.t('message.note.move_to_trash_content'), + onOk () { + that.executeNoteCommand('move-to-trash') + + that.$message.success(that.$i18n.t('message.note.move_to_trash_success'), 4) + }, + onCancel () {} + }) }, - showDeleteConfirm () { + showDeletePermConfirm () { let that = this this.$confirm({ - title: that.$i18n.t('message.note.move_to_rb_confirm'), - content: that.$i18n.t('message.note.move_to_rb_content'), + title: that.$i18n.t('message.note.delete_confirm'), + content: that.$i18n.t('message.note.delete_content'), onOk () { - that.executeNoteCommand('delete-temporary') + that.executeNoteCommand('delete-permanently') - that.$message.success(that.$i18n.t('message.note.move_to_rb_success'), 4) + that.$message.success(that.$i18n.t('message.note.delete_success'), 4) }, onCancel () {} }) @@ -153,11 +197,17 @@ export default { a { display: inline-block; height: 100%; - padding: 2px 6px; + padding: 2px 7px; span { vertical-align: middle; } + + &.disabled { + cursor: default; + color: inherit; + opacity: 0.5; + } } .left-controls { diff --git a/zeppelin-web-vue/src/components/Notebook/NoteTree.vue b/zeppelin-web-vue/src/components/Notebook/NoteTree.vue index 0e8dacd..01b1544 100644 --- a/zeppelin-web-vue/src/components/Notebook/NoteTree.vue +++ b/zeppelin-web-vue/src/components/Notebook/NoteTree.vue @@ -14,6 +14,13 @@ </div> </div> + <a-input-search + v-if="!isLoading" + placeholder="search notebooks" + class="search-notebook-box" + @search="onSearch" + /> + <ul> <li v-for="(note, index) in this.notes" @@ -25,10 +32,46 @@ v-bind:title="note.path" class="text-ellipsis" :class="{'active': note.id === activeNoteId}" - v-on:click="openNote(note)" + @click="openNote(note)" > <a-icon type="file" /> - {{ getFileName(note.path) }} + <span>{{ getFileName(note.path) }}</span> + + <a-dropdown + class="note-menu" + placement="bottomRight" + > + <a class="ant-dropdown-link" href="#"> + <a-icon type="ellipsis" /> + </a> + <a-menu slot="overlay"> + <a-menu-item> + <a + href="javascript: void(0);" + @click="openNote(note)" + > + Open Notebook + </a> + </a-menu-item> + <a-menu-item> + <a + href="javascript: void(0);" + @click="showRenameDialog(note)" + > + Rename + </a> + </a-menu-item> + <a-menu-divider /> + <a-menu-item> + <a + href="javascript: void(0);" + @click="showMoveToTrashConfirm(note.id)" + > + Move to Trash + </a> + </a-menu-item> + </a-menu> + </a-dropdown> </a> </li> </ul> @@ -40,6 +83,11 @@ import ws from '@/services/ws-helper' export default { name: 'NoteTree', + data () { + return { + + } + }, mounted () { ws.getConn().send({ op: 'LIST_NOTES' }) }, @@ -63,6 +111,31 @@ export default { }, getFileName (path) { return path.substr(path.lastIndexOf('/') + 1) + }, + onSelect (keys) { + console.log('Trigger Select', keys) + }, + onSearch (value) { + // a + }, + showMoveToTrashConfirm (noteId) { + let that = this + this.$confirm({ + title: that.$i18n.t('message.note.move_to_trash_confirm'), + content: that.$i18n.t('message.note.move_to_trash_content'), + onOk () { + that.$root.executeCommand('note', 'move-to-trash', noteId) + + that.$message.success(that.$i18n.t('message.note.move_to_trash_success'), 4) + }, + onCancel () {} + }) + }, + showRenameDialog (note) { + this.$root.executeCommand('showRenameNoteDialog', { + id: note.id, + path: note.path + }) } } } @@ -82,6 +155,11 @@ export default { } } +.search-notebook-box { + padding: 8px 6px; + width: calc(100%); +} + .notes { list-style: none; margin: 0; @@ -92,13 +170,24 @@ export default { margin: 0; padding: 0; - li { + li.note { + position: relative; + a { font-size: 14px; padding: 5px 10px; - display: block; + display: flex; border-left: 4px solid transparent; + i { + line-height: 20px; + } + + &> span { + position: relative; + padding-left: 5px; + } + &.active { background: #f1eeee; border-left-color: #2f71a9; @@ -106,7 +195,17 @@ export default { &:hover { background: #F1F1F1; + } + } + + a.note-menu { + position: absolute; + top: 5px; + right: 5px; + padding: 0; + i { + transform: rotate(90deg); } } } diff --git a/zeppelin-web-vue/src/components/Notebook/RecycleBin.vue b/zeppelin-web-vue/src/components/Notebook/RecycleBin.vue deleted file mode 100644 index 10dc920..0000000 --- a/zeppelin-web-vue/src/components/Notebook/RecycleBin.vue +++ /dev/null @@ -1,110 +0,0 @@ -<template> - <div class="notes"> - <div - v-if="isLoading" - > - <div - v-for="index in 3" - :key="index" - class="timeline-item" - > - <div class="animated-background"> - <div class="background-masker nb-label-separator"></div> - </div> - </div> - </div> - - <ul> - <li - v-for="(note, index) in this.notes" - :key="index" - class="note" - > - <a - href="javascript: void(0);" - v-bind:title="note.path" - class="text-ellipsis" - :class="{'active': note.id === activeNoteId}" - v-on:click="openNote(note)" - > - <a-icon type="file" /> - {{ getFileName(note.path) }} - </a> - </li> - </ul> - </div> -</template> - -<script> -export default { - name: 'RecycleBin', - computed: { - isLoading () { - return this.$store.state.NotebookStore.isListLoading - }, - activeNoteId () { - return this.$store.state.TabManagerStore.currentTab && this.$store.state.TabManagerStore.currentTab.id - }, - notes () { - return this.$store.state.NotebookStore.notes.filter(n => (n.path ? n.path.split('/')[1] === this.$root.TRASH_FOLDER_ID : false)) - } - }, - methods: { - openNote (note) { - this.$root.executeCommand('tabs', 'open', { - type: 'note', - note: note - }) - }, - getFileName (path) { - return path.substr(path.lastIndexOf('/') + 1) - } - } -} -</script> - -<style lang="scss" scoped> -.timeline-item { - padding: 0 12px; - margin: 9px auto; - height: 20px; - - .nb-label-separator { - left: 20px; - top: 0; - width: 4px; - height: 24px; - } -} - -.notes { - list-style: none; - margin: 0; - padding: 0; - - ul { - list-style: none; - margin: 0; - padding: 0; - - li { - a { - font-size: 14px; - padding: 5px 10px; - display: block; - border-left: 4px solid transparent; - - &.active { - background: #f1eeee; - border-left-color: #2f71a9; - } - - &:hover { - background: #F1F1F1; - - } - } - } - } -} -</style> diff --git a/zeppelin-web-vue/src/components/Notebook/Rename.vue b/zeppelin-web-vue/src/components/Notebook/Rename.vue new file mode 100644 index 0000000..cc72214 --- /dev/null +++ b/zeppelin-web-vue/src/components/Notebook/Rename.vue @@ -0,0 +1,91 @@ +<template> + <div> + <a-modal + v-model="showDialog" + title="Rename Note" + onOk="handleOk" + :maskClosable="false" + > + <template slot="footer"> + <a-button key="back" @click="handleCancel">Cancel</a-button> + <a-button key="submit" type="primary" :loading="loading" @click="handleOk"> + Rename + </a-button> + </template> + + <a-form layout="vertical"> + <a-form-item + label="Note Name" + > + <a-input placeholder="Enter Note Name" v-model="name"/> + </a-form-item> + + <a-alert message="Use '/' to create folders. Example: /NoteDirA/Note1" type="info" /> + + <input type="hidden" v-model="sourceNoteId" name="sourceNoteId" value="" /> + + </a-form> + </a-modal> + </div> +</template> + +<script> +import { EventBus } from '@/services/event-bus' + +export default { + name: 'RenameNote', + data () { + return { + showDialog: false, + loading: false, + + name: '', + sourceNoteId: '' + } + }, + computed: { + interpreters () { + return this.$store.state.InterpreterStore.interpreters + } + }, + mounted () { + EventBus.$on('showRenameNoteDialog', (note) => { + this.sourceNoteId = note.id + this.name = note.path + this.showDialog = true + }) + }, + methods: { + handleOk (e) { + this.loading = true + + this.$root.executeCommand('note', 'rename', { + newNoteName: this.name, + sourceNoteId: this.sourceNoteId + }) + + let that = this + setTimeout(() => { + this.showDialog = false + this.loading = false + + this.resetForm() + + that.$message.success(that.$i18n.t('message.note.rename_success'), 4) + // Pending - validation + // Pending update everywhere + }, 1000) + }, + handleCancel (e) { + this.showDialog = false + }, + resetForm () { + this.name = '' + this.sourceNoteId = '' + } + } +} +</script> + +<style lang="scss" scoped> +</style> diff --git a/zeppelin-web-vue/src/components/Notebook/Trash.vue b/zeppelin-web-vue/src/components/Notebook/Trash.vue new file mode 100644 index 0000000..9d59cb4 --- /dev/null +++ b/zeppelin-web-vue/src/components/Notebook/Trash.vue @@ -0,0 +1,190 @@ +<template> + <div class="notes"> + <div + v-if="isLoading" + > + <div + v-for="index in 3" + :key="index" + class="timeline-item" + > + <div class="animated-background"> + <div class="background-masker nb-label-separator"></div> + </div> + </div> + </div> + + <ul> + <li + v-for="(note, index) in this.notes" + :key="index" + class="note" + > + <a + href="javascript: void(0);" + v-bind:title="note.path" + class="text-ellipsis" + :class="{'active': note.id === activeNoteId}" + @click="openNote(note)" + > + <a-icon type="file" /> + <span>{{ getFileName(note.path) }}</span> + + <a-dropdown + class="note-menu" + placement="bottomRight" + > + <a class="ant-dropdown-link" href="#"> + <a-icon type="ellipsis" /> + </a> + <a-menu slot="overlay"> + <a-menu-item> + <a + href="javascript: void(0);" + @click="openNote(note)" + > + Open Notebook + </a> + </a-menu-item> + <a-menu-divider /> + <a-menu-item> + <a + href="javascript: void(0);" + @click="executeNoteCommand('restore-note', note.id)" + > + Restore + </a> + </a-menu-item> + <a-menu-item> + <a + href="javascript: void(0);" + @click="showDeletePermConfirm(note.id)" + > + Delete Permanently + </a> + </a-menu-item> + </a-menu> + </a-dropdown> + </a> + </li> + </ul> + + <div + v-if="this.notes.length === 0" + class="pt-2 pl-2" + > + {{ $t("message.note.empty_trash") }} + </div> + </div> +</template> + +<script> +export default { + name: 'Trash', + computed: { + isLoading () { + return this.$store.state.NotebookStore.isListLoading + }, + activeNoteId () { + return this.$store.state.TabManagerStore.currentTab && this.$store.state.TabManagerStore.currentTab.id + }, + notes () { + return this.$store.state.NotebookStore.notes.filter(n => (n.path ? n.path.split('/')[1] === this.$root.TRASH_FOLDER_ID : false)) + } + }, + methods: { + executeNoteCommand (command, args) { + this.$root.executeCommand('note', command, args) + }, + openNote (note) { + this.$root.executeCommand('tabs', 'open', { + type: 'note', + note: note + }) + }, + showDeletePermConfirm (noteId) { + let that = this + this.$confirm({ + title: that.$i18n.t('message.note.delete_confirm'), + content: that.$i18n.t('message.note.delete_content'), + onOk () { + that.executeNoteCommand('delete-permanently', noteId) + + that.$message.success(that.$i18n.t('message.note.delete_success'), 4) + }, + onCancel () {} + }) + }, + getFileName (path) { + return path.substr(path.lastIndexOf('/') + 1) + } + } +} +</script> + +<style lang="scss" scoped> +.timeline-item { + padding: 0 12px; + margin: 9px auto; + height: 20px; + + .nb-label-separator { + left: 20px; + top: 0; + width: 4px; + height: 24px; + } +} + +.notes { + list-style: none; + margin: 0; + padding: 0; + + ul { + list-style: none; + margin: 0; + padding: 0; + + li.note { + position: relative; + + a { + font-size: 14px; + padding: 5px 10px; + display: flex; + border-left: 4px solid transparent; + + i { + line-height: 20px; + } + + &> span { + position: relative; + padding-left: 5px; + } + + &.active { + background: #f1eeee; + border-left-color: #2f71a9; + } + + &:hover { + background: #F1F1F1; + } + } + + a.note-menu { + position: absolute; + top: 5px; + right: 5px; + padding: 0; + + i { + transform: rotate(90deg); + } + } + } + } +} +</style> diff --git a/zeppelin-web-vue/src/i18n.js b/zeppelin-web-vue/src/i18n.js index 3e2424f..92ce8fb 100644 --- a/zeppelin-web-vue/src/i18n.js +++ b/zeppelin-web-vue/src/i18n.js @@ -21,9 +21,14 @@ export const i18n = new VueI18n({ clone_success: 'Note cloned successfully.', clear_output_confirm: 'Do you want to clear the ouput for all the paragraphs?', clear_output_success: 'Output cleared successfully for all the paragraphs.', - move_to_rb_confirm: 'Do you want to delete this Note?', - move_to_rb_content: 'This will move the note to Recycle Bin and you can still recover it.', - move_to_rb_success: 'Note moved to recycle bin successfully.' + move_to_trash_confirm: 'Do you want to delete this Note?', + move_to_trash_content: 'This will move the note to Trash and you can still recover it.', + move_to_trash_success: 'Note moved to Trash successfully.', + delete_confirm: 'Do you want to delete the notebook permanently?', + delete_content: 'This will remove it permanently and can not be recovered.', + delete_success: 'Note deleted successfully.', + rename_success: 'Note renamed successfully', + empty_trash: 'Empty Trash' } } }, diff --git a/zeppelin-web-vue/src/mixins/array_utils.js b/zeppelin-web-vue/src/mixins/array_utils.js new file mode 100644 index 0000000..a7932b3 --- /dev/null +++ b/zeppelin-web-vue/src/mixins/array_utils.js @@ -0,0 +1,8 @@ +export default { + mergeArray (a, b, prop) { + return b.map((itemb) => { + let srcItem = a.find(itema => itema[prop] === itemb[prop]) + return srcItem? { ...srcItem, ...itemb } : itemb + }) + } +} diff --git a/zeppelin-web-vue/src/services/command-manager.js b/zeppelin-web-vue/src/services/command-manager.js index f271132..edd22a2 100644 --- a/zeppelin-web-vue/src/services/command-manager.js +++ b/zeppelin-web-vue/src/services/command-manager.js @@ -52,7 +52,10 @@ export default { let isActiveNote = (store.state.TabManagerStore.currentTab && store.state.TabManagerStore.currentTab.type === 'note') - if (!(isActiveNote || ['show-create', 'create', 'show-import', 'import-json'].indexOf(command) !== -1)) { + if (!(isActiveNote || ['show-create', 'create', 'rename', 'show-import', 'import-json', + 'move-to-trash', 'restore-note', 'delete-permanently'].indexOf(command) !== -1) + ) + { return } let note = store.state.TabManagerStore.currentTab @@ -70,6 +73,9 @@ export default { case 'import-json': notebookUtils.importJSON(args) break + case 'rename': + notebookUtils.rename(args) + break case 'clear-output': notebookUtils.clearAllOutputs(note.id) break @@ -89,13 +95,14 @@ export default { break case 'print': break - case 'delete-temporary': - notebookUtils.deleteTemporary(note.id) + case 'move-to-trash': + notebookUtils.moveToTrash(args || (note && note.id)) break case 'restore-note': - notebookUtils.restore(note.id) + notebookUtils.restore(args || (note && note.id)) break case 'delete-permanently': + notebookUtils.deletePermanently(args || (note && note.id)) break case 'show-clone': notebookUtils.showCloneModal(note.id) diff --git a/zeppelin-web-vue/src/services/notebook-utils.js b/zeppelin-web-vue/src/services/notebook-utils.js index 9e2bb41..85e0c1c 100644 --- a/zeppelin-web-vue/src/services/notebook-utils.js +++ b/zeppelin-web-vue/src/services/notebook-utils.js @@ -44,6 +44,19 @@ export default { // Pending - open the note after create }, + rename (params) { + console.log(params) + wsHelper.getConn().send({ + op: 'NOTE_RENAME', + data: { + id: params.sourceNoteId, + name: params.newNoteName + } + }) + + // Reload the left sidebar will happen automatically as it will return the full list as the response + }, + open (note) { wsFactory.initNoteConnection(note.id, this.store) @@ -111,7 +124,7 @@ export default { }) }, - deleteTemporary (noteId) { + moveToTrash (noteId) { wsHelper.getConn().send({ op: 'MOVE_NOTE_TO_TRASH', data: { @@ -120,10 +133,10 @@ export default { }) // Remove the tab - this.store.dispatch('removeTab', this.store.state.TabManagerStore.currentTab) - - // Reload the note list - this.reloadList() + let currentTab = this.store.state.TabManagerStore.currentTab + if (currentTab && currentTab.id === noteId) { + this.store.dispatch('removeTab', this.store.state.TabManagerStore.currentTab) + } }, deletePermanently (noteId) { @@ -133,9 +146,6 @@ export default { id: noteId } }) - - // Reload the note list - this.reloadList() }, restore (noteId) { @@ -145,8 +155,5 @@ export default { id: noteId } }) - - // Reload the note list - this.reloadList() } } diff --git a/zeppelin-web-vue/src/stores/notebook_store.js b/zeppelin-web-vue/src/stores/notebook_store.js index 5cebad1..616a7f4 100644 --- a/zeppelin-web-vue/src/stores/notebook_store.js +++ b/zeppelin-web-vue/src/stores/notebook_store.js @@ -1,4 +1,5 @@ import Vue from 'vue' +import arrayUtils from '@/mixins/array_utils.js' export default { state: { @@ -66,7 +67,7 @@ export default { }, mutateNotes (state, data) { state.isListLoading = false - state.notes = data.notes + state.notes = arrayUtils.mergeArray(state.notes, data.notes, 'id') }, mutateNote (state, noteObj) { let index = state.notes.map(function (n) { return n.id }).indexOf(noteObj.note.id) @@ -163,7 +164,7 @@ export default { setNavBar (context, data) { context.commit('mutateNavBar', data) }, - setNoteMenu (context, data) { + setNoteList (context, data) { context.commit('mutateNotes', data) }, setNoteContent (context, data) { diff --git a/zeppelin-web-vue/src/stores/tab_manager_store.js b/zeppelin-web-vue/src/stores/tab_manager_store.js index 39731f1..4abf6c4 100644 --- a/zeppelin-web-vue/src/stores/tab_manager_store.js +++ b/zeppelin-web-vue/src/stores/tab_manager_store.js @@ -13,11 +13,11 @@ export default { }, mutations: { addTab (state, data) { - let isExist = state.tabs.filter(t => (t.path && t.path === data.path) || (!t.path && t.type === data.type)) - state.currentTab = data - if (isExist.length === 0) { + let filteredTab = state.tabs.filter(t => (t.path && t.path === data.path) || (!t.path && t.type === data.type)) + if (filteredTab.length === 0) { state.tabs.push(data) } + state.currentTab = state.tabs[state.tabs.length - 1] return state }, removeTab (state, data) {