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/08/30 08:02:17 UTC

[incubator-pinot] 01/02: adding autocomplete in sql editor

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

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

commit d6b9bef3a1ae10c0c7ec3707652e950c6cdca147
Author: Sanket Shah <er...@gmail.com>
AuthorDate: Wed Aug 5 19:54:37 2020 +0530

    adding autocomplete in sql editor
---
 .../src/main/resources/app/pages/Query.tsx         | 161 +++++++++++++-------
 .../src/main/resources/app/styles/styles.css       |  61 ++++++++
 .../main/resources/app/utils/PinotMethodUtils.ts   |  11 +-
 .../src/main/resources/app/utils/Utils.tsx         | 163 ++++++++++++++++++++-
 pinot-controller/src/main/resources/package.json   |   1 +
 5 files changed, 342 insertions(+), 55 deletions(-)

diff --git a/pinot-controller/src/main/resources/app/pages/Query.tsx b/pinot-controller/src/main/resources/app/pages/Query.tsx
index fdb9b88..e3b67f5 100644
--- a/pinot-controller/src/main/resources/app/pages/Query.tsx
+++ b/pinot-controller/src/main/resources/app/pages/Query.tsx
@@ -29,6 +29,10 @@ import 'codemirror/lib/codemirror.css';
 import 'codemirror/theme/material.css';
 import 'codemirror/mode/javascript/javascript';
 import 'codemirror/mode/sql/sql';
+import 'codemirror/addon/hint/show-hint';
+import 'codemirror/addon/hint/sql-hint';
+import 'codemirror/addon/hint/show-hint.css';
+import NativeCodeMirror from 'codemirror';
 import _ from 'lodash';
 import FormControlLabel from '@material-ui/core/FormControlLabel';
 import Switch from '@material-ui/core/Switch';
@@ -40,6 +44,7 @@ import QuerySideBar from '../components/Query/QuerySideBar';
 import TableToolbar from '../components/TableToolbar';
 import SimpleAccordion from '../components/SimpleAccordion';
 import PinotMethodUtils from '../utils/PinotMethodUtils';
