Skip to content

Commit

Permalink
Support for Update & Delete in ZooKeeper Browser and added SQL Functi…
Browse files Browse the repository at this point in the history
…ons 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.kishore@gmail.com>
  • Loading branch information
shahsank3t and kishoreg committed Sep 15, 2020
1 parent 054faf7 commit 5da3433
Show file tree
Hide file tree
Showing 11 changed files with 394 additions and 57 deletions.
106 changes: 106 additions & 0 deletions 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;
@@ -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;
Expand Up @@ -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: {
Expand Down Expand Up @@ -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);
Expand All @@ -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;

Expand All @@ -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>
Expand Down Expand Up @@ -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>
</>
);
};
Expand Down
Expand Up @@ -116,4 +116,5 @@ declare module 'Models' {
export type ZKGetList = Array<string>

export type ZKConfig = Object;
export type ZKOperationResponsne = any;
}
9 changes: 9 additions & 0 deletions pinot-controller/src/main/resources/app/pages/Query.tsx
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -270,6 +278,7 @@ const QueryPage = () => {

Array.prototype.push.apply(defaultHint.list, finalList);

defaultHint.list = _.uniqBy(defaultHint.list, 'text');
return defaultHint;
};

Expand Down

0 comments on commit 5da3433

Please sign in to comment.