diff --git a/app/assets/components/CodeMirror.tsx b/app/assets/components/CodeMirror.tsx index 36864e7e..8f8d56c5 100644 --- a/app/assets/components/CodeMirror.tsx +++ b/app/assets/components/CodeMirror.tsx @@ -40,7 +40,7 @@ export default class ReactCodeMirror extends React.PureComponent { if (stream.eatSpace()) { return null; } - stream.eatWhile(/[\$\w\u4e00-\u9fa5]/); + stream.eatWhile(/[\$:\w\u4e00-\u9fa5]/); const cur = stream.current(); if (keyWords.some(item => item === cur)) { return 'keyword'; @@ -127,6 +127,7 @@ export default class ReactCodeMirror extends React.PureComponent { change.preventDefault(); } }; + codemirrorValueChange = (doc, change) => { if (change.origin !== 'setValue') { if (this.props.onChange) { diff --git a/app/assets/config/nebulaQL.ts b/app/assets/config/nebulaQL.ts index 547536a8..950fdbaf 100644 --- a/app/assets/config/nebulaQL.ts +++ b/app/assets/config/nebulaQL.ts @@ -133,6 +133,9 @@ const nebulaWordsUppercase = [ 'USERS', 'UUID', 'VALUES', + 'COMMENT', + ':PARAM', + ':PARAMS', ]; export const ban = ['use', 'USE']; @@ -217,6 +220,8 @@ export const operators = [ 'MINUS', // uuid 'uuid', + // assignment + '=>', ]; const nebulaWordsLowercase = nebulaWordsUppercase.map(w => w.toLowerCase()); diff --git a/app/assets/modules/Console/LocalCmdOutputBox.tsx b/app/assets/modules/Console/LocalCmdOutputBox.tsx new file mode 100644 index 00000000..e2c41db1 --- /dev/null +++ b/app/assets/modules/Console/LocalCmdOutputBox.tsx @@ -0,0 +1,32 @@ +import { Alert } from 'antd'; +import React from 'react'; + +import './index.less'; + +interface IProps { + cmd: string; + result?: string; + onHistoryItem: (value: string) => void; +} + +class LocalCmdOutputBox extends React.Component { + render() { + const { cmd, result } = this.props; + return ( +
+ this.props.onHistoryItem(cmd)}> + $ {cmd} +

+ } + className="cmd-value" + type={result?.includes('success') ? 'success' : 'error'} + /> +
{result}
+
+ ); + } +} + +export default LocalCmdOutputBox; diff --git a/app/assets/modules/Console/index.less b/app/assets/modules/Console/index.less index ef66e093..feea129a 100644 --- a/app/assets/modules/Console/index.less +++ b/app/assets/modules/Console/index.less @@ -124,3 +124,38 @@ } } +.cmd-box { + background: #fff; + position: relative; + display: flex; + flex-direction: column; + margin-bottom: 15px; + + .cmd-value { + padding: 0 12px; + width: 100%; + cursor: pointer; + font-size: 16px; + white-space: nowrap; + text-overflow: ellipsis; + line-height: 42px; + border: none; + + .gql { + overflow: hidden; + text-overflow: ellipsis; + } + } + + .ant-alert-success .gql { + color: #52c41a; + } + + .ant-alert-error .gql { + color: #ff4d4f; + } + + .cmd-result { + padding: 20px; + } +} diff --git a/app/assets/modules/Console/index.tsx b/app/assets/modules/Console/index.tsx index 2e040ead..0bd5d2c9 100644 --- a/app/assets/modules/Console/index.tsx +++ b/app/assets/modules/Console/index.tsx @@ -10,11 +10,14 @@ import { IDispatch, IRootState } from '#assets/store'; import { trackPageView } from '#assets/utils/stat'; import './index.less'; +import LocalCmdOutputBox from './LocalCmdOutputBox'; import SpaceSearchInput from './SpaceSearchInput'; interface IState { isUpDown: boolean; history: boolean; + lastCmd: string | null; + lastGql: string | null; } const mapState = (state: IRootState) => ({ @@ -41,6 +44,8 @@ interface IProps ReturnType, RouteComponentProps {} +// split from semicolon out of quotation marks +const SEMICOLON_REG = /((?:[^;'"]*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*')[^;'"]*)+)|;/; class Console extends React.Component { codemirror; editor; @@ -51,6 +56,8 @@ class Console extends React.Component { this.state = { isUpDown: true, history: false, + lastCmd: null, + lastGql: null, }; } @@ -66,36 +73,68 @@ class Console extends React.Component { return []; }; + handleSaveQuery = (query: string) => { + if (query !== '') { + const history = this.getLocalStorage(); + history.push(query); + localStorage.setItem('history', JSON.stringify(history)); + } + }; + + checkSwitchSpaceGql = (query: string) => { + const queryList = query.split(SEMICOLON_REG).filter(Boolean); + const reg = /^USE `?[0-9a-zA-Z_]+`?(?=[\s*;?]?)/gim; + if (queryList.some(sentence => sentence.trim().match(reg))) { + return intl.get('common.disablesUseToSwitchSpace'); + } + }; handleRun = async () => { - const gql = this.editor.getValue(); - if (!gql) { + const query = this.editor.getValue(); + if (!query) { message.error(intl.get('common.sorryNGQLCannotBeEmpty')); return; } - // Hack: - // replace the string in quotes with a constant, avoid special symbols in quotation marks from affecting regex match - // then split the gql entered by the user into sentences based on semicolons - // use regex to determine whether each sentence is a 'use space' statement - const _gql = gql - .replace(/[\r\n]/g, '') - .replace(/(["'])[^]*?\1/g, '_CONTENT_') - .toUpperCase(); - const sentenceList = _gql.split(';'); - const reg = /^USE `?[0-9a-zA-Z_]+`?(?=[\s*;?]?)/gm; - if (sentenceList.some(sentence => sentence.match(reg))) { - return message.error(intl.get('common.disablesUseToSwitchSpace')); + const errInfo = this.checkSwitchSpaceGql(query); + if (errInfo) { + return message.error(errInfo); } + this.editor.execCommand('goDocEnd'); - const history = this.getLocalStorage(); - history.push(gql); - localStorage.setItem('history', JSON.stringify(history)); + const { gqlList, paramList } = this.splitQuery(query); + const _gql = gqlList.join(';'); + const _cmd = paramList.join(';'); + this.handleSaveQuery(_gql); + this.handleSaveQuery(_cmd); - await this.props.asyncRunGQL(gql); + await this.props.asyncRunGQL({ + gql: _gql, + paramList, + }); this.setState({ isUpDown: true, + lastCmd: _cmd, + lastGql: _gql, }); }; + splitQuery = (query: string) => { + const queryList = query.split(SEMICOLON_REG).filter(Boolean); + const paramList: string[] = []; + const gqlList: string[] = []; + queryList.forEach(query => { + const _query = query.trim(); + if (_query.startsWith(':')) { + paramList.push(_query); + } else { + gqlList.push(_query); + } + }); + return { + paramList, + gqlList, + }; + }; + handleHistoryItem = (value: string) => { this.props.updateCurrentGQL(value); this.setState({ @@ -148,7 +187,7 @@ class Console extends React.Component { }; render() { - const { isUpDown, history } = this.state; + const { isUpDown, history, lastCmd, lastGql } = this.state; const { currentSpace, currentGQL, result, runGQLLoading } = this.props; return (
@@ -207,11 +246,22 @@ class Console extends React.Component {
- this.handleHistoryItem(e)} - /> + {lastCmd && ( + this.handleHistoryItem(e)} + /> + )} + {lastGql !== '' && ( + !item.startsWith(':')) + .pop()} + onHistoryItem={e => this.handleHistoryItem(e)} + /> + )}
{ return { @@ -20,9 +21,12 @@ export const _console = createModel({ }, }, effects: { - async asyncRunGQL(gql) { + async asyncRunGQL({ gql, paramList }) { const result = (await service.execNGQL( - { gql }, + { + gql, + paramList, + }, { trackEventConfig: { category: 'console',