[Feature][UI] Toggle layout. (#11)

diff --git a/studio/components/log/index.module.scss b/studio/components/log/index.module.scss
index ad16ccc..d783027 100644
--- a/studio/components/log/index.module.scss
+++ b/studio/components/log/index.module.scss
@@ -28,3 +28,6 @@
     }
   }
 }
+.log-wrap {
+  position: relative;
+}
diff --git a/studio/components/log/index.tsx b/studio/components/log/index.tsx
index f319cd1..c4758a4 100644
--- a/studio/components/log/index.tsx
+++ b/studio/components/log/index.tsx
@@ -17,6 +17,12 @@
 
 import { NTabs, NTabPane, NLog, NConfigProvider } from 'naive-ui'
 import { defineComponent, PropType } from 'vue'
+import {
+  ResizeHandler,
+  ResizedOptions,
+  HandlerPlacement
+} from '../resize-handler'
+import { useLayoutStore } from '@/store/layout'
 import hljs from 'highlight.js/lib/core'
 import styles from './index.module.scss'
 
@@ -48,15 +54,30 @@
       ]
     }))
 
+    const layoutStore = useLayoutStore()
+
+    const onResized = (resized: ResizedOptions) => {
+      let height = layoutStore.editorHeight - resized.y
+      if (height < 40) height = 35
+      if (height > layoutStore.editorHeight) height = layoutStore.editorHeight
+      layoutStore.setLogHeight(height)
+    }
+
     return () => {
       return (
-        <NTabs type='card' closable size='small'>
-          <NTabPane name='运行日志'>
-            <NConfigProvider hljs={hljs} class={styles.hljs}>
-              <NLog log={props.value} language='studio-log' />
-            </NConfigProvider>
-          </NTabPane>
-        </NTabs>
+        <div
+          class={styles['log-wrap']}
+          style={{ height: `${layoutStore.getLogHeight}px` }}
+        >
+          <NTabs type='card' closable size='small'>
+            <NTabPane name='运行日志'>
+              <NConfigProvider hljs={hljs} class={styles.hljs}>
+                <NLog log={props.value} language='studio-log' />
+              </NConfigProvider>
+            </NTabPane>
+          </NTabs>
+          <ResizeHandler placement={HandlerPlacement.T} onResized={onResized} />
+        </div>
       )
     }
   }
diff --git a/studio/components/monaco/index.tsx b/studio/components/monaco/index.tsx
index a60c3ff..b1d85c7 100644
--- a/studio/components/monaco/index.tsx
+++ b/studio/components/monaco/index.tsx
@@ -19,7 +19,7 @@
 import * as monaco from 'monaco-editor'
 import { useFormItem } from 'naive-ui/es/_mixins'
 import { call } from 'naive-ui/es/_utils'
-import { ResizeHandler, ResizedOptions } from '../resize-handler'
+import { useLayoutStore } from '@/store/layout'
 import type {
   MaybeArray,
   OnUpdateValue,
@@ -51,10 +51,10 @@
   props,
   emits: ['change', 'focus', 'blur'],
   setup(props, ctx) {
-    const heightRef = ref(450)
     const editorRef = ref()
     let editor = null as monaco.editor.IStandaloneCodeEditor | null
     const formItem = useFormItem({})
+    const layoutStore = useLayoutStore()
 
     const initMonacoEditor = () => {
       const dom = editorRef.value
@@ -89,26 +89,16 @@
 
     onMounted(() => initMonacoEditor())
     return () => (
-      <>
-        <div
-          ref={editorRef}
-          style={{
-            height: `${heightRef.value}px`,
-            width: '100%',
-            border: '1px solid #eee'
-          }}
-        />
-        <ResizeHandler
-          placement='y'
-          onResized={(resized: ResizedOptions) => {
-            let height = resized.y
-            if (height < 100) height = 100
-            if (height > window.innerHeight * 0.5)
-              height = window.innerHeight * 0.5
-            heightRef.value = height
-          }}
-        />
-      </>
+      <div
+        ref={editorRef}
+        style={{
+          height: `${
+            layoutStore.getEditorHeight - layoutStore.getLogHeight - 90
+          }px`,
+          width: '100%',
+          border: '1px solid #eee'
+        }}
+      />
     )
   }
 })
diff --git a/studio/components/resize-handler/index.tsx b/studio/components/resize-handler/index.tsx
index 9872934..1d55809 100644
--- a/studio/components/resize-handler/index.tsx
+++ b/studio/components/resize-handler/index.tsx
@@ -27,6 +27,7 @@
 }
 
 export { ResizedOptions }
