Browse Source

[editor] Convert the mako syntax drop down to Vue and add it to the editor web component

Johan Åhlén 4 years ago
parent
commit
03609c028c

+ 3 - 0
desktop/core/src/desktop/js/apps/editor/components/aceEditor/AceEditor.vue

@@ -28,6 +28,7 @@
       :editor-id="id"
       :editor-id="id"
       :executor="executor"
       :executor="executor"
     />
     />
+    <ace-syntax-dropdown v-if="editor" :editor="editor" :editor-id="id" />
   </div>
   </div>
 </template>
 </template>
 
 
@@ -38,6 +39,7 @@
   import { Ace } from 'ext/ace';
   import { Ace } from 'ext/ace';
 
 
   import { attachPredictTypeahead } from './acePredict';
   import { attachPredictTypeahead } from './acePredict';
+  import AceSyntaxDropdown from './AceSyntaxDropdown.vue';
   import AceAutocomplete from './autocomplete/AceAutocomplete.vue';
   import AceAutocomplete from './autocomplete/AceAutocomplete.vue';
   import AceGutterHandler from './AceGutterHandler';
   import AceGutterHandler from './AceGutterHandler';
   import AceLocationHandler, { ACTIVE_STATEMENT_CHANGED_EVENT } from './AceLocationHandler';
   import AceLocationHandler, { ACTIVE_STATEMENT_CHANGED_EVENT } from './AceLocationHandler';