+import '../styles/styles.css';
 
 const useStyles = makeStyles((theme) => ({
   title: {
@@ -48,7 +53,11 @@ const useStyles = makeStyles((theme) => ({
   },
   rightPanel: {},
   codeMirror: {
-    '& .CodeMirror': { height: 100, border: '1px solid #BDCCD9', fontSize: '13px' },
+    '& .CodeMirror': {
+      height: 100,
+      border: '1px solid #BDCCD9',
+      fontSize: '13px',
+    },
   },
   queryOutput: {
     '& .CodeMirror': { height: 430, border: '1px solid #BDCCD9' },
@@ -74,8 +83,8 @@ const useStyles = makeStyles((theme) => ({
     marginBottom: '20px',
   },
   sqlError: {
-    whiteSpace: 'pre-wrap'
-  }
+    whiteSpace: 'pre-wrap',
+  },
 }));
 
 const jsonoptions = {
@@ -85,16 +94,19 @@ const jsonoptions = {
   gutters: ['CodeMirror-lint-markers'],
   lint: true,
   theme: 'default',
-  readOnly: true
+  readOnly: true,
 };
 
 const sqloptions = {
   lineNumbers: true,
-  mode: 'sql',
+  mode: 'text/x-sql',
   styleActiveLine: true,
-  gutters: ['CodeMirror-lint-markers'],
   lint: true,
-  theme: 'default'
+  theme: 'default',
+  indentWithTabs: true,
+  smartIndent: true,
+  lineWrapping: true,
+  extraKeys: { "'@'": 'autocomplete' },
 };
 
 const QueryPage = () => {
@@ -125,13 +137,13 @@ const QueryPage = () => {
 
   const [queryStats, setQueryStats] = useState<TableData>({
     columns: [],
-    records: []
+    records: [],
   });
 
   const [checked, setChecked] = React.useState({
     tracing: false,
     querySyntaxPQL: false,
-    showResultJSON: false
+    showResultJSON: false,
   });
 
   const [copyMsg, showCopyMsg] = React.useState(false);
@@ -162,10 +174,14 @@ const QueryPage = () => {
       });
     }
 
-    const results = await PinotMethodUtils.getQueryResults(params, url, checked);
+    const results = await PinotMethodUtils.getQueryResults(
+      params,
+      url,
+      checked
+    );
     setResultError(results.error || '');
-    setResultData(results.result || {columns: [], records: []});
-    setQueryStats(results.queryStats || {columns: [], records: []});
+    setResultData(results.result || { columns: [], records: [] });
+    setQueryStats(results.queryStats || { columns: [], records: [] });
     setOutputResult(JSON.stringify(results.data, null, 2) || '');
     setQueryLoader(false);
   };
@@ -223,6 +239,40 @@ const QueryPage = () => {
     fetchData();
   }, []);
 
+  const handleSqlHints = (cm: NativeCodeMirror.Editor) => {
+    const tableNames = [];
+    tableList.records.forEach((obj, i) => {
+      tableNames.push(obj[i]);
+    });
+    const columnNames = tableSchema.records.map((obj) => {
+      return obj[0];
+    });
+    const hintOptions = [];
+    const defaultHint = (NativeCodeMirror as any).hint.sql(cm);
+
+    Array.prototype.push.apply(hintOptions, Utils.generateCodeMirrorOptions(tableNames, 'TABLE'));
+    Array.prototype.push.apply(hintOptions, Utils.generateCodeMirrorOptions(columnNames, 'COLUMNS'));
+
+    const cur = cm.getCursor();
+    const curLine = cm.getLine(cur.line);
+    let start = cur.ch;
+    let end = start;
+    // eslint-disable-next-line no-plusplus
+    while (end < curLine.length && /[\w$]/.test(curLine.charAt(end))) ++end;
+    // eslint-disable-next-line no-plusplus
+    while (start && /[\w$]/.test(curLine.charAt(start - 1))) --start;
+    const curWord = start !== end && curLine.slice(start, end);
+    const regex = new RegExp(`^${  curWord}`, 'i');
+
+    const finalList =  (!curWord ? hintOptions : hintOptions.filter(function (item) {
+      return item.displayText.match(regex);
+    })).sort();
+
+    Array.prototype.push.apply(defaultHint.list, finalList);
+
+    return defaultHint;
+  };
+
   return fetching ? (
     <AppLoader />
   ) : (
@@ -235,13 +285,27 @@ const QueryPage = () => {
           selectedTable={selectedTable}
         />
       </Grid>
-      <Grid item xs style={{ padding: 20, backgroundColor: 'white', maxHeight: 'calc(100vh - 70px)', overflowY: 'auto' }}>
+      <Grid
+        item
+        xs
+        style={{
+          padding: 20,
+          backgroundColor: 'white',
+          maxHeight: 'calc(100vh - 70px)',
+          overflowY: 'auto',
+        }}
+      >
         <Grid container>
           <Grid item xs={12} className={classes.rightPanel}>
             <div className={classes.sqlDiv}>
               <TableToolbar name="SQL Editor" showSearchBox={false} />
               <CodeMirror
-                options={sqloptions}
+                options={{
+                  ...sqloptions,
+                  hintOptions: {
+                    hint: handleSqlHints,
+                  },
+                }}
                 value={inputQuery}
                 onChange={handleOutputDataChange}
                 className={classes.codeMirror}
@@ -281,16 +345,17 @@ const QueryPage = () => {
               </Grid>
             </Grid>
 
-            {queryLoader ?
+            {queryLoader ? (
               <AppLoader />
-            :
+            ) : (
               <>
-                {
-                  resultError ?
-                  <Alert severity="error" className={classes.sqlError}>{resultError}</Alert>
-                :
+                {resultError ? (
+                  <Alert severity="error" className={classes.sqlError}>
+                    {resultError}
+                  </Alert>
+                ) : (
                   <>
-                    {queryStats.records.length ?
+                    {queryStats.records.length ? (
                       <Grid item xs style={{ backgroundColor: 'white' }}>
                         <CustomizedTables
                           title="Query Response Stats"
@@ -299,8 +364,7 @@ const QueryPage = () => {
                           inAccordionFormat={true}
                         />
                       </Grid>
-                      : null 
-                    }
+                    ) : null}
 
                     <Grid item xs style={{ backgroundColor: 'white' }}>
                       {resultData.records.length ? (
@@ -338,7 +402,8 @@ const QueryPage = () => {
                                 icon={<FileCopyIcon fontSize="inherit" />}
                                 severity="info"
                               >
-                                Copied {resultData.records.length} rows to Clipboard
+                                Copied {resultData.records.length} rows to
+                                Clipboard
                               </Alert>
                             ) : null}
 
@@ -355,37 +420,35 @@ const QueryPage = () => {
                               className={classes.runNowBtn}
                             />
                           </Grid>
-                          {!checked.showResultJSON
-                            ?
-                              <CustomizedTables
-                                title="Query Result"
-                                data={resultData}
-                                isPagination
-                                isSticky={true}
-                                showSearchBox={true}
-                                inAccordionFormat={true}
+                          {!checked.showResultJSON ? (
+                            <CustomizedTables
+                              title="Query Result"
+                              data={resultData}
+                              isPagination
+                              isSticky={true}
+                              showSearchBox={true}
+                              inAccordionFormat={true}
+                            />
+                          ) : resultData.records.length ? (
+                            <SimpleAccordion
+                              headerTitle="Query Result (JSON Format)"
+                              showSearchBox={false}
+                            >
+                              <CodeMirror
+                                options={jsonoptions}
+                                value={outputResult}
+                                className={classes.queryOutput}
+                                autoCursor={false}
                               />
-                            :
-                            resultData.records.length ? (
-                              <SimpleAccordion
-                                headerTitle="Query Result (JSON Format)"
-                                showSearchBox={false}
-                              >
-                                <CodeMirror
-                                  options={jsonoptions}
-                                  value={outputResult}
-                                  className={classes.queryOutput}
-                                  autoCursor={false}
-                                />
-                              </SimpleAccordion>
-                            ) : null}
+                            </SimpleAccordion>
+                          ) : null}
                         </>
                       ) : null}
                     </Grid>
                   </>
-                }
+                )}
               </>
-            }
+            )}
           </Grid>
         </Grid>
       </Grid>
diff --git a/pinot-controller/src/main/resources/app/styles/styles.css b/pinot-controller/src/main/resources/app/styles/styles.css
index 8e764f2..d3c2d97 100644
--- a/pinot-controller/src/main/resources/app/styles/styles.css
+++ b/pinot-controller/src/main/resources/app/styles/styles.css
@@ -19,4 +19,65 @@
 
 body {
     font-family: 'Source Sans Pro', sans-serif;
+}
+
+li.CodeMirror-hint {
+  margin: 0;
+  padding: 4px 0 4px 25px;
+  border-radius: 2px;
+  white-space: pre;
+  color: black;
+  cursor: pointer;
+  position: relative;
+  line-height: 1.5;
+}
+
+li.CodeMirror-hint:before {
+  position: absolute;
+  left: 5px;
+  top: 5px;
+  border-radius: 50%;
+  font-size: 12px;
+  height: 15px;
+  width: 15px;
+  line-height: 15px;
+  text-align: center;
+  color: white;
+  -moz-box-sizing: border-box;
+  box-sizing: border-box;
+}
+
+li.codemirror-table.CodeMirror-hint,
+li.codemirror-column.CodeMirror-hint{
+  color: black;
+}
+
+li.codemirror-table.CodeMirror-hint > div >  span.funcText > span,
+li.codemirror-column.CodeMirror-hint > div >  span.funcText > span{
+  font-weight: bold;
+  font-size: 10px;
+}
+
+li.codemirror-table.CodeMirror-hint::before {
+  content: "T";
+  background: #2F7E89;
+}
+
+li.codemirror-column.CodeMirror-hint::before {
+  content: "C";
+  background: #05a;
+}
+
+.CodeMirror-hints {
+  padding: 4px;
+  box-shadow: 0px 0px 5px rgba(0,0,0,.2);
+  border-radius: 0px;
+  font-family: 'Source Sans Pro', sans-serif;
+  font-size: 12px;
+  border: 1px solid #eaeaea;
+  min-width: 140px;
+}
+
+li.CodeMirror-hint-active {
+  background-color: #dae2f2;
 }
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts b/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
index ff80e01..30947cf 100644
--- a/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
+++ b/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
@@ -92,7 +92,7 @@ const getInstanceData = (instances, liveInstanceArr) => {
 
   return Promise.all(promiseArr).then((result) => {
     return {
-      columns: ['Insance Name', 'Enabled', 'Hostname', 'Port', 'Status'],
+      columns: ['Instance Name', 'Enabled', 'Hostname', 'Port', 'Status'],
       records: [
         ...result.map(({ data }) => [
           data.instanceName,
@@ -533,6 +533,15 @@ const getNodeData = (path) => {
     const currentNodeData = results[0].data || {};
     const currentNodeListStat = results[1].data;
     const currentNodeMetadata = results[2].data;
+
+    if(currentNodeMetadata['ctime'] || currentNodeMetadata['mtime']){
+      currentNodeMetadata['ctime'] = moment(+currentNodeMetadata['ctime']).format(
+        'MMMM Do YYYY, h:mm:ss'
+      );
+      currentNodeMetadata['mtime'] = moment(+currentNodeMetadata['mtime']).format(
+        'MMMM Do YYYY, h:mm:ss'
+      );
+    }
     return { currentNodeData, currentNodeMetadata, currentNodeListStat };
   });
 };
diff --git a/pinot-controller/src/main/resources/app/utils/Utils.tsx b/pinot-controller/src/main/resources/app/utils/Utils.tsx
index 8fa36d8..7f80dfb 100644
--- a/pinot-controller/src/main/resources/app/utils/Utils.tsx
+++ b/pinot-controller/src/main/resources/app/utils/Utils.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable no-nested-ternary */
 /**
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
@@ -49,7 +50,7 @@ const tableFormat = (data) => {
   const results = [];
   rows.forEach((singleRow) => {
     const obj = {};
-    singleRow.forEach((val: any, index: number)=>{
+    singleRow.forEach((val: any, index: number) => {
       obj[header[index]] = val;
     });
     results.push(obj);
@@ -64,14 +65,14 @@ const getSegmentStatus = (idealStateObj, externalViewObj) => {
   const externalSegmentKeys = Object.keys(externalViewObj);
   const externalSegmentCount = externalSegmentKeys.length;
 
-  if(idealSegmentCount !== externalSegmentCount){
+  if (idealSegmentCount !== externalSegmentCount) {
     return 'Bad';
   }
 
   let segmentStatus = 'Good';
   idealSegmentKeys.map((segmentKey) => {
-    if(segmentStatus === 'Good'){
-      if( !_.isEqual( idealStateObj[segmentKey], externalViewObj[segmentKey] ) ){
+    if (segmentStatus === 'Good') {
+      if (!_.isEqual(idealStateObj[segmentKey], externalViewObj[segmentKey])) {
         segmentStatus = 'Bad';
       }
     }
@@ -90,9 +91,161 @@ const findNestedObj = (entireObj, keyToFind, valToFind) => {
   return foundObj;
 };
 
+const generateCodeMirrorOptions = (array, type, modeType?) => {
+  const arr = [];
+  // eslint-disable-next-line no-shadow
+  const nestedFields = (arrayList, type, level, oldObj?) => {
+    _.map(arrayList, (a) => {
+      const obj = {
+        text: a.displayName || a.name || a,
+        displayText: a.displayName || a.name || a,
+        filterText: oldObj
+          ? `${oldObj.filterText}.${a.displayName || a.name || a}`
+          : a.displayName || a.name || a,
+        argsType: '',
+        description: '',
+        render: (el, cm, data) => {},
+        className:
+          type === 'FUNCTION'
+            ? 'codemirror-func'
+            : type === 'SQL'
+              ? 'codemirror-sql'
+              : type === 'BINARY-OPERATORS'
+                ? 'codemirror-Operators'
+                : type === 'TABLE'
+                  ? 'codemirror-table'
+                  : 'codemirror-column'
+      };
+      obj[type === 'FUNCTION' ? 'returnType' : 'type'] =
+        type === 'FUNCTION'
+          ? a.returnType
+          : type === 'SQL'
+            ? 'SQL'
+            : 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);
+      };
+
+      if (oldObj === undefined) {
+        arr.push(obj);
+      } else {
+        const index = _.findIndex(
+          arr,
+          (n) => n.filterText === oldObj.filterText
+        );
+        if (index !== -1) {
+          const name = obj.displayText;
+          if (modeType === 'sql') {
+            obj.displayText = `${oldObj.displayText}.${name}`;
+            obj.text = `${oldObj.text}.${name}`;
+          } else {
+            obj.displayText = name;
+            obj.text = name;
+          }
+          obj.filterText = `${oldObj.text}.${name}`;
+          if (arr[index].fields) {
+            arr[index].fields.push(obj);
+          } else {
+            arr[index].fields = [];
+            arr[index].fields.push(obj);
+          }
+        } else {
+          const indexPath = getNestedObjPathFromList(arr, oldObj);
+          if (indexPath && indexPath.length) {
+            pushNestedObjectInArray(indexPath, obj, arr, modeType);
+          }
+        }
+      }
+      if (a.fields) {
+        nestedFields(a.fields, type, level + 1, obj);
+      }
+    });
+    return arr;
+  };
+  return nestedFields(array, type, 0);
+};
+
+const pushNestedObjectInArray = (pathArr, obj, targetList, modeType) => {
+  const rollOverFields = (target) => {
+    _.map(target, (list) => {
+      if (pathArr === list.filterText) {
+        if (modeType === 'sql') {
+          obj.displayText = `${pathArr}.${obj.displayText}`;
+          obj.text = `${pathArr}.${obj.text}`;
+        } else {
+          obj.displayText = obj.displayText;
+          obj.text = obj.text;
+        }
+        obj.filterText = `${pathArr}.${obj.text}`;
+        if (list.fields) {
+          list.fields.push(obj);
+        } else {
+          list.fields = [];
+          list.fields.push(obj);
+        }
+      } else if (list.fields) {
+        rollOverFields(list.fields);
+      }
+    });
+  };
+  rollOverFields(targetList);
+};
+
+const getNestedObjPathFromList = (list, obj) => {
+  const str = [];
+  const recursiveFunc = (arr, level) => {
+    _.map(arr, (a) => {
+      if (a.fields) {
+        str.push(a.filterText);
+        recursiveFunc(a.fields, level + 1);
+      } else if (obj.filterText === a.filterText) {
+        str.push(a.filterText);
+      }
+    });
+    return _.findLast(str);
+  };
+  return recursiveFunc(list, 0);
+};
+
+const codeMirrorOptionsTemplate = (el, data) => {
+  const text = document.createElement('div');
+  const fNameSpan = document.createElement('span');
+  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);
+
+  // data.returnType is for UDF Function ||  data.type is for Fields
+  if (data.returnType || data.type) {
+    const returnTypetxt = document.createElement('div');
+    returnTypetxt.setAttribute('class', 'fieldText');
+    const content = data.returnType
+      ? `Return Type: ${data.returnType}`
+      : `Type: ${data.type}`;
+    returnTypetxt.innerHTML = content;
+    el.appendChild(returnTypetxt);
+  }
+};
+
 export default {
   sortArray,
   tableFormat,
   getSegmentStatus,
-  findNestedObj
+  findNestedObj,
+  generateCodeMirrorOptions
 };
diff --git a/pinot-controller/src/main/resources/package.json b/pinot-controller/src/main/resources/package.json
index 81598e1..a05cb3b 100644
--- a/pinot-controller/src/main/resources/package.json
+++ b/pinot-controller/src/main/resources/package.json
@@ -58,6 +58,7 @@
     "@material-ui/core": "4.11.0",
     "@material-ui/icons": "^4.9.1",
     "@material-ui/lab": "^4.0.0-alpha.51",
+    "@types/codemirror": "0.0.97",
     "@types/react-router-dom": "^5.1.5",
     "axios": "^0.19.2",
     "codemirror": "^5.55.0",


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