You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pinot.apache.org by ki...@apache.org on 2020/09/15 14:08:31 UTC

[incubator-pinot] branch master updated: Support for Update & Delete in ZooKeeper Browser and added SQL Functions in SQL Editor autocomplete list (#5981)

This is an automated email from the ASF dual-hosted git repository.

kishoreg pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new 5da3433  Support for Update & Delete in ZooKeeper Browser and added SQL Functions in SQL Editor autocomplete list (#5981)
5da3433 is described below

commit 5da34330bce20ea1f4b86b47b2b2266a764e29c1
Author: Sanket Shah <sh...@users.noreply.github.com>
AuthorDate: Tue Sep 15 19:38:16 2020 +0530

    Support for Update & Delete in ZooKeeper Browser and added SQL Functions in SQL Editor autocomplete list (#5981)
    
    * Adding api to edit ZK path
    
    * Adding delete api
    
    * Support for Update & Delete in ZooKeeper Browser and added SQL Functions in SQL Editor autocomplete list
    
    * showing notification on operation completion, display last refresh time, fixed refresh action
    
    Co-authored-by: kishoreg <g....@gmail.com>
---
 .../src/main/resources/app/components/Confirm.tsx  | 106 ++++++++++++++++
 .../resources/app/components/CustomCodemirror.tsx  |  66 ++++++++++
 .../app/components/Zookeeper/TreeDirectory.tsx     | 136 +++++++++++++++++++--
 .../src/main/resources/app/interfaces/types.d.ts   |   1 +
 .../src/main/resources/app/pages/Query.tsx         |   9 ++
 .../src/main/resources/app/pages/ZookeeperPage.tsx |  63 +++++-----
 .../src/main/resources/app/requests/index.ts       |  10 +-
 .../src/main/resources/app/styles/styles.css       |   5 +
 .../main/resources/app/utils/PinotMethodUtils.ts   |  22 +++-
 .../src/main/resources/app/utils/Utils.tsx         |  31 ++---
 .../src/main/resources/app/utils/axios-config.ts   |   2 +-
 11 files changed, 394 insertions(+), 57 deletions(-)

diff --git a/pinot-controller/src/main/resources/app/components/Confirm.tsx b/pinot-controller/src/main/resources/app/components/Confirm.tsx
new file mode 100644
index 0000000..bb11f99
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/components/Confirm.tsx
@@ -0,0 +1,106 @@
+/* eslint-disable no-nested-ternary */
+/**
+ * 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 React, { useEffect } from 'react';
+import { Button, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, makeStyles } from '@material-ui/core';
+import { green, red } from '@material-ui/core/colors';
+
+const useStyles = makeStyles((theme) => ({
+  dialogContent: {
+    minWidth: 900
+  },
+  dialogTextContent: {
+    fontWeight: 600
+  },
+  dialogActions: {
+    justifyContent: 'center'
+  },
+  green: {
+    fontWeight: 600,
+    color: green[500],
+    borderColor: green[500],
+    '&:hover': {
+      backgroundColor: green[50],
+      borderColor: green[500]
+    }
+  },
+  red: {
+    fontWeight: 600,
+    color: red[500],
+    borderColor: red[500],
+    '&:hover': {
+      backgroundColor: red[50],
+      borderColor: red[500]
+    }
+  }
+}));
+
+
+type Props = {
+  openDialog: boolean,
+  dialogTitle?: string,
+  dialogContent: string,
+  successCallback: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
+  closeDialog: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
+  dialogYesLabel?: string,
+  dialogNoLabel?: string
+};
+
+const Confirm = ({openDialog, dialogTitle, dialogContent, successCallback, closeDialog, dialogYesLabel, dialogNoLabel}: Props) => {
+  const classes = useStyles();
+  const [open, setOpen] = React.useState(openDialog);
+
+  useEffect(()=>{
+    setOpen(openDialog);
+  }, [openDialog])
+
+  const isStringDialog = typeof dialogContent === 'string';
+
+  return (
+    <div>
+      <Dialog
+        open={open}
+        onClose={closeDialog}
+        aria-labelledby="alert-dialog-title"
+        aria-describedby="alert-dialog-description"
+        maxWidth={false}
+      >
+        {dialogTitle && <DialogTitle id="alert-dialog-title">{dialogTitle}</DialogTitle>}
+        <DialogContent className={`${!isStringDialog ? classes.dialogContent : ""}`}>
+          {isStringDialog ?
+            <DialogContentText id="alert-dialog-description" className={classes.dialogTextContent}>
+              {dialogContent}
+            </DialogContentText>
+          : dialogContent}
+        </DialogContent>
+        <DialogActions style={{paddingBottom: 20}} className={`${isStringDialog ? classes.dialogActions : ""}`}>
+          <Button variant="outlined" onClick={closeDialog} color="secondary" className={classes.red}>
+            {dialogNoLabel || "No"}
+          </Button>
+          <Button variant="outlined" onClick={successCallback} color="primary" autoFocus className={classes.green}>
+            {dialogYesLabel || "Yes"}
+          </Button>
+        </DialogActions>
+      </Dialog>
+    </div>
+  );
+};
+
+export default Confirm;
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/components/CustomCodemirror.tsx b/pinot-controller/src/main/resources/app/components/CustomCodemirror.tsx
new file mode 100644
index 0000000..4e55dec
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/components/CustomCodemirror.tsx
@@ -0,0 +1,66 @@
+/* eslint-disable no-nested-ternary */
+/**
+ * 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 React, {  } from 'react';
+import { UnControlled as CodeMirror } from 'react-codemirror2';
+import 'codemirror/lib/codemirror.css';
+import 'codemirror/theme/material.css';
+import 'codemirror/mode/javascript/javascript'
+import { makeStyles } from '@material-ui/core';
+
+type Props = {
+  data: Object,
+  isEditable?: Object,
+  returnCodemirrorValue?: Function
+};
+
+const useStyles = makeStyles((theme) => ({
+  codeMirror: {
+    '& .CodeMirror': { height: 600, border: '1px solid #BDCCD9', fontSize: '13px' },
+  }
+}));
+
+const CustomCodemirror = ({data, isEditable, returnCodemirrorValue}: Props) => {
+  const classes = useStyles();
+
+  const jsonoptions = {
+    lineNumbers: true,
+    mode: 'application/json',
+    styleActiveLine: true,
+    gutters: ['CodeMirror-lint-markers'],
+    lint: true,
+    theme: 'default',
+    readOnly: !isEditable
+  };
+
+  return (
+    <CodeMirror
+      options={jsonoptions}
+      value={JSON.stringify(data, null , 2)}
+      className={classes.codeMirror}
+      autoCursor={false}
+      onChange={(editor, data, value) => {
+        returnCodemirrorValue && returnCodemirrorValue(value);
+      }}
+    />
+  );
+};
+
+export default CustomCodemirror;
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/components/Zookeeper/TreeDirectory.tsx b/pinot-controller/src/main/resources/app/components/Zookeeper/TreeDirectory.tsx
index 20074fb..10e5e6f 100644
--- a/pinot-controller/src/main/resources/app/components/Zookeeper/TreeDirectory.tsx
+++ b/pinot-controller/src/main/resources/app/components/Zookeeper/TreeDirectory.tsx
@@ -26,11 +26,20 @@ import RefreshOutlinedIcon from '@material-ui/icons/RefreshOutlined';
 import NoteAddOutlinedIcon from '@material-ui/icons/NoteAddOutlined';
 import DeleteOutlineOutlinedIcon from '@material-ui/icons/DeleteOutlineOutlined';
 import EditOutlinedIcon from '@material-ui/icons/EditOutlined';
-import { Grid, ButtonGroup, Button, Tooltip, Popover, Typography } from '@material-ui/core';
+import { Grid, ButtonGroup, Button, Tooltip, Popover, Typography, Snackbar } from '@material-ui/core';
 import MaterialTree from '../MaterialTree';
+import Confirm from '../Confirm';
+import CustomCodemirror from '../CustomCodemirror';
+import PinotMethodUtils from '../../utils/PinotMethodUtils';
+import Utils from '../../utils/Utils';
+import MuiAlert from '@material-ui/lab/Alert';
 
 const drawerWidth = 400;
 
+const Alert = (props) => {
+  return <MuiAlert elevation={6} variant="filled" {...props} />;
+}
+
 const useStyles = makeStyles((theme: Theme) =>
   createStyles({
     drawer: {
@@ -92,13 +101,29 @@ type Props = {
   selected: any;
   handleToggle: any;
   handleSelect: any;
-  refreshAction: Function;
+  isLeafNodeSelected: boolean;
+  currentNodeData: Object;
+  currentNodeMetadata: any;
+  showInfoEvent: Function;
+  fetchInnerPath: Function;
 };
 
-const TreeDirectory = ({treeData, showChildEvent, expanded, selected, handleToggle, handleSelect, refreshAction}: Props) => {
+const TreeDirectory = ({
+  treeData, showChildEvent, selectedNode, expanded, selected, handleToggle, fetchInnerPath,
+  handleSelect, isLeafNodeSelected, currentNodeData, currentNodeMetadata, showInfoEvent
+}: Props) => {
   const classes = useStyles();
 
+  let newCodeMirrorData = null;
+  const [confirmDialog, setConfirmDialog] = React.useState(false);
+  const [dialogTitle, setDialogTitle] = React.useState(null);
+  const [dialogContent, setDialogContent] = React.useState(null);
+  const [dialogSuccessCb, setDialogSuccessCb] = React.useState(null);
+  const [dialogYesLabel, setDialogYesLabel] = React.useState(null);
+  const [dialogNoLabel, setDialogNoLabel] = React.useState(null);
   const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
+  const [notificationData, setNotificationData] = React.useState({type: '', message: ''});
+  const [showNotification, setShowNotification] = React.useState(false);
 
   const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
     setAnchorEl(event.currentTarget);
@@ -108,6 +133,83 @@ const TreeDirectory = ({treeData, showChildEvent, expanded, selected, handleTogg
     setAnchorEl(null);
   };
 
+  const handleEditClick = (event: React.MouseEvent<HTMLButtonElement>) => {
+    if(!isLeafNodeSelected){
+      return;
+    }
+    setDialogTitle("Update Node Data");
+    setDialogContent(<CustomCodemirror
+      data={currentNodeData}
+      isEditable={true}
+      returnCodemirrorValue={(val)=>{ newCodeMirrorData = val;}}
+    />)
+    setDialogYesLabel("Update");
+    setDialogNoLabel("Cancel");
+    setDialogSuccessCb(() => confirmUpdate);
+    setConfirmDialog(true);
+  };
+
+  const handleDeleteClick = (event: React.MouseEvent<HTMLButtonElement>) => {
+    if(!isLeafNodeSelected){
+      return;
+    }
+    setDialogContent("Delete this node?");
+    setDialogSuccessCb(() => deleteNode);
+    setConfirmDialog(true);
+  };
+
+  const confirmUpdate = () => {
+    setDialogYesLabel("Yes");
+    setDialogNoLabel("No");
+    setDialogContent("Are you sure want to update this node?");
+    setDialogSuccessCb(() => updateNode);
+  }
+
+  const updateNode = async () => {
+    const nodeData = {
+      path: selectedNode,
+      data: newCodeMirrorData.trim(),
+      expectedVersion: currentNodeMetadata.version,
+      accessOption: currentNodeMetadata.ephemeralOwner === 0 ? 1 : 10
+    }
+    const result = await PinotMethodUtils.putNodeData(nodeData);
+    if(result.data.status){
+      setNotificationData({type: 'success', message: result.data.status})
+      showInfoEvent(selectedNode);
+    } else {
+      setNotificationData({type: 'error', message: result.data.error})
+    }
+    setShowNotification(true);
+    closeDialog();
+  }
+
+  const deleteNode = async () => {
+    const parentPath = selectedNode.split('/').slice(0, selectedNode.split('/').length-1).join('/');
+    const treeObj = Utils.findNestedObj(treeData, 'fullPath', parentPath);
+    const result = await PinotMethodUtils.deleteNode(selectedNode);
+    if(result.data.status){
+      setNotificationData({type: 'success', message: result.data.status})
+      showInfoEvent(selectedNode);
+      fetchInnerPath(treeObj);
+    } else {
+      setNotificationData({type: 'error', message: result.data.error})
+    }
+    setShowNotification(true);
+    closeDialog();
+  }
+
+  const closeDialog = () => {
+    setConfirmDialog(false);
+    setDialogContent(null);
+    setDialogTitle(null);
+    setDialogYesLabel(null);
+    setDialogNoLabel(null);
+  };
+
+  const hideNotification = () => {
+    setShowNotification(false);
+  }
+
   const open = Boolean(anchorEl);
   const id = open ? 'simple-popover' : undefined;
 
@@ -127,16 +229,16 @@ const TreeDirectory = ({treeData, showChildEvent, expanded, selected, handleTogg
             <div className={classes.buttonGrpDiv}>
               <ButtonGroup color="primary" aria-label="outlined primary button group" className={classes.btnGroup}>
                 <Tooltip title="Refresh">
-                  <Button onClick={(e)=>{refreshAction();}}><RefreshOutlinedIcon/></Button>
+                  <Button onClick={(e)=>{showInfoEvent(selectedNode);}}><RefreshOutlinedIcon/></Button>
                 </Tooltip>
                 <Tooltip title="Add">
                   <Button onClick={handleClick}><NoteAddOutlinedIcon/></Button>
                 </Tooltip>
-                <Tooltip title="Delete">
-                  <Button onClick={handleClick}><DeleteOutlineOutlinedIcon/></Button>
+                <Tooltip title="Delete" open={false}>
+                  <Button onClick={handleDeleteClick} disabled={!isLeafNodeSelected}><DeleteOutlineOutlinedIcon/></Button>
                 </Tooltip>
-                <Tooltip title="Edit">
-                  <Button onClick={handleClick}><EditOutlinedIcon/></Button>
+                <Tooltip title="Edit" open={false}>
+                  <Button onClick={handleEditClick} disabled={!isLeafNodeSelected}><EditOutlinedIcon/></Button>
                 </Tooltip>
               </ButtonGroup>
             </div>
@@ -168,6 +270,24 @@ const TreeDirectory = ({treeData, showChildEvent, expanded, selected, handleTogg
           </Grid>
         </div>
       </Drawer>
+      <Confirm
+        openDialog={confirmDialog}
+        dialogTitle={dialogTitle}
+        dialogContent={dialogContent}
+        successCallback={dialogSuccessCb}
+        closeDialog={closeDialog}
+        dialogYesLabel={dialogYesLabel}
+        dialogNoLabel={dialogNoLabel}
+      />
+      <Snackbar
+        anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
+        open={showNotification}
+        onClose={hideNotification}
+        key="notification"
+        autoHideDuration={3000}
+      >
+        <Alert severity={notificationData.type}>{notificationData.message}</Alert>
+      </Snackbar>
     </>
   );
 };
diff --git a/pinot-controller/src/main/resources/app/interfaces/types.d.ts b/pinot-controller/src/main/resources/app/interfaces/types.d.ts
index b46f261..4199174 100644
--- a/pinot-controller/src/main/resources/app/interfaces/types.d.ts
+++ b/pinot-controller/src/main/resources/app/interfaces/types.d.ts
@@ -116,4 +116,5 @@ declare module 'Models' {
   export type ZKGetList = Array<string>
 
   export type ZKConfig = Object;
+  export type ZKOperationResponsne = any;
 }
diff --git a/pinot-controller/src/main/resources/app/pages/Query.tsx b/pinot-controller/src/main/resources/app/pages/Query.tsx
index e3b67f5..53fd15d 100644
--- a/pinot-controller/src/main/resources/app/pages/Query.tsx
+++ b/pinot-controller/src/main/resources/app/pages/Query.tsx
@@ -109,6 +109,13 @@ const sqloptions = {
   extraKeys: { "'@'": 'autocomplete' },
 };
 
+const sqlFuntionsList = [
+  "COUNT", "MIN", "MAX", "SUM", "AVG", "MINMAXRANGE", "DISTINCTCOUNT", "DISTINCTCOUNTBITMAP",
+  "SEGMENTPARTITIONEDDISTINCTCOUNT", "DISTINCTCOUNTHLL", "DISTINCTCOUNTRAWHLL", "FASTHLL",
+  "DISTINCTCOUNTTHETASKETCH", "DISTINCTCOUNTRAWTHETASKETCH", "COUNTMV", "MINMV", "MAXMV",
+  "SUMMV", "AVGMV", "MINMAXRANGEMV", "DISTINCTCOUNTMV", "DISTINCTCOUNTBITMAPMV", "DISTINCTCOUNTHLLMV",
+  "DISTINCTCOUNTRAWHLLMV", "DISTINCT", "ST_UNION"];
+
 const QueryPage = () => {
   const classes = useStyles();
   const [fetching, setFetching] = useState(true);
@@ -252,6 +259,7 @@ const QueryPage = () => {
 
     Array.prototype.push.apply(hintOptions, Utils.generateCodeMirrorOptions(tableNames, 'TABLE'));
     Array.prototype.push.apply(hintOptions, Utils.generateCodeMirrorOptions(columnNames, 'COLUMNS'));
+    Array.prototype.push.apply(hintOptions, Utils.generateCodeMirrorOptions(sqlFuntionsList, 'FUNCTION'));
 
     const cur = cm.getCursor();
     const curLine = cm.getLine(cur.line);
@@ -270,6 +278,7 @@ const QueryPage = () => {
 
     Array.prototype.push.apply(defaultHint.list, finalList);
 
+    defaultHint.list = _.uniqBy(defaultHint.list, 'text');
     return defaultHint;
   };
 
diff --git a/pinot-controller/src/main/resources/app/pages/ZookeeperPage.tsx b/pinot-controller/src/main/resources/app/pages/ZookeeperPage.tsx
index 8a88b16..fa83c33 100644
--- a/pinot-controller/src/main/resources/app/pages/ZookeeperPage.tsx
+++ b/pinot-controller/src/main/resources/app/pages/ZookeeperPage.tsx
@@ -21,16 +21,13 @@
 import React, { useEffect, useState } from 'react';
 import { makeStyles, useTheme } from '@material-ui/core/styles';
 import { Grid, Paper, Tabs, Tab } from '@material-ui/core';
-import { UnControlled as CodeMirror } from 'react-codemirror2';
-import 'codemirror/lib/codemirror.css';
-import 'codemirror/theme/material.css';
-import 'codemirror/mode/javascript/javascript';
 import _ from 'lodash';
 import AppLoader from '../components/AppLoader';
 import PinotMethodUtils from '../utils/PinotMethodUtils';
 import TreeDirectory from '../components/Zookeeper/TreeDirectory';
 import TabPanel from '../components/TabPanel';
 import Utils from '../utils/Utils';
+import CustomCodemirror from '../components/CustomCodemirror';
 
 const useStyles = makeStyles((theme) => ({
   root:{
@@ -48,21 +45,12 @@ const useStyles = makeStyles((theme) => ({
     borderRadius: 4,
     marginBottom: '20px',
   },
-  codeMirror: {
-    '& .CodeMirror': { height: 600, border: '1px solid #BDCCD9', fontSize: '13px' },
+  lastRefreshDiv: {
+    direction: 'rtl',
+    margin: '-15px 0'
   }
 }));
 
-const jsonoptions = {
-  lineNumbers: true,
-  mode: 'application/json',
-  styleActiveLine: true,
-  gutters: ['CodeMirror-lint-markers'],
-  lint: true,
-  theme: 'default',
-  readOnly: true
-};
-
 const ZookeeperPage = () => {
   const classes = useStyles();
   const theme = useTheme();
@@ -72,10 +60,12 @@ const ZookeeperPage = () => {
   const [currentNodeMetadata, setCurrentNodeMetadata] =  useState({});
   const [selectedNode, setSelectedNode] =  useState(null);
   const [count, setCount] = useState(1);
+  const [leafNode, setLeafNode] = useState(false);
 
   // states and handlers for toggle and select of tree
   const [expanded, setExpanded] = React.useState<string[]>(["1"]);
   const [selected, setSelected] = React.useState<string[]>(["1"]);
+  const [lastRefresh, setLastRefresh] = React.useState(null);
 
   const handleToggle = (event: React.ChangeEvent<{}>, nodeIds: string[]) => {
     setExpanded(nodeIds);
@@ -86,6 +76,8 @@ const ZookeeperPage = () => {
       setSelected(nodeIds);
       const treeObj = Utils.findNestedObj(treeData, 'nodeId', nodeIds);
       if(treeObj){
+        setLeafNode(treeObj.isLeafNode);
+        setSelectedNode(treeObj.fullPath || '/');
         showInfoEvent(treeObj.fullPath || '/');
       }
     }
@@ -96,6 +88,7 @@ const ZookeeperPage = () => {
     const { currentNodeData, currentNodeMetadata } = await PinotMethodUtils.getNodeData(fullPath);
     setCurrentNodeData(currentNodeData);
     setCurrentNodeMetadata(currentNodeMetadata);
+    setLastRefresh(new Date());
   }
 
   // handlers for Tabs
@@ -114,6 +107,7 @@ const ZookeeperPage = () => {
   const fetchInnerPath = async (pathObj) => {
     const {newTreeData, currentNodeData, currentNodeMetadata, counter } = await PinotMethodUtils.getZookeeperData(pathObj.fullPath, count);
     pathObj.child = newTreeData[0].child;
+    pathObj.isLeafNode = newTreeData[0].child.length === 0;
     pathObj.hasChildRendered = true;
     // setting the old treeData again here since pathObj has the reference of old treeData
     // and newTreeData is not useful here.
@@ -135,6 +129,7 @@ const ZookeeperPage = () => {
     setCount(counter);
     setExpanded(["1"]);
     setSelected(["1"]);
+    setLastRefresh(new Date());
     setFetching(false);
   };
 
@@ -142,6 +137,20 @@ const ZookeeperPage = () => {
     fetchData();
   }, []);
 
+  const renderLastRefresh = () => (
+    <div className={classes.lastRefreshDiv}>
+      <p>
+        {`Last Refreshed: ${lastRefresh.toLocaleTimeString("en-US",{
+            hour12: true,
+            hour: 'numeric',
+            minute: '2-digit',
+            second: '2-digit'
+          })}
+        `}
+      </p>
+    </div>
+  )
+
   return fetching ? (
     <AppLoader />
   ) : (
@@ -155,7 +164,11 @@ const ZookeeperPage = () => {
           selected={selected}
           handleToggle={handleToggle}
           handleSelect={handleSelect}
-          refreshAction={fetchData}
+          isLeafNodeSelected={leafNode}
+          currentNodeData={currentNodeData}
+          currentNodeMetadata={currentNodeMetadata}
+          showInfoEvent={showInfoEvent}
+          fetchInnerPath={fetchInnerPath}
         />
       </Grid>
       <Grid item xs style={{ padding: 20, backgroundColor: 'white', maxHeight: 'calc(100vh - 70px)', overflowY: 'auto' }}>
@@ -178,23 +191,15 @@ const ZookeeperPage = () => {
               index={0}
               dir={theme.direction}
             >
+              {lastRefresh && renderLastRefresh()}
               <div className={classes.codeMirrorDiv}>
-                <CodeMirror
-                  options={jsonoptions}
-                  value={JSON.stringify(currentNodeData, null , 2)}
-                  className={classes.codeMirror}
-                  autoCursor={false}
-                />
+                <CustomCodemirror data={currentNodeData}/>
               </div>
             </TabPanel>
             <TabPanel value={value} index={1} dir={theme.direction}>
+              {lastRefresh && renderLastRefresh()}
               <div className={classes.codeMirrorDiv}>
-                <CodeMirror
-                  options={jsonoptions}
-                  value={JSON.stringify(currentNodeMetadata, null , 2)}
-                  className={classes.codeMirror}
-                  autoCursor={false}
-                />
+                <CustomCodemirror data={currentNodeMetadata}/>
               </div>
             </TabPanel>
           </Grid>
diff --git a/pinot-controller/src/main/resources/app/requests/index.ts b/pinot-controller/src/main/resources/app/requests/index.ts
index 4b6e239..373c9e7 100644
--- a/pinot-controller/src/main/resources/app/requests/index.ts
+++ b/pinot-controller/src/main/resources/app/requests/index.ts
@@ -19,7 +19,7 @@
 
 import { AxiosResponse } from 'axios';
 import { TableData, Instances, Instance, Tenants, ClusterConfig, TableName, TableSize,
-  IdealState, QueryTables, TableSchema, SQLResult, ClusterName, ZKGetList, ZKConfig
+  IdealState, QueryTables, TableSchema, SQLResult, ClusterName, ZKGetList, ZKConfig, ZKOperationResponsne
 } from 'Models';
 import { baseApi } from '../utils/axios-config';
 
@@ -78,4 +78,10 @@ export const zookeeperGetStat = (params: string): Promise<AxiosResponse<ZKConfig
   baseApi.get(`/zk/stat?path=${params}`);
 
 export const zookeeperGetListWithStat = (params: string): Promise<AxiosResponse<ZKConfig>> =>
-  baseApi.get(`/zk/lsl?path=${params}`);
\ No newline at end of file
+  baseApi.get(`/zk/lsl?path=${params}`);
+
+export const zookeeperPutData = (params: string): Promise<AxiosResponse<ZKOperationResponsne>> =>
+  baseApi.put(`/zk/put?${params}`, null, { headers: { 'Content-Type': 'application/json; charset=UTF-8', 'Accept': 'text/plain, */*; q=0.01' } });
+
+export const zookeeperDeleteNode = (params: string): Promise<AxiosResponse<ZKOperationResponsne>> =>
+  baseApi.delete(`/zk/delete?path=${params}`);
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/styles/styles.css b/pinot-controller/src/main/resources/app/styles/styles.css
index d3c2d97..5415efc 100644
--- a/pinot-controller/src/main/resources/app/styles/styles.css
+++ b/pinot-controller/src/main/resources/app/styles/styles.css
@@ -68,6 +68,11 @@ li.codemirror-column.CodeMirror-hint::before {
   background: #05a;
 }
 
+li.codemirror-func.CodeMirror-hint::before {
+  content: "F";
+  background: #74457a;
+}
+
 .CodeMirror-hints {
   padding: 4px;
   box-shadow: 0px 0px 5px rgba(0,0,0,.2);
diff --git a/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts b/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
index 30947cf..35a2c9e 100644
--- a/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
+++ b/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
@@ -38,7 +38,9 @@ import {
   zookeeperGetList,
   zookeeperGetData,
   zookeeperGetListWithStat,
-  zookeeperGetStat
+  zookeeperGetStat,
+  zookeeperPutData,
+  zookeeperDeleteNode
 } from '../requests';
 import Utils from './Utils';
 
@@ -546,6 +548,20 @@ const getNodeData = (path) => {
   });
 };
 
+const putNodeData = (data) => {
+  const serializedData = Utils.serialize(data);
+  return zookeeperPutData(serializedData).then((obj)=>{
+    return obj;
+  });
+};
+
+const deleteNode = (path) => {
+  const params = encodeURIComponent(path);
+  return zookeeperDeleteNode(params).then((obj)=>{
+    return obj;
+  });
+};
+
 export default {
   getTenantsData,
   getAllInstances,
@@ -565,5 +581,7 @@ export default {
   getInstanceConfig,
   getTenantsFromInstance,
   getZookeeperData,
-  getNodeData
+  getNodeData,
+  putNodeData,
+  deleteNode
 };
diff --git a/pinot-controller/src/main/resources/app/utils/Utils.tsx b/pinot-controller/src/main/resources/app/utils/Utils.tsx
index 7f80dfb..c859a96 100644
--- a/pinot-controller/src/main/resources/app/utils/Utils.tsx
+++ b/pinot-controller/src/main/resources/app/utils/Utils.tsx
@@ -102,8 +102,6 @@ const generateCodeMirrorOptions = (array, type, modeType?) => {
         filterText: oldObj
           ? `${oldObj.filterText}.${a.displayName || a.name || a}`
           : a.displayName || a.name || a,
-        argsType: '',
-        description: '',
         render: (el, cm, data) => {},
         className:
           type === 'FUNCTION'
@@ -124,12 +122,6 @@ const generateCodeMirrorOptions = (array, type, modeType?) => {
             : type === 'BINARY-OPERATORS'
               ? 'Binary Operators'
               : a.type;
-      if (type === 'FUNCTION') {
-        obj.argsType = a.argTypes.toString();
-        obj.description = a.description
-          ? `Description: ${a.description}`
-          : undefined;
-      }
       obj.render = (el, cm, data) => {
         codeMirrorOptionsTemplate(el, data);
       };
@@ -221,12 +213,6 @@ const codeMirrorOptionsTemplate = (el, data) => {
   fNameSpan.setAttribute('class', 'funcText');
   fNameSpan.innerHTML = data.displayText;
 
-  // data.argsType is only for UDF Function
-  if (data.argsType && data.argsType.length) {
-    const paramSpan = document.createElement('span');
-    paramSpan.innerHTML = `(${data.argsType})`;
-    fNameSpan.appendChild(paramSpan);
-  }
   text.appendChild(fNameSpan);
   el.appendChild(text);
 
@@ -242,10 +228,25 @@ const codeMirrorOptionsTemplate = (el, data) => {
   }
 };
 
+const serialize = (obj: any, prefix?: any) => {
+  let str = [], p;
+  for (p in obj) {
+    if (obj.hasOwnProperty(p)) {
+      var k = prefix ? prefix + "[" + p + "]" : p,
+        v = obj[p];
+      str.push((v !== null && typeof v === "object") ?
+        serialize(v, k) :
+        encodeURIComponent(k) + "=" + encodeURIComponent(v));
+    }
+  }
+  return str.join("&");
+}
+
 export default {
   sortArray,
   tableFormat,
   getSegmentStatus,
   findNestedObj,
-  generateCodeMirrorOptions
+  generateCodeMirrorOptions,
+  serialize
 };
diff --git a/pinot-controller/src/main/resources/app/utils/axios-config.ts b/pinot-controller/src/main/resources/app/utils/axios-config.ts
index 3d2b924..2277066 100644
--- a/pinot-controller/src/main/resources/app/utils/axios-config.ts
+++ b/pinot-controller/src/main/resources/app/utils/axios-config.ts
@@ -27,7 +27,7 @@ const handleError = (error: any) => {
   if (isDev) {
     console.log(error);
   }
-  return error;
+  return error.response || error;
 };
 
 const handleResponse = (response: any) => {


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org