@@ -72,6 +74,7 @@
   export default defineComponent({
   export default defineComponent({
     name: 'AceEditor',
     name: 'AceEditor',
     components: {
     components: {
+      AceSyntaxDropdown,
       AceAutocomplete
       AceAutocomplete
     },
     },
     props: {
     props: {

+ 7 - 2
desktop/core/src/desktop/js/apps/editor/components/aceEditor/AceLocationHandler.ts

@@ -18,6 +18,11 @@ import { Ace } from 'ext/ace';
 import ace from 'ext/aceHelper';
 import ace from 'ext/aceHelper';
 import KnockoutObservable from '@types/knockout';
 import KnockoutObservable from '@types/knockout';
 
 
+import {
+  SQL_SYNTAX_DROPDOWN_HIDE_TOPIC,
+  SQL_SYNTAX_DROPDOWN_SHOW_TOPIC,
+  SqlSyntaxDropdownShowEvent
+} from './events';
 import { ActiveStatementChangedEventDetails } from './types';
 import { ActiveStatementChangedEventDetails } from './types';
 import Executor from 'apps/editor/execution/executor';
 import Executor from 'apps/editor/execution/executor';
 import DataCatalogEntry, { TableSourceMeta } from 'catalog/DataCatalogEntry';
 import DataCatalogEntry, { TableSourceMeta } from 'catalog/DataCatalogEntry';
@@ -417,7 +422,7 @@ export default class AceLocationHandler implements Disposable {
     const onContextMenu = (e: { clientX: number; clientY: number; preventDefault: () => void }) => {
     const onContextMenu = (e: { clientX: number; clientY: number; preventDefault: () => void }) => {
       const selectionRange = this.editor.selection.getRange();
       const selectionRange = this.editor.selection.getRange();
       huePubSub.publish('context.popover.hide');
       huePubSub.publish('context.popover.hide');
-      huePubSub.publish('sql.syntax.dropdown.hide');
+      huePubSub.publish(SQL_SYNTAX_DROPDOWN_HIDE_TOPIC);
       if (selectionRange.isEmpty()) {
       if (selectionRange.isEmpty()) {
         const pointerPosition = this.editor.renderer.screenToTextCoordinates(
         const pointerPosition = this.editor.renderer.screenToTextCoordinates(
           e.clientX + 5,
           e.clientX + 5,
@@ -514,7 +519,7 @@ export default class AceLocationHandler implements Disposable {
               });
               });
             }
             }
           } else if (token.syntaxError) {
           } else if (token.syntaxError) {
-            huePubSub.publish('sql.syntax.dropdown.show', {
+            huePubSub.publish<SqlSyntaxDropdownShowEvent>(SQL_SYNTAX_DROPDOWN_SHOW_TOPIC, {
               editorId: this.editorId,
               editorId: this.editorId,
               data: token.syntaxError,
               data: token.syntaxError,
               editor: this.editor,
               editor: this.editor,

+ 31 - 0
desktop/core/src/desktop/js/apps/editor/components/aceEditor/AceSyntaxDropdown.scss

@@ -0,0 +1,31 @@
+/*
+ Licensed to Cloudera, Inc. under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  Cloudera, Inc. 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.
+*/
+
+.ace-syntax-dropdown-container {
+  position: fixed;
+  z-index: 10000;
+
+  .hue-dropdown-drawer-inner {
+    max-height: 200px;
+    overflow-y: auto;
+
+    li button {
+      font-size: 13px;
+    }
+  }
+}

+ 121 - 0
desktop/core/src/desktop/js/apps/editor/components/aceEditor/AceSyntaxDropdown.vue

@@ -0,0 +1,121 @@
+<!--
+  Licensed to Cloudera, Inc. under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  Cloudera, Inc. 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.
+-->
+
+<template>
+  <div class="ace-syntax-dropdown-container" :style="position">
+    <DropdownMenuOptions
+      :force-bottom-placement="true"
+      :open="visible"
+      :options="options"
+      @close="closePanel"
+      @option-selected="optionSelected"
+    />
+  </div>
+</template>
+
+<script lang="ts">
+  import { Ace } from 'ext/ace';
+  import { defineComponent, PropType, ref, toRefs } from 'vue';
+
+  import './AceSyntaxDropdown.scss';
+  import { SQL_SYNTAX_DROPDOWN_SHOW_TOPIC, SqlSyntaxDropdownShowEvent } from './events';
+  import DropdownMenuOptions from 'components/dropdown/DropdownMenuOptions.vue';
+  import { Option } from 'components/dropdown/types';
+  import SubscriptionTracker from 'components/utils/SubscriptionTracker';
+  import huePubSub from 'utils/huePubSub';
+  import I18n from 'utils/i18n';
+  import { getFromLocalStorage, setInLocalStorage } from 'utils/storageUtils';
+
+  interface SyntaxOption extends Option {
+    suppress?: string;
+  }
+
+  export default defineComponent({
+    name: 'AceSyntaxDropdown',
+    components: { DropdownMenuOptions },
+    props: {
+      editor: {
+        type: Object as PropType<Ace.Editor>,
+        required: true
+      },
+      editorId: {
+        type: String,
+        required: true
+      }
+    },
+    emits: [],
+    setup(props) {
+      const { editor, editorId } = toRefs(props);
+      const subTracker = new SubscriptionTracker();
+      const visible = ref(false);
+      const range = ref<Ace.Range>();
+      const options = ref<SyntaxOption[]>([]);
+      const position = ref<Pick<CSSStyleDeclaration, 'left' | 'top'>>();
+
+      const closePanel = (): void => {
+        options.value = [];
+        range.value = undefined;
+        visible.value = false;
+      };
+
+      const optionSelected = ({ suppress, value }: SyntaxOption) => {
+        if (suppress) {
+          const currentSuppressedRules = getFromLocalStorage(
+            'hue.syntax.checker.suppressedRules',
+            {} as Record<string, boolean>
+          );
+          currentSuppressedRules[suppress] = true;
+          setInLocalStorage('hue.syntax.checker.suppressedRules', currentSuppressedRules);
+          huePubSub.publish('editor.refresh.statement.locations', editorId.value);
+        } else if (range.value) {
+          editor.value.session.replace(range.value, value);
+        }
+      };
+
+      subTracker.subscribe('sql.syntax.dropdown.hide', closePanel);
+
+      subTracker.subscribe<SqlSyntaxDropdownShowEvent>(SQL_SYNTAX_DROPDOWN_SHOW_TOPIC, details => {
+        if (details.editorId !== editorId.value) {
+          return;
+        }
+        const newOptions: SyntaxOption[] = details.data.expected.map(expected => ({
+          label: expected.text,
+          value: expected.text
+        }));
+        if (details.data.ruleId) {
+          newOptions.push({
+            label: '-',
+            value: '_divider_',
+            divider: true
+          });
+          newOptions.push({
+            label: I18n('Ignore this type of error'),
+            value: '_suppress_',
+            suppress: details.data.ruleId + details.data.text.toLowerCase()
+          });
+        }
+        position.value = { top: `${details.source.bottom}px`, left: `${details.source.left}px` };
+        range.value = details.range;
+        options.value = newOptions;
+        visible.value = true;
+      });
+
+      return { closePanel, options, optionSelected, position, visible };
+    }
+  });
+</script>

+ 4 - 0
desktop/core/src/desktop/js/apps/editor/components/aceEditor/__snapshots__/AceEditor.test.ts.snap

@@ -99,5 +99,9 @@ exports[`AceEditor.vue should render 1`] = `
     sqlreferenceprovider="[object Object]"
     sqlreferenceprovider="[object Object]"
     temporaryonly="false"
     temporaryonly="false"
   />
   />
+  <ace-syntax-dropdown-stub
+    editor="[object Object]"
+    editorid="some-id"
+  />
 </div>
 </div>
 `;
 `;

+ 4 - 4
desktop/core/src/desktop/js/apps/editor/components/aceEditor/autocomplete/AutocompleteResults.ts

@@ -238,8 +238,8 @@ class AutocompleteResults {
     const foundArgumentDetails = (await getArgumentDetailsForUdf(
     const foundArgumentDetails = (await getArgumentDetailsForUdf(
       this.sqlReferenceProvider,
       this.sqlReferenceProvider,
       this.executor.connector(),
       this.executor.connector(),
-      this.parseResult.udfArgument.name,
-      this.parseResult.udfArgument.position
+      this.parseResult.udfArgument!.name,
+      this.parseResult.udfArgument!.position
     )) || [{ type: 'T' }];
     )) || [{ type: 'T' }];
 
 
     if (foundArgumentDetails.length === 0 && this.parseResult.suggestColumns) {
     if (foundArgumentDetails.length === 0 && this.parseResult.suggestColumns) {
@@ -701,7 +701,7 @@ class AutocompleteResults {
       try {
       try {
         const databases = await databasesPromise;
         const databases = await databasesPromise;
         const foundDb = databases.find(dbEntry =>
         const foundDb = databases.find(dbEntry =>
-          equalIgnoreCase(dbEntry.name, suggestTables.identifierChain[0].name)
+          equalIgnoreCase(dbEntry.name, suggestTables.identifierChain![0].name)
         );
         );
         if (foundDb) {
         if (foundDb) {
           tableSuggestions = await getTableSuggestions();
           tableSuggestions = await getTableSuggestions();
@@ -1196,7 +1196,7 @@ class AutocompleteResults {
     // Last one is either partial name or empty
     // Last one is either partial name or empty
     parts.pop();
     parts.pop();
 
 
-    await new Promise(resolve => {
+    await new Promise<void>(resolve => {
       const apiHelperFn = (<{ [fn: string]: (arg: unknown) => JQueryXHR }>(<unknown>apiHelper))[
       const apiHelperFn = (<{ [fn: string]: (arg: unknown) => JQueryXHR }>(<unknown>apiHelper))[
         fetchFunction
         fetchFunction
       ];
       ];

+ 31 - 0
desktop/core/src/desktop/js/apps/editor/components/aceEditor/events.ts

@@ -0,0 +1,31 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. 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 { Ace } from 'ext/ace';
+import { SyntaxError } from 'parse/types';
+
+export const SQL_SYNTAX_DROPDOWN_SHOW_TOPIC = 'sql.syntax.dropdown.show';
+export interface SqlSyntaxDropdownShowEvent {
+  editorId: string;
+  editor: Ace.Editor;
+  data: SyntaxError;
+  range: Ace.Range;
+  sourceType: string;
+  defaultDatabase: string;
+  source: { top: number; right: number; bottom: number; left: number };
+}
+
+export const SQL_SYNTAX_DROPDOWN_HIDE_TOPIC = 'sql.syntax.dropdown.hide';

+ 29 - 0
desktop/core/src/desktop/js/components/dropdown/DropdownDivider.vue

@@ -0,0 +1,29 @@
+<!--
+  Licensed to Cloudera, Inc. under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  Cloudera, Inc. 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.
+-->
+
+<template>
+  <li class="dropdown-divider">&nbsp;</li>
+</template>
+
+<script lang="ts">
+  import { defineComponent } from 'vue';
+
+  export default defineComponent({
+    name: 'DropdownDivider'
+  });
+</script>

+ 5 - 0
desktop/core/src/desktop/js/components/dropdown/DropdownDrawer.scss

@@ -48,6 +48,11 @@
       list-style: none;
       list-style: none;
       font-size: 13px;
       font-size: 13px;
 
 
+      li.dropdown-divider {
+        height: 1px;
+        background-color: $hue-border-color;
+      }
+
       li {
       li {
         color: $fluid-gray-800;
         color: $fluid-gray-800;
 
 

+ 5 - 1
desktop/core/src/desktop/js/components/dropdown/DropdownDrawer.vue

@@ -72,6 +72,10 @@
       triggerElement: {
       triggerElement: {
         type: Object as PropType<HTMLElement | null>,
         type: Object as PropType<HTMLElement | null>,
         default: null
         default: null
+      },
+      forceBottomPlacement: {
+        type: Boolean,
+        default: false
       }
       }
     },
     },
     emits: ['close'],
     emits: ['close'],
@@ -95,7 +99,7 @@
         await nextTick();
         await nextTick();
 
 
         const { bottom, right } = isOutsideViewport(<HTMLElement>this.$refs.innerPanelElement);
         const { bottom, right } = isOutsideViewport(<HTMLElement>this.$refs.innerPanelElement);
-        if (bottom) {
+        if (bottom && !this.forceBottomPlacement) {
           const bottomOffset = this.triggerElement?.offsetHeight || 0;
           const bottomOffset = this.triggerElement?.offsetHeight || 0;
           // position: relative Prevents fixed element from appearing below the window when at the bottom of the page
           // position: relative Prevents fixed element from appearing below the window when at the bottom of the page
           this.parentPosition = { position: 'relative' };
           this.parentPosition = { position: 'relative' };

+ 1 - 1
desktop/core/src/desktop/js/components/dropdown/DropdownMenuButton.vue

@@ -18,7 +18,7 @@
 
 
 <template>
 <template>
   <li>
   <li>
-    <button @click="$emit('click')"><slot /></button>
+    <button @click.stop="$emit('click')"><slot /></button>
   </li>
   </li>
 </template>
 </template>
 
 

+ 20 - 11
desktop/core/src/desktop/js/components/dropdown/DropdownMenuOptions.vue

@@ -1,34 +1,38 @@
 <template>
 <template>
   <DropdownDrawer
   <DropdownDrawer
-    :open="open && options.length"
+    :open="open && options.length > 0"
+    :force-bottom-placement="forceBottomPlacement"
     :trigger-element="keydownElement"
     :trigger-element="keydownElement"
     @close="$emit('close')"
     @close="$emit('close')"
   >
   >
     <ul>
     <ul>
-      <DropdownMenuButton
-        v-for="(option, idx) of options"
-        :key="option.value || option"
-        :class="{ selected: idx === selectedIndex }"
-        @click="onOptionClick(option, idx)"
-      >
-        {{ getLabel(option) }}
-      </DropdownMenuButton>
+      <template v-for="(option, idx) of options" :key="option.value || option.label || option">
+        <DropdownMenuButton
+          v-if="!option.divider"
+          :class="{ selected: idx === selectedIndex }"
+          @click="onOptionClick(option, idx)"
+        >
+          {{ getLabel(option) }}
+        </DropdownMenuButton>
+        <DropdownDivider v-else />
+      </template>
     </ul>
     </ul>
   </DropdownDrawer>
   </DropdownDrawer>
 </template>
 </template>
 
 
 <script lang="ts">
 <script lang="ts">
   import { defineComponent, PropType } from 'vue';
   import { defineComponent, PropType } from 'vue';
+  import DropdownDivider from './DropdownDivider.vue';
 
 
   import DropdownDrawer from './DropdownDrawer.vue';
   import DropdownDrawer from './DropdownDrawer.vue';
   import DropdownMenuButton from './DropdownMenuButton.vue';
   import DropdownMenuButton from './DropdownMenuButton.vue';
-  import { Option } from './DropDownMenuOptions';
+  import { Option } from './types';
 
 
   export const getLabel = (option: Option): string =>
   export const getLabel = (option: Option): string =>
     (option as { label: string }).label || (option as string);
     (option as { label: string }).label || (option as string);
 
 
   export default defineComponent({
   export default defineComponent({
-    components: { DropdownMenuButton, DropdownDrawer },
+    components: { DropdownDivider, DropdownMenuButton, DropdownDrawer },
     props: {
     props: {
       options: {
       options: {
         type: Array as PropType<Option[]>,
         type: Array as PropType<Option[]>,
@@ -41,6 +45,10 @@
       open: {
       open: {
         type: Boolean,
         type: Boolean,
         default: false
         default: false
+      },
+      forceBottomPlacement: {
+        type: Boolean,
+        default: false
       }
       }
     },
     },
     emits: ['close', 'option-active', 'option-selected'],
     emits: ['close', 'option-active', 'option-selected'],
@@ -97,6 +105,7 @@
       onOptionClick(option: Option, index: number) {
       onOptionClick(option: Option, index: number) {
         this.selectedIndex = index;
         this.selectedIndex = index;
         this.$emit('option-selected', option);
         this.$emit('option-selected', option);
+        this.$emit('close');
       }
       }
     }
     }
   });
   });

+ 3 - 3
desktop/core/src/desktop/js/components/dropdown/DropDownMenuOptions.d.ts → desktop/core/src/desktop/js/components/dropdown/types.ts

@@ -3,15 +3,15 @@
 // distributed with this work for additional information
 // distributed with this work for additional information
 // regarding copyright ownership.  Cloudera, Inc. licenses this file
 // regarding copyright ownership.  Cloudera, Inc. licenses this file
 // to you under the Apache License, Version 2.0 (the
 // to you under the Apache License, Version 2.0 (the
-// 'License'); you may not use this file except in compliance
+// "License"); you may not use this file except in compliance
 // with the License.  You may obtain a copy of the License at
 // with the License.  You may obtain a copy of the License at
 //
 //
 //     http://www.apache.org/licenses/LICENSE-2.0
 //     http://www.apache.org/licenses/LICENSE-2.0
 //
 //
 // Unless required by applicable law or agreed to in writing, software
 // Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an 'AS IS' BASIS,
+// distributed under the License is distributed on an "AS IS" BASIS,
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // See the License for the specific language governing permissions and
 // limitations under the License.
 // limitations under the License.
 
 
-export type Option = string | { value: string; label: string };
+export type Option = string | { value: string; label: string; divider?: boolean };

+ 1 - 0
desktop/core/src/desktop/js/parse/types.ts

@@ -48,6 +48,7 @@ export interface SyntaxError {
   expectedStatementEnd?: boolean;
   expectedStatementEnd?: boolean;
   loc: ParsedLocation;
   loc: ParsedLocation;
   text: string;
   text: string;
+  ruleId?: string;
 }
 }
 
 
 export interface IdentifierLocation {
 export interface IdentifierLocation {

+ 0 - 3
desktop/libs/notebook/src/notebook/templates/editor2.mako

@@ -29,7 +29,6 @@
 
 
 <%namespace name="configKoComponents" file="/config_ko_components.mako" />
 <%namespace name="configKoComponents" file="/config_ko_components.mako" />
 <%namespace name="notebookKoComponents" file="/common_notebook_ko_components.mako" />
 <%namespace name="notebookKoComponents" file="/common_notebook_ko_components.mako" />
-<%namespace name="sqlSyntaxDropdown" file="/sql_syntax_dropdown.mako" />
 
 
 <link rel="stylesheet" href="${ static('notebook/css/editor2.css') }">
 <link rel="stylesheet" href="${ static('notebook/css/editor2.css') }">
 <link rel="stylesheet" href="${ static('desktop/ext/css/bootstrap-editable.css') }">
 <link rel="stylesheet" href="${ static('desktop/ext/css/bootstrap-editable.css') }">
@@ -1041,8 +1040,6 @@
   </div>
   </div>
 </div>
 </div>
 
 
-${ sqlSyntaxDropdown.sqlSyntaxDropdown() }
-
 <script type="text/javascript">
 <script type="text/javascript">
   window.EDITOR_BINDABLE_ELEMENT = '#editorComponents';
   window.EDITOR_BINDABLE_ELEMENT = '#editorComponents';