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.majithia@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
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 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 @@
body {
color: #2c3e50;
+ font-size: 14px;
+ line-height: 1.5;
+
a {
text-decoration: none;
color: #333;
@@ -99,7 +104,7 @@
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 @@
// 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 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 @@
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 @@
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 @@
},
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,11 +55,38 @@
<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>
+
+ <a
+ href="javascript: void(0);"
+ @click="executeNoteCommand('restore-note')"
+ v-if="isDeleted"
+ >
+ <a-tooltip placement="top">
+ <template slot="title">
+ <span>Restore</span>
+ </template>
+ <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>Delete Permanently</span>
</template>
<a-icon type="delete" />
</a-tooltip>
@@ -65,6 +96,7 @@
<div class="right-controls">
<a
href="javascript: void(0);"
+ :disabled="isDeleted"
>
<a-tooltip placement="top">
<template slot="title">
@@ -76,6 +108,7 @@
<a
href="javascript: void(0);"
+ :disabled="isDeleted"
>
<a-tooltip placement="top">
<template slot="title">
@@ -85,19 +118,11 @@
</a-tooltip>
</a>
- <a
- href="javascript: void(0);"
- @click="executeNoteCommand('reload')"
+ <a-dropdown
+ :disabled="isDeleted"
+ :class="{disabled: isDeleted}"
+ :trigger="['click']"
>
- <a-tooltip placement="top">
- <template slot="title">
- <span>Reload</span>
- </template>
- <a-icon type="reload" />
- </a-tooltip>
- </a>
-
- <a-dropdown :trigger="['click']">
<a class="ant-dropdown-link" href="#">
<span> Default </span>
<a-icon type="down" />
@@ -124,19 +149,38 @@
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)
},
- showDeleteConfirm () {
+ showMoveToTrashConfirm () {
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.move_to_trash_confirm'),
+ content: that.$i18n.t('message.note.move_to_trash_content'),
onOk () {
- that.executeNoteCommand('delete-temporary')
+ that.executeNoteCommand('move-to-trash')
- that.$message.success(that.$i18n.t('message.note.move_to_rb_success'), 4)
+ 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 () {}
})
@@ -153,11 +197,17 @@
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 @@
export default {
name: 'NoteTree',
+ data () {
+ return {
+
+ }
+ },
mounted () {
ws.getConn().send({ op: 'LIST_NOTES' })
},
@@ -63,6 +111,31 @@
},
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 @@
}
}
+.search-notebook-box {
+ padding: 8px 6px;
+ width: calc(100%);
+}
+
.notes {
list-style: none;
margin: 0;
@@ -92,13 +170,24 @@
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 @@
&: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 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
})
},
- deleteTemporary (noteId) {
+ moveToTrash (noteId) {
wsHelper.getConn().send({
op: 'MOVE_NOTE_TO_TRASH',
data: {
@@ -120,10 +133,10 @@
})
// 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 @@
id: noteId
}
})
-
- // Reload the note list
- this.reloadList()
},
restore (noteId) {
@@ -145,8 +155,5 @@
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 @@
},
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 @@
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 @@
},
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) {