diff --git a/desktop/core/src/desktop/js/apps/editor/components/__snapshots__/ko.syntaxDropdown.test.js.snap b/desktop/core/src/desktop/js/apps/editor/components/__snapshots__/ko.syntaxDropdown.test.js.snap
new file mode 100644
index 00000000000..dd7db50bfcc
--- /dev/null
+++ b/desktop/core/src/desktop/js/apps/editor/components/__snapshots__/ko.syntaxDropdown.test.js.snap
@@ -0,0 +1,51 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ko.syntaxDropdown.js should match snapshot 1`] = `
+"
"
+`;
+
+exports[`ko.syntaxDropdown.js should render component on show event 1`] = `""`;
diff --git a/desktop/core/src/desktop/js/apps/editor/components/ko.syntaxDropdown.js b/desktop/core/src/desktop/js/apps/editor/components/ko.syntaxDropdown.js
new file mode 100644
index 00000000000..46e5add83af
--- /dev/null
+++ b/desktop/core/src/desktop/js/apps/editor/components/ko.syntaxDropdown.js
@@ -0,0 +1,147 @@
+// 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 * as ko from 'knockout';
+import $ from 'jquery';
+
+import 'ko/components/ko.dropDown';
+import componentUtils from 'ko/components/componentUtils';
+import DisposableComponent from 'ko/components/DisposableComponent';
+import huePubSub from 'utils/huePubSub';
+import I18n from 'utils/i18n';
+import { hueLocalStorage } from 'utils/storageUtils';
+
+export const SYNTAX_DROPDOWN_COMPONENT = 'sql-syntax-dropdown';
+export const SYNTAX_DROPDOWN_ID = 'sqlSyntaxDropdown';
+export const SHOW_EVENT = 'sql.syntax.dropdown.show';
+export const SHOWN_EVENT = 'sql.syntax.dropdown.shown';
+export const HIDE_EVENT = 'sql.syntax.dropdown.hide';
+
+// prettier-ignore
+const TEMPLATE = `
+
+`;
+
+const hideSyntaxDropdown = () => {
+ const $sqlSyntaxDropdown = $(`#${SYNTAX_DROPDOWN_ID}`);
+ if ($sqlSyntaxDropdown.length) {
+ ko.cleanNode($sqlSyntaxDropdown[0]);
+ $sqlSyntaxDropdown.remove();
+ $(document).off('click', hideOnClickOutside);
+ }
+};
+
+const hideOnClickOutside = event => {
+ const $modal = $('.modal');
+ if (
+ $.contains(document, event.target) &&
+ !$.contains($(`#${SYNTAX_DROPDOWN_ID}`)[0], event.target) &&
+ ($modal[0].length === 0 || !$.contains($modal[0], event.target))
+ ) {
+ hideSyntaxDropdown();
+ }
+};
+
+class SqlSyntaxDropdownViewModel extends DisposableComponent {
+ constructor(params) {
+ super();
+
+ this.selected = ko.observable();
+
+ const expected = params.data.expected.map(expected => expected.text);
+ // TODO: Allow suppression of unknown columns etc.
+ if (params.data.ruleId) {
+ if (expected.length > 0) {
+ expected.push({
+ divider: true
+ });
+ }
+ expected.push({
+ label: I18n('Ignore this type of error'),
+ suppressRule: params.data.ruleId.toString() + params.data.text.toLowerCase()
+ });
+ }
+ this.expected = ko.observableArray(expected);
+
+ const selectedSub = this.selected.subscribe(newValue => {
+ if (typeof newValue.suppressRule !== 'undefined') {
+ const suppressedRules = hueLocalStorage('hue.syntax.checker.suppressedRules') || {};
+ suppressedRules[newValue.suppressRule] = true;
+ hueLocalStorage('hue.syntax.checker.suppressedRules', suppressedRules);
+ huePubSub.publish('editor.refresh.statement.locations', params.editorId);
+ } else {
+ params.editor.session.replace(params.range, newValue);
+ }
+ hideSyntaxDropdown();
+ });
+
+ this.addDisposalCallback(() => {
+ selectedSub.dispose();
+ });
+
+ this.left = ko.observable(params.source.left);
+ this.top = ko.observable(params.source.bottom);
+
+ const closeOnEsc = e => {
+ if (e.keyCode === 27) {
+ hideSyntaxDropdown();
+ }
+ };
+
+ $(document).on('keyup', closeOnEsc);
+
+ this.addDisposalCallback(() => {
+ $(document).off('keyup', closeOnEsc);
+ });
+
+ window.setTimeout(() => {
+ $(document).on('click', hideOnClickOutside);
+ }, 0);
+
+ this.addDisposalCallback(() => {
+ $(document).off('click', hideOnClickOutside);
+ });
+ }
+}
+
+componentUtils.registerComponent(SYNTAX_DROPDOWN_COMPONENT, SqlSyntaxDropdownViewModel, TEMPLATE);
+
+huePubSub.subscribe(HIDE_EVENT, hideSyntaxDropdown);
+huePubSub.subscribe(SHOW_EVENT, details => {
+ hideSyntaxDropdown();
+ const $sqlSyntaxDropdown = $(
+ ``
+ );
+ $('body').append($sqlSyntaxDropdown);
+ ko.applyBindings(details, $sqlSyntaxDropdown[0]);
+ huePubSub.publish(SHOWN_EVENT);
+});
diff --git a/desktop/core/src/desktop/js/apps/editor/components/ko.syntaxDropdown.test.js b/desktop/core/src/desktop/js/apps/editor/components/ko.syntaxDropdown.test.js
new file mode 100644
index 00000000000..efe6ba1e309
--- /dev/null
+++ b/desktop/core/src/desktop/js/apps/editor/components/ko.syntaxDropdown.test.js
@@ -0,0 +1,54 @@
+// 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 { SHOW_EVENT, SHOWN_EVENT, SYNTAX_DROPDOWN_ID } from './ko.syntaxDropdown';
+import huePubSub from 'utils/huePubSub';
+
+describe('ko.syntaxDropdown.js', () => {
+ const publishShowEvent = () => {
+ huePubSub.publish(SHOW_EVENT, {
+ data: {
+ text: 'floo',
+ expected: [{ text: 'foo' }],
+ ruleId: 123
+ },
+ source: {
+ left: 10,
+ bottom: 20
+ }
+ });
+ };
+
+ it('should render component on show event', async () => {
+ expect(document.querySelectorAll(`#${SYNTAX_DROPDOWN_ID}`)).toHaveLength(0);
+
+ publishShowEvent();
+
+ expect(document.querySelectorAll(`#${SYNTAX_DROPDOWN_ID}`)).toHaveLength(1);
+ expect(window.document.documentElement.outerHTML).toMatchSnapshot();
+ });
+
+ it('should match snapshot', async () => {
+ const shownPromise = new Promise(resolve => {
+ huePubSub.subscribeOnce(SHOWN_EVENT, resolve);
+ });
+ publishShowEvent();
+
+ await shownPromise;
+
+ expect(window.document.documentElement.outerHTML).toMatchSnapshot();
+ });
+});
diff --git a/desktop/core/src/desktop/js/apps/notebook/app.js b/desktop/core/src/desktop/js/apps/notebook/app.js
index 78d7377ca88..ce3544c279b 100644
--- a/desktop/core/src/desktop/js/apps/notebook/app.js
+++ b/desktop/core/src/desktop/js/apps/notebook/app.js
@@ -23,6 +23,7 @@ import 'ext/jquery.hotkeys';
import 'jquery/plugins/jquery.hdfstree';
import NotebookViewModel from './NotebookViewModel';
+import '../editor/components/ko.syntaxDropdown';
import {
ACTIVE_SNIPPET_CONNECTOR_CHANGED_EVENT,
IGNORE_NEXT_UNLOAD_EVENT
diff --git a/desktop/core/src/desktop/templates/sql_syntax_dropdown.mako b/desktop/core/src/desktop/templates/sql_syntax_dropdown.mako
deleted file mode 100644
index 2f37f7958d6..00000000000
--- a/desktop/core/src/desktop/templates/sql_syntax_dropdown.mako
+++ /dev/null
@@ -1,132 +0,0 @@
-## 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 sys
-from desktop.lib.i18n import smart_unicode
-if sys.version_info[0] > 2:
- from django.utils.translation import gettext as _
-else:
- from django.utils.translation import ugettext as _
-%>
-
-<%def name="sqlSyntaxDropdown()">
-
-
-
-%def>
diff --git a/desktop/libs/notebook/src/notebook/templates/editor_components.mako b/desktop/libs/notebook/src/notebook/templates/editor_components.mako
index d785b5dba99..cc51822cca3 100644
--- a/desktop/libs/notebook/src/notebook/templates/editor_components.mako
+++ b/desktop/libs/notebook/src/notebook/templates/editor_components.mako
@@ -103,7 +103,6 @@ else:
<%namespace name="dashboard" file="/common_dashboard.mako" />
-<%namespace name="sqlSyntaxDropdown" file="/sql_syntax_dropdown.mako" />
%def>
@@ -130,8 +129,6 @@ else:
% endif
-${ sqlSyntaxDropdown.sqlSyntaxDropdown() }
-