+export { HandlerPlacement } from './types'
 
 export const ResizeHandler = defineComponent({
   name: 'resize-handler',
@@ -52,7 +53,7 @@
     const classes = getClasses(placement)
 
     const onMouseDown = (ev: MouseEvent) => {
-      document.body.style[`user-select-${placement}` as any] = 'none'
+      document.body.style[`user-select` as any] = 'none'
       document.addEventListener('mousemove', onMouseMove)
       document.addEventListener('mouseup', onMouseUp)
     }
@@ -65,7 +66,7 @@
     const onMouseUp = (ev: MouseEvent) => {
       document.removeEventListener('mousemove', onMouseMove)
       document.removeEventListener('mouseup', onMouseUp)
-      document.body.style[`user-select-${placement}` as any] = 'auto'
+      document.body.style[`user-select` as any] = 'auto'
     }
 
     onMounted(() => {
diff --git a/studio/components/studio-content/index.module.scss b/studio/components/studio-content/index.module.scss
index 2fab968..a620cdb 100644
--- a/studio/components/studio-content/index.module.scss
+++ b/studio/components/studio-content/index.module.scss
@@ -22,8 +22,14 @@
 .editor {
   display: flex;
   flex-direction: column;
+  height: 100%;
 }
 
 .tab {
   flex: 1;
+  :global {
+    .n-tabs {
+      height: 100%;
+    }
+  }
 }
diff --git a/studio/components/studio-content/index.tsx b/studio/components/studio-content/index.tsx
index 56adb6c..d8ede37 100644
--- a/studio/components/studio-content/index.tsx
+++ b/studio/components/studio-content/index.tsx
@@ -15,19 +15,25 @@
  * limitations under the License.
  */
 
-import { defineComponent } from 'vue'
+import { defineComponent, ref, onMounted } from 'vue'
 import { NDialogProvider, NLayoutContent } from 'naive-ui'
 import { Toolbar } from '../toolbar'
 import { Tabs } from '../tab'
+import { useLayoutStore } from '@/store/layout'
 import styles from './index.module.scss'
 
 export const StudioContent = defineComponent({
   name: 'studio-content',
   setup() {
+    const editorRef = ref()
+    const layoutStore = useLayoutStore()
+    onMounted(() => {
+      layoutStore.setEditorHeight(editorRef.value.clientHeight)
+    })
     return () => (
       <NDialogProvider>
         <NLayoutContent class={styles['studio-content']}>
-          <div class={styles['editor']}>
+          <div class={styles['editor']} ref={editorRef}>
             <Toolbar />
             <div class={styles['tab']}>
               <Tabs />
diff --git a/studio/components/studio-header/index.module.scss b/studio/components/studio-header/index.module.scss
index 0d6163f..c9b9f8f 100644
--- a/studio/components/studio-header/index.module.scss
+++ b/studio/components/studio-header/index.module.scss
@@ -15,6 +15,21 @@
  * limitations under the License.
  */
 
+@mixin icon() {
+  width: 18px;
+  height: 18px;
+  border: 1px solid var(--n-text-color);
+  &::before {
+    content: '';
+    display: block;
+  }
+}
+@mixin label-icon() {
+  @include icon();
+  margin-top: 6px;
+  border: 1px solid var(--n-option-text-color-active);
+}
+
 .studio-header {
   height: 40px;
   line-height: 40px;
@@ -23,3 +38,30 @@
   position: relative;
   z-index: 30;
 }
+.icon-button {
+  margin-top: 2px;
+}
+.icon-vertical {
+  @include icon();
+  &::before {
+    width: 6px;
+    height: 18px;
+    background-color: var(--n-text-color);
+  }
+}
+.label-icon-vertical {
+  @include label-icon();
+  &::before {
+    width: 6px;
+    height: 18px;
+    border-right: 1px solid var(--n-option-text-color-active);
+  }
+}
+.label-icon-horizontal {
+  @include label-icon();
+  &::before {
+    width: 18px;
+    height: 12px;
+    border-bottom: 1px solid var(--n-option-text-color-active);
+  }
+}
diff --git a/studio/components/studio-header/index.tsx b/studio/components/studio-header/index.tsx
index 6e029dd..a4f2b62 100644
--- a/studio/components/studio-header/index.tsx
+++ b/studio/components/studio-header/index.tsx
@@ -14,19 +14,56 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { defineComponent } from 'vue'
-import { NLayoutHeader, NGradientText, NSpace } from 'naive-ui'
+import { defineComponent, h } from 'vue'
+import {
+  NLayoutHeader,
+  NGradientText,
+  NSpace,
+  NDropdown,
+  NButton
+} from 'naive-ui'
+import { useLayoutStore } from '@/store/layout'
 import styles from './index.module.scss'
 
 export const StudioHeader = defineComponent({
   name: 'studio-header',
   setup() {
+    const layoutStore = useLayoutStore()
+    const onSelect = (key: string) => {
+      if (key === '1') {
+        layoutStore.toggleSider()
+        return
+      }
+      if (key === '2') {
+        layoutStore.toggleLog()
+        return
+      }
+    }
     return () => (
       <NLayoutHeader class={styles['studio-header']}>
         <NSpace justify='space-between' align='center'>
           <NGradientText type='primary' size={20}>
             DolphinScheduler Studio
           </NGradientText>
+          <NDropdown
+            trigger='click'
+            onSelect={onSelect}
+            options={[
+              {
+                key: '1',
+                label: () => h('div', { class: styles['label-icon-vertical'] })
+              },
+              {
+                key: '2',
+                label: () =>
+                  h('div', { class: styles['label-icon-horizontal'] })
+              }
+            ]}
+          >
+            <NButton quaternary type='primary' class={styles['icon-button']}>
+              <div class={styles['icon-vertical']}></div>
+            </NButton>
+          </NDropdown>
         </NSpace>
       </NLayoutHeader>
     )
diff --git a/studio/components/studio-sider/index.tsx b/studio/components/studio-sider/index.tsx
index b1b725e..eb8c2ae 100644
--- a/studio/components/studio-sider/index.tsx
+++ b/studio/components/studio-sider/index.tsx
@@ -19,14 +19,15 @@
 import { ResizeHandler, ResizedOptions } from '../resize-handler'
 import { SearchBar, Files } from '@/components'
 import { useFile } from './use-file'
+import { useLayoutStore } from '@/store/layout'
 import styles from './index.module.scss'
 
 export const StudioSider = defineComponent({
   name: 'studio-sider',
   setup() {
-    const widthRef = ref(300)
     const inputRef = ref()
     const fileRef = ref()
+    const layoutStore = useLayoutStore()
     const {
       state,
       onCreateFile,
@@ -37,8 +38,18 @@
       onRename
     } = useFile(inputRef, fileRef)
 
+    const onResized = (resized: ResizedOptions) => {
+      let width = resized.x
+      if (width < 100) width = 100
+      if (width > window.innerWidth * 0.5) width = window.innerWidth * 0.5
+      layoutStore.setSiderWidth(width)
+    }
+
     return () => (
-      <NLayoutSider class={styles['studio-sider']} width={widthRef.value}>
+      <NLayoutSider
+        class={styles['studio-sider']}
+        width={layoutStore.getSiderWidth}
+      >
         <NSpace
           vertical
           class={styles['studio-sider-content']}
@@ -59,14 +70,7 @@
             ref={fileRef}
           />
         </NSpace>
-        <ResizeHandler
-          onResized={(resized: ResizedOptions) => {
-            let width = resized.x
-            if (width < 100) width = 100
-            if (width > window.innerWidth * 0.5) width = window.innerWidth * 0.5
-            widthRef.value = width
-          }}
-        />
+        <ResizeHandler onResized={onResized} />
       </NLayoutSider>
     )
   }
diff --git a/studio/store/layout/index.ts b/studio/store/layout/index.ts
new file mode 100644
index 0000000..cc06023
--- /dev/null
+++ b/studio/store/layout/index.ts
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { defineStore } from 'pinia'
+import type { ILayoutState } from './types'
+
+export const useLayoutStore = defineStore({
+  id: 'layout',
+  state: (): ILayoutState => ({
+    siderWidth: 300,
+    prevSiderWidth: 300,
+    logHeight: 400,
+    prevLogHeight: 400,
+    editorHeight: 0
+  }),
+  persist: true,
+  getters: {
+    getSiderWidth(): number {
+      return this.siderWidth
+    },
+    getLogHeight(): number {
+      return this.logHeight
+    },
+    getEditorHeight(): number {
+      return this.editorHeight
+    }
+  },
+  actions: {
+    toggleSider() {
+      if (this.siderWidth) this.prevSiderWidth = this.siderWidth
+      this.siderWidth = this.siderWidth ? 0 : this.prevSiderWidth
+    },
+    setSiderWidth(siderWidth: number) {
+      this.siderWidth = siderWidth
+    },
+    toggleLog() {
+      if (this.logHeight) this.prevLogHeight = this.logHeight
+      this.logHeight = this.logHeight ? 0 : this.prevLogHeight
+    },
+    setLogHeight(logHeight: number) {
+      this.logHeight = logHeight
+    },
+    setEditorHeight(editorHeight: number) {
+      this.editorHeight = editorHeight
+    }
+  }
+})
diff --git a/studio/store/layout/types.ts b/studio/store/layout/types.ts
new file mode 100644
index 0000000..01a9a2e
--- /dev/null
+++ b/studio/store/layout/types.ts
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+interface ILayoutState {
+  siderWidth: number
+  prevSiderWidth: number
+  logHeight: number
+  prevLogHeight: number
+  editorHeight: number
+}
+
+export type { ILayoutState }