gethue
Version:
Hue is an Open source SQL Query Editor for Databases/Warehouses
1,011 lines (931 loc) • 41.7 kB
JavaScript
// 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 $ from 'jquery';
import * as ko from 'knockout';
import { CancellablePromise } from '../../../api/cancellablePromise';
import AsteriskContextTabs from './asteriskContextTabs';
import CollectionContextTabs from './collectionContextTabs';
import DataCatalogContext from './dataCatalogContext';
import DocumentContext, { DOCUMENT_CONTEXT_TEMPLATE } from './documentContext';
import FunctionContextTabs, { FUNCTION_CONTEXT_TEMPLATE } from './functionContext';
import { DOCUMENT_CONTEXT_FOOTER } from './ko.documentContextFooter';
import LangRefContext from './langRefContext';
import PartitionContext, { PARTITION_CONTEXT_TEMPLATE } from './partitionContext';
import ResizeHelper from './resizeHelper';
import StorageContext from './storageContext';
import { getNamespaces } from '../../../catalog/contextCatalog';
import dataCatalog from '../../../catalog/dataCatalog';
import { GET_KNOWN_CONFIG_TOPIC } from '../../../config/events';
import { findEditorConnector } from '../../../config/hueConfig';
import { ASSIST_KEY_COMPONENT } from '../../../ko/components/assist/ko.assistKey';
import componentUtils from '../../../ko/components/componentUtils';
import huePubSub from '../../../utils/huePubSub';
import I18n from '../../../utils/i18n';
import { getFromLocalStorage } from '../../../utils/storageUtils';
export const CONTEXT_POPOVER_CLASS = 'hue-popover';
export const HIDE_CONTEXT_POPOVER_EVENT = 'context.popover.hide';
export const CONTEXT_POPOVER_HIDDEN_EVENT = 'context.popover.hidden';
export const SHOW_CONTEXT_POPOVER_EVENT = 'context.popover.show';
export const NAME = 'context-popover';
// prettier-ignore
const SUPPORT_TEMPLATES = `
<script type="text/html" id="context-popover-footer">
<div class="context-popover-flex-bottom-links">
<div class="context-popover-link-row">
<a href="javascript: void(0);" class="inactive-action" data-bind="visible: showInAssistEnabled, publish: 'context.popover.show.in.assist'">
<i style="font-size: 11px;" title="${I18n(
'Show in Assist...'
)}" class="fa fa-search"></i> ${I18n('Assist')}
</a>
<!-- ko if: isDocument -->
<!-- ko with: contents -->
<!-- ko if: documentId -->
<a href="javascript: void(0);" class="inactive-action" data-bind="click: download">
<i style="font-size: 11px;" title="${I18n('Download')}" class="fa fa-download"></i> ${I18n('Download')}
</a>
<!-- /ko -->
<a href="javascript: void(0);" class="inactive-action" data-bind="click: open">
<i style="font-size: 11px;" title="${I18n('Open')}" class="fa fa-file-o"></i> ${I18n('Open')}
</a>
<!-- /ko -->
<!-- /ko -->
<!-- ko if: expandColumnsEnabled -->
<!-- ko with: contents.data -->
<!-- ko if: selectedColumns().length > 0 -->
<a class="inactive-action pointer" data-bind="click: expand">${I18n(
'Expand to selected columns'
)}</a>
<!-- /ko -->
<!-- ko if: selectedColumns().length === 0 -->
<a class="inactive-action pointer" data-bind="click: expand">${I18n(
'Expand to all columns'
)}</a>
<!-- /ko -->
<!-- /ko -->
<!-- /ko -->
</div>
<!-- ko if: isDocument -->
<div class="context-popover-bottom-attributes">
<!-- ko with: contents -->
<!-- ko component: { name: '${ DOCUMENT_CONTEXT_FOOTER }', params: { popoverData: $data } } --><!-- /ko -->
<!-- /ko -->
</div>
<!-- /ko -->
</div>
</script>
<script type="text/html" id="context-popover-collection-stats-details">
<div class="context-popover-flex-fill">
<div style="padding: 8px">
<div data-bind="with: stats">
<!-- ko hueSpinner: { spin: $parent.loadingStats, center: true, size: 'large' } --><!-- /ko -->
<div class="alert" data-bind="visible: !$parent.loadingStats() && !$parent.statsSupported()">${I18n(
'This field does not support stats'
)}</div>
<div class="alert" data-bind="visible: !$parent.loadingStats() && $parent.statsSupported() && $data.data().length == 0">${I18n(
'There are no stats to be shown'
)}</div>
<table style="width: 100%" data-bind="visible: !$parent.loadingStats() && $data.data().length > 0" class="table table-condensed">
<tbody data-bind="foreach: $data.data">
<tr>
<td style="vertical-align: top"><strong data-bind="text: key"></strong></td>
<!-- ko if: key == 'facets' -->
<td>
<!-- ko if: val[Object.keys(val)[0]] != null -->
<table>
<tbody data-bind="foreach: Object.keys(val[Object.keys(val)[0]])">
<tr>
<td style="vertical-align: top; padding-left: 4px; padding-right: 4px"><strong data-bind="text: $data"></strong></td>
<td data-bind="template: 'context-popover-collection-stats-facets'"></td>
</tr>
</tbody>
</table>
<!-- /ko -->
<!-- ko ifnot: val[Object.keys(val)[0]] != null -->
${I18n('Not available')}
<!-- /ko -->
</td>
<!-- /ko -->
<!-- ko ifnot: key == 'facets' -->
<td data-bind="text: val"></td>
<!-- /ko -->
</tr>
</tbody>
</table>
</div>
</div>
</div>
</script>
<script type="text/html" id="context-popover-collection-stats-facets">
<table style="width: 100%">
<tbody data-bind="foreach: Object.keys($parent.val[Object.keys($parent.val)[0]][$data])">
<tr>
<td style="vertical-align: top; padding-left: 4px; padding-right: 4px"><strong data-bind="text: $data"></strong></td>
<!-- ko ifnot: $data == 'facets' -->
<td style="vertical-align: top" data-bind="text: $parents[1].val[Object.keys($parents[1].val)[0]][$parent][$data]"></td>
<!-- /ko -->
</tr>
</tbody>
</table>
</script>
<script type="text/html" id="context-popover-collection-terms-details">
<div class="context-popover-flex-fill">
<input type="text" data-bind="value: terms.prefix, clearable: terms.prefix, valueUpdate:'afterkeydown'" placeholder="${I18n(
'Filter...'
)}" class="pull-right">
<div data-bind="with: terms">
<!-- ko hueSpinner: { spin: $parent.loadingTerms, center: true, size: 'large' } --><!-- /ko -->
<div class="clearfix"></div>
<div class="alert" data-bind="visible: !$parent.loadingTerms() && $data.data().length == 0">${I18n(
'There are no terms to be shown'
)}</div>
<table style="width: 100%" data-bind="visible: !$parent.loadingTerms() && $data.data().length > 0" class="table table-condensed">
<tbody data-bind="foreach: $data.data">
<tr>
<td data-bind="text: val.value"></td>
<td style="width: 40px">
<div class="progress">
<div class="bar-label" data-bind="text:val.count"></div>
<div class="bar bar-info" style="margin-top:-20px;" data-bind="style: {'width': ((val.count / $parent.data()[0].val.count) * 100) + '%'}"></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</script>
<script type="text/html" id="context-popover-asterisk-details">
<div class="context-popover-flex-fill">
<!-- ko component: { name: 'sql-columns-table', params: { columns: columns, scrollToColumns: false } } --><!-- /ko -->
</div>
</script>
<script type="text/html" id="context-hue-app-details">
<div class="context-popover-flex-fill" style="overflow: auto;" data-bind="with: data">
<div style="padding: 8px">
<div style="width:100%; text-align: center; margin-top: 30px; font-size: 100px; color: #787878;" data-bind="template: { name: 'app-icon-template', data: { icon: interpreter.type } }"></div>
<div style="width: 100%; margin-top: 20px; text-align:center">
<a style="font-size: 20px;" href="javascript:void(0)" data-bind="text: interpreter.displayName, hueLink: interpreter.page, click: function () { $parents[1].close(); }, attr: { 'title': interpreter.tooltip }"></a>
</div>
</div>
</div>
</script>
<script type="text/html" id="context-popover-contents">
<div class="context-popover-content">
<!-- ko with: contents -->
<!-- ko if: typeof tabs !== 'undefined' -->
<ul class="nav nav-tabs context-popover-tabs" data-bind="foreach: tabs">
<li data-bind="click: function () { $parent.activeTab(id); }, css: { 'active' : $parent.activeTab() === id }">
<a class="context-popover-tab" data-toggle="tab" data-bind="text: label, attr: { href: '#' + id }"></a>
</li>
</ul>
<div class="context-popover-tab-container" data-bind="foreach: tabs">
<div class="context-popover-tab-pane tab-pane" data-bind="visible : $parent.activeTab() === id, attr: { id: id }, css: { 'active' : $parent.activeTab() === id }">
<div class="context-popover-flex">
<!-- ko with: templateData -->
<div class="context-popover-flex-fill" data-bind="visible: loading"><!-- ko hueSpinner: { spin: loading, center: true, size: 'large' } --><!-- /ko --></div>
<!-- ko if: ! loading() && hasErrors() -->
<div class="context-popover-flex-fill">
<div class="alert">
<span data-bind="text: $parent.errorText"></span>
<!-- ko if: $parent.enableSampleError && $parents[1].activeTab() === 'sample' -->
<a href="javascript:void(0);" data-bind="click: function(){ huePubSub.publish('sample.error.insert.click', $data); huePubSub.publish('context.popover.hide');}">${I18n(
'Insert '
)}<span data-bind="text:$parent.title"></span> ${I18n('sample query')}</a> ${I18n(
'at cursor'
)}
<!-- /ko -->
</div>
</div>
<!-- /ko -->
<!-- ko if: ! loading() && ! hasErrors() -->
<!-- ko template: { name: $parent.template } --><!-- /ko -->
<!-- /ko -->
<!-- /ko -->
<!-- ko template: { name: 'context-popover-footer', data: $parents[1] } --><!-- /ko -->
</div>
</div>
</div>
<!-- /ko -->
<!-- ko if: typeof tabs === 'undefined' -->
<div class="context-popover-flex-fill" data-bind="visible: loading"><!-- ko hueSpinner: { spin: loading, center: true, size: 'large' } --><!-- /ko --></div>
<!-- ko if: !loading() && hasErrors() -->
<div class="context-popover-flex-fill">
<div class="alert" data-bind="text: errorText"></div>
</div>
<!-- /ko -->
<!-- ko if: !loading() && !hasErrors() -->
<!-- ko template: { name: template } --><!-- /ko -->
<!-- /ko -->
<!-- ko template: { name: 'context-popover-footer', data: $parent } --><!-- /ko -->
<!-- /ko -->
<!-- /ko -->
</div>
</script>
<script type="text/html" id="context-catalog-doc-title">
</script>
<script type="text/html" id="context-lang-ref-title">
<div class="hue-popover-title" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-right: 20px;">
<i class="fa fa-book muted" style="margin-top: 3px"></i> <span class="blue" style="padding-left: 4px;" data-bind="text: title"></span>
<div class="hue-popover-title-actions">
<a class="pointer inactive-action" title="${I18n(
'Pin'
)}" data-bind="visible: popover.pinEnabled, click: popover.pin"><i class="fa fa-fw fa-thumb-tack"></i></a>
<a class="pointer inactive-action" title="${I18n(
'Close'
)}" data-bind="visible: !popover.closeDisabled, click: popover.close"><i class="fa fa-fw fa-times"></i></a>
</div>
</div>
</script>
<script type="text/html" id="context-lang-ref-contents">
<div class="context-popover-content">
<div class="context-popover-flex-fill context-popover-docs-details" data-bind="htmlUnsecure: body"></div>
<div class="context-popover-flex-bottom-links">
<div class="context-popover-link-row">
<a class="inactive-action pointer" data-bind="click: openInRightAssist">
<i style="font-size: 11px;" title="${I18n(
'Show in Assist...'
)}" class="fa fa-search"></i> ${I18n('Assist')}
</a>
</div>
</div>
</div>
</script>
<script type="text/html" id="context-catalog-entry-title">
<div class="hue-popover-title">
<!-- ko if: catalogEntry().isTable() -->
<span class="hue-popover-title-secondary-icon-container">
<!-- ko hueSpinner: { spin: loading, inline: true } --><!-- /ko -->
<!-- ko ifnot: loading -->
<!-- ko if:catalogEntry().isIcebergTable() -->
<i class="hue-popover-title-icon fa muted fa-snowflake-o" title="${I18n(
'Iceberg table'
)}"></i>
<!-- /ko -->
<!-- ko ifnot:catalogEntry().isIcebergTable() -->
<i class="hue-popover-title-icon fa muted fa-table"></i>
<!-- /ko -->
<!-- /ko -->
</span>
<!-- /ko -->
<!-- ko ifnot: catalogEntry().isTable() -->
<i class="hue-popover-title-icon fa muted" data-bind="css:
catalogEntry() && (catalogEntry().isView() || parentIsView())
? 'fa-eye'
: (catalogEntry().isDatabase()
? 'fa-database'
: (catalogEntry().isModel()
? 'fa-puzzle-piece'
: 'fa-table'))"></i>
<!-- /ko -->
<span class="hue-popover-title-text" data-bind="foreach: breadCrumbs">
<!-- ko ifnot: isActive --><div><a href="javascript: void(0);" data-bind="click: makeActive, text: name"></a>.</div><!-- /ko -->
<!-- ko if: isActive -->
<div>
<span data-bind="text: name"></span>
<!-- ko with: catalogEntry -->
<!-- ko if: isField() -->
(<span data-bind="text: getType()"></span>)
<!-- ko if: isKey() -->
<!-- ko component: { name: '${ ASSIST_KEY_COMPONENT }', params: { entry: $data, onForeignKeyClick: $parents[1].setEntry.bind($parents[1]) } } --><!-- /ko -->
<!-- /ko -->
<!-- /ko -->
<!-- /ko -->
</div>
<!-- /ko -->
</span>
<div class="hue-popover-title-actions">
<!-- ko hueSpinner: { spin: loading, inline: true } --><!-- /ko -->
<a class="pointer inactive-action" title="${I18n(
'Refresh'
)}" data-bind="visible: !loading() && catalogEntry() && !catalogEntry().isTemporary, click: refresh"><i class="fa fa-fw fa-refresh"></i></a>
<a class="pointer inactive-action" title="${I18n(
'Pin'
)}" data-bind="visible: popover.pinEnabled && catalogEntry() && !catalogEntry().isTemporary, click: popover.pin"><i class="fa fa-fw fa-thumb-tack"></i></a>
<a class="pointer inactive-action" title="${I18n(
'Close'
)}" data-bind="visible: !popover.closeDisabled, click: popover.close"><i class="fa fa-fw fa-times"></i></a>
</div>
</div>
</script>
<script type="text/html" id="context-catalog-entry-contents">
<div class="context-popover-content">
<div class="context-popover-flex-fill" data-bind="visible: loading"><!-- ko hueSpinner: { spin: loading, center: true, size: 'xlarge' } --><!-- /ko --></div>
<!-- ko if: !loading() && hasErrors() -->
<div class="context-popover-flex-fill">
<div class="alert" data-bind="text: errorText"></div>
</div>
<!-- /ko -->
<!-- ko if: !loading() && !hasErrors() && typeof catalogEntry() !== 'undefined'-->
<div class="context-popover-flex-fill" data-bind="with: catalogEntry">
<div class="context-popover-inner-content">
<!-- ko if: $parent.comment() && !isTemporary -->
<div class="context-popover-comment" data-bind="attr: { 'title': $parent.comment }, multiLineEllipsis: { expanded: $parent.commentExpanded, expandable: true, expandClass: 'context-popover-comment-expanded' }, text: $parent.comment"></div>
<!-- /ko -->
<!-- ko ifnot: $parent.commentExpanded -->
<!-- ko if: window.HAS_CATALOG && !isTemporary && (getDialect() === 'hive' || getDialect() === 'impala') -->
<div data-bind="component: { name: 'nav-tags', params: { catalogEntry: $data, overflowEllipsis: true } }"></div>
<!-- /ko -->
<!-- ko if: isView() && $parent.viewSql() -->
<a href="javascript:void(0);" style="text-align: right; margin-bottom: 5px;" data-bind="toggle: $parent.viewSqlVisible, text: $parent.viewSqlVisible() ? '${I18n(
'Show columns'
)}' : '${I18n('Show view SQL')}'"></a>
<!-- /ko -->
<!-- ko if: $parent.viewSqlVisible -->
<div class="context-popover-sql" data-bind="highlight: { value: $parent.viewSql, enableOverflow: true, formatted: true, dialect: getDialect() }"></div>
<!-- /ko -->
<!-- ko ifnot: $parent.viewSqlVisible -->
<!-- ko component: { name: 'catalog-entries-list', params: { catalogEntry: $data, onClick: $parent.catalogEntry, onSampleClick: $parent.onSampleClick } } --><!-- /ko -->
<!-- /ko -->
<!-- /ko -->
</div>
</div>
<div class="context-popover-flex-bottom-links">
<div class="context-popover-link-row">
<!-- ko if: catalogEntry -->
<a class="inactive-action pointer" data-bind="visible: popover.showInAssistEnabled || catalogEntry() !== originalCatalogEntry, click: showInAssist">
<i style="font-size: 11px;" title="${I18n(
'Show in Assist...'
)}" class="fa fa-search"></i> ${I18n('Assist')}
</a>
<!-- ko if: window.HAS_SQL_DASHBOARD -->
<a class="inactive-action pointer" data-bind="visible: openActionsEnabled, click: openInDashboard">
<i style="font-size: 11px;" title="${I18n(
'Open in Dashboard...'
)}" class="fa fa-external-link"></i> ${I18n('Dashboard')}
</a>
<!-- /ko -->
<!-- ko if: catalogEntry().getDialect() !== 'solr' && openActionsEnabled() -->
<a class="inactive-action pointer" data-bind="click: openInTableBrowser">
<i style="font-size: 11px;" title="${I18n(
'Open in Table Browser...'
)}" class="fa fa-external-link"></i> ${I18n('Table Browser')}
</a>
<!-- /ko -->
<!-- /ko -->
</div>
</div>
<!-- /ko -->
</div>
</script>
<script type="text/html" id="context-storage-entry-title">
<div class="hue-popover-title">
<i class="hue-popover-title-icon fa muted" data-bind="css: storageEntry() && storageEntry().definition.type === 'dir' ? 'fa-folder-o' : 'fa-file-o'"></i>
<span class="hue-popover-title-text" data-bind="foreach: breadCrumbs">
<!-- ko ifnot: isActive --><div><a href="javascript: void(0);" data-bind="click: makeActive, text: name"></a><!-- ko if: $index() > 0 -->/<!-- /ko --></div><!-- /ko -->
<!-- ko if: isActive -->
<div>
<span data-bind="text: name"></span>
</div>
<!-- /ko -->
</span>
<div class="hue-popover-title-actions">
<!-- ko hueSpinner: { spin: loading, inline: true } --><!-- /ko -->
<a class="pointer inactive-action" title="${I18n(
'Go Home'
)}" data-bind="visible: !loading(), click: goHome"><i class="fa fa-fw fa-home"></i></a>
<a class="pointer inactive-action" title="${I18n(
'Close'
)}" data-bind="visible: !popover.closeDisabled, click: popover.close"><i class="fa fa-fw fa-times"></i></a>
</div>
</div>
</script>
<script type="text/html" id="context-storage-entry-contents">
<div class="context-popover-content" data-bind="with: storageEntry">
<!-- ko if: !loading() && hasErrors() -->
<div class="context-popover-flex-fill">
<div class="alert" data-bind="text: errorText"></div>
</div>
<!-- /ko -->
<div class="context-popover-flex-fill" data-bind="visible: loading"><!-- ko hueSpinner: { spin: loading, center: true, size: 'xlarge' } --><!-- /ko --></div>
<!-- ko if: !loading() && !hasErrors() -->
<!-- ko with: definition -->
<div class="context-popover-flex-attributes">
<!-- ko if: typeof humansize !== 'undefined' -->
<div class="context-popover-attribute"><div>${I18n(
'Size'
)}</div><div data-bind="text: humansize"></div></div>
<!-- /ko -->
<!-- ko if: typeof stats !== 'undefined' -->
<!-- ko with: stats -->
<!-- ko if: user -->
<div class="context-popover-attribute"><div>${I18n(
'Owner'
)}</div><div data-bind="text: user"></div></div>
<!-- /ko -->
<!-- /ko -->
<!-- /ko -->
<!-- ko if: typeof rwx !== 'undefined' -->
<div class="context-popover-attribute"><div>${I18n(
'Permissions'
)}</div><div data-bind="text: rwx"></div></div>
<!-- /ko -->
</div>
<!-- /ko -->
<!-- ko if: definition.type === 'dir' -->
<div class="context-popover-flex-fill storage-entry-container" data-bind="fetchMore: { fetchMore: fetchMore.bind($data), hasMore: hasMorePages, loadingMore: loadingMore.bind($data) }">
<table class="table table-condensed table-nowrap">
<thead>
<tr>
<th width="1%"></th>
<th>${I18n('Name')}</th>
<th>${I18n('Size')}</th>
<th>${I18n('Permissions')}</th>
</tr>
</thead>
<tbody>
<!-- ko if: $parent.storageEntry().parent -->
<tr>
<td><i class="fa fa-folder-o"></i></td>
<td><a href="javascript: void(0);" data-bind="click: function () { $parent.storageEntry($parent.storageEntry().parent) }">..</a></td>
<td data-bind="text: $parent.storageEntry().definition.humansize"></td>
<td data-bind="text: $parent.storageEntry().definition.rwx"></td>
</tr>
<!-- /ko -->
<!-- ko foreach: entries -->
<tr>
<td><i class="fa" data-bind="css: definition.type === 'dir' ? 'fa-folder-o' : 'fa-file-o'"></i></td>
<td><a href="javascript: void(0);" data-bind="click: function () { $parents[1].storageEntry($data) }, text: definition.name"></a></td>
<td data-bind="text: definition.humansize"></td>
<td data-bind="text: definition.rwx"></td>
</tr>
<!-- /ko -->
</tbody>
</table>
</div>
<!-- /ko -->
<!-- ko if: definition.type !== 'dir' -->
<div class="context-popover-flex-header"><div class="context-popover-header">${I18n(
'Preview'
)}</div></div>
<div class="context-popover-flex-fill storage-entry-container">
<div data-bind="with: preview">
<!-- ko if: view && view.contents -->
<pre data-bind="text: view.contents"></pre>
<!-- /ko -->
<!-- ko if: view && !view.contents -->
<div class="empty-file-contents">${I18n('Empty file...')}</div>
<!-- /ko -->
</div>
</div>
<!-- /ko -->
<!-- /ko -->
<div class="context-popover-flex-bottom-links">
<div class="context-popover-link-row">
<!-- ko ifnot: loading -->
<a class="inactive-action pointer" data-bind="click: $parent.openInFileBrowser">
<i style="font-size: 11px;" title="${I18n(
'Open in File Browser...'
)}" class="fa fa-external-link"></i> ${I18n('File Browser')}
</a>
<!-- ko if: typeof $parent.editorLocation !== 'undefined' -->
<a class="inactive-action pointer" data-bind="click: function () { $parent.replaceInEditor($data, $parent) }">
<i style="font-size: 11px;" title="${I18n(
'Replace the editor content...'
)}" class="fa fa-pencil"></i> ${I18n('Insert in the editor')}
</a>
<!-- /ko -->
<!-- /ko -->
</div>
</div>
</div>
</script>
`;
// prettier-ignore
const CONTEXT_POPOVER_TEMPLATE = `
<div class="${ CONTEXT_POPOVER_CLASS }" data-bind="css: orientationClass, style: { 'left': left() + 'px', 'top': top() + 'px', 'width': width() + 'px', height: height() + 'px' }, resizable: { containment: 'document', handles: resizeHelper.resizableHandles, start: resizeHelper.resizeStart, stop: resizeHelper.resizeStop, resize: resizeHelper.resize }">
<div class="hue-popover-arrow" data-bind="style: { 'margin-left': leftAdjust() + 'px', 'margin-top': topAdjust() + 'px' }"></div>
<!-- ko if: typeof titleTemplate !== 'undefined' -->
<!-- ko template: { name: titleTemplate, data: contents } --><!-- /ko -->
<!-- /ko -->
<!-- ko if: typeof titleTemplate === 'undefined' -->
<div class="hue-popover-title" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-right: 30px;">
<i class="fa muted" data-bind="css: iconClass" style="margin-top: 3px"></i> <span style="padding-left: 4px;" data-bind="text: title"></span>
<!-- ko if: typeof subtitle !== 'undefined' && subtitle -->
<span class="muted" style="padding-left: 4px;" data-bind="html: subtitle"></span>
<!-- /ko -->
<div style="position: absolute; right: 6px; top: 8px;">
<a class="pointer inactive-action" data-bind="visible: pinEnabled, click: pin"><i class="fa fa-fw fa-thumb-tack"></i></a>
<a class="pointer inactive-action" data-bind="click: close"><i class="fa fa-fw fa-times"></i></a>
</div>
</div>
<!-- /ko -->
<!-- ko if: typeof contentsComponent !== 'undefined' -->
<!-- ko component: { name: contentsComponent, params: data } --><!-- /ko -->
<!-- /ko -->
<!-- ko if: typeof contentsTemplate !== 'undefined' -->
<!-- ko template: { name: contentsTemplate, data: contents } --><!-- /ko -->
<!-- /ko -->
<!-- ko if: typeof contentsTemplate === 'undefined' && typeof contentsComponent === 'undefined' -->
<!-- ko template: 'context-popover-contents' --><!-- /ko -->
<!-- /ko -->
</div>
`;
// prettier-ignore
const GLOBAL_SEARCH_TEMPLATE = `
<!-- ko if: isCatalogEntry -->
<!-- ko with: contents -->
<div class="global-search-catalog-entry">
<!-- ko template: 'context-catalog-entry-title' --><!-- /ko -->
<!-- ko template: 'context-catalog-entry-contents' --><!-- /ko -->
</div>
<!-- /ko -->
<!-- /ko -->
<!-- ko ifnot: isCatalogEntry -->
<!-- ko template: 'context-popover-contents' --><!-- /ko -->
<!-- /ko -->
`;
const HALF_ARROW = 6;
let preventHide = false;
const hidePopover = function () {
if (!preventHide) {
const $contextPopover = $('#contextPopover');
if ($contextPopover.length > 0) {
ko.cleanNode($contextPopover[0]);
$contextPopover.remove();
$(document).off('click.context');
huePubSub.publish(CONTEXT_POPOVER_HIDDEN_EVENT);
}
}
};
class ContextPopoverViewModel {
constructor(params) {
const self = this;
self.disposals = [];
self.left = ko.observable(0);
self.top = ko.observable(0);
const popoverSize = getFromLocalStorage('assist.popover.size', {
width: 450,
height: 400
});
self.width = ko.observable(popoverSize.width);
self.height = ko.observable(popoverSize.height);
self.leftAdjust = ko.observable(0);
self.topAdjust = ko.observable(0);
self.data = params.data;
self.connector = params.connector;
self.sourceType = params.sourceType;
self.namespace = params.namespace;
self.compute = params.compute;
self.defaultDatabase = params.defaultDatabase;
self.close = hidePopover;
self.onSampleClick = params.onSampleClick;
const orientation = params.orientation || 'bottom';
self.contents = null;
self.resizeHelper = new ResizeHelper(orientation, self.leftAdjust, self.topAdjust, noHide => {
preventHide = noHide;
});
if (typeof params.source.element !== 'undefined') {
// Track the source element and close the popover if moved
const $source = $(params.source.element);
const originalSourceOffset = $source.offset();
let currentSourceOffset;
const detectMoveInterval = window.setInterval(() => {
currentSourceOffset = $source.offset();
if (currentSourceOffset.top !== originalSourceOffset.top) {
hidePopover();
}
}, 200);
self.disposals.push(() => {
window.clearInterval(detectMoveInterval);
});
}
const windowWidth = $(window).width();
const fitHorizontally = function () {
let left =
params.source.left +
Math.round((params.source.right - params.source.left) / 2) -
self.width() / 2;
if (left + self.width() > windowWidth - 10) {
self.leftAdjust(left + self.width() - windowWidth + 5);
left = windowWidth - self.width() - 10;
} else if (left < 10) {
self.leftAdjust(left - 10 - HALF_ARROW);
left = 10;
} else {
self.leftAdjust(-HALF_ARROW);
}
self.left(left);
};
const windowHeight = $(window).height();
const fitVertically = function () {
let top =
params.source.top +
Math.round((params.source.bottom - params.source.top) / 2) -
self.height() / 2;
if (top + self.height() > windowHeight - 10) {
self.topAdjust(top + self.height() - windowHeight + 5);
top = windowHeight - self.height() - 10;
} else if (top < 10) {
self.topAdjust(top - 10 - HALF_ARROW);
top = 10;
} else {
self.topAdjust(-HALF_ARROW);
}
self.top(top);
};
switch (orientation) {
case 'top':
fitHorizontally();
self.top(params.source.top - self.height());
break;
case 'right':
fitVertically();
self.left(params.source.right);
break;
case 'bottom':
fitHorizontally();
self.top(params.source.bottom);
break;
case 'left':
fitVertically();
self.left(params.source.left - self.width());
}
self.isSolr = params.sourceType === 'solr';
self.isComplex = params.data.type === 'complex';
self.isFunction = params.data.type === 'function';
self.isStorageEntry = params.data.type === 'storageEntry';
self.isAsterisk = params.data.type === 'asterisk';
self.isDocument = params.data.type.toLowerCase() === 'hue';
self.isCollection = params.data.type === 'collection';
self.isCatalogEntry = !!params.data.catalogEntry;
self.isLangRef = params.data.type === 'statementType';
if (
self.isCatalogEntry &&
params.data.catalogEntry.getDialect() === 'solr' &&
params.data.catalogEntry.isField()
) {
self.isCollection = true;
self.isCatalogEntry = false;
self.namespace = params.data.catalogEntry.namespace;
self.compute = params.data.catalogEntry.compute;
}
self.showInAssistEnabled =
(typeof params.showInAssistEnabled !== 'undefined' ? params.showInAssistEnabled : true) &&
(self.isDocument || self.isCollection || self.isCatalogEntry);
self.openInDashboardEnabled = self.isCatalogEntry && params.data.catalogEntry.path.length <= 2;
self.openInTableBrowserEnabled =
self.isCatalogEntry && params.data.catalogEntry.path.length <= 2;
self.expandColumnsEnabled = self.isAsterisk;
self.pinEnabled =
params.pinEnabled &&
!self.isFunction &&
!self.isAsterisk &&
!self.isStorageEntry &&
!self.isCatalogEntry;
if (params.data.type === 'quickQuery') {
self.contentsComponent = 'quick-query-context';
self.title = I18n('Quick Query');
self.iconClass = 'fa-play';
self.pinEnabled = false;
} else if (self.isCatalogEntry) {
self.contents = new DataCatalogContext({
popover: self,
catalogEntry: params.data.catalogEntry
});
self.titleTemplate = 'context-catalog-entry-title';
self.contentsTemplate = 'context-catalog-entry-contents';
} else if (self.isFunction) {
self.contents = new FunctionContextTabs(self.data, self.connector);
self.title = self.data.function;
self.iconClass = 'fa-superscript';
} else if (self.isStorageEntry) {
self.contents = new StorageContext({
popover: self,
storageEntry: params.data.storageEntry,
editorLocation: params.data.editorLocation
});
self.titleTemplate = 'context-storage-entry-title';
self.contentsTemplate = 'context-storage-entry-contents';
} else if (self.isAsterisk) {
self.contents = new AsteriskContextTabs(
self.data,
self.connector,
self.namespace,
self.compute,
self.defaultDatabase
);
self.title = '*';
self.iconClass = 'fa-table';
} else if (self.isDocument) {
self.contents = new DocumentContext(self.data.definition);
self.titleTemplate = 'context-catalog-doc-title';
} else if (self.isCollection) {
self.contents = new CollectionContextTabs(self.data);
self.title = self.data.catalogEntry.path.slice(1).join('.');
self.iconClass = 'fa-search';
} else if (self.isLangRef) {
self.contents = new LangRefContext({ popover: self, data: params.data });
self.titleTemplate = 'context-lang-ref-title';
self.contentsTemplate = 'context-lang-ref-contents';
} else {
self.title = '';
self.iconClass = 'fa-info';
}
self.orientationClass = 'hue-popover-' + orientation;
if (params.delayedHide) {
let hideTimeout = -1;
const onLeave = function () {
hideTimeout = window.setTimeout(() => {
$('.hue-popover').fadeOut(200, () => {
hidePopover();
});
}, 1000);
};
const onEnter = function () {
window.clearTimeout(hideTimeout);
};
$(params.delayedHide)
.add($('.hue-popover'))
.on('mouseleave', onLeave)
.on('mouseenter', onEnter);
const keepPopoverOpenOnClick = function () {
window.clearTimeout(hideTimeout);
$(params.delayedHide)
.add($('.hue-popover'))
.off('mouseleave', onLeave)
.off('mouseenter', onEnter);
};
$('.hue-popover').on('click', keepPopoverOpenOnClick);
self.disposals.push(() => {
$(params.delayedHide)
.add($('.hue-popover'))
.off('mouseleave', onLeave)
.off('mouseenter', onEnter);
$('.hue-popover').off('click', keepPopoverOpenOnClick);
});
}
const closeOnEsc = function (e) {
if (e.keyCode === 27) {
hidePopover();
}
};
$(document).on('keyup', closeOnEsc);
self.disposals.push(() => {
$(document).off('keyup', closeOnEsc);
});
window.setTimeout(() => {
$(document).off('click.context');
$(document).on('click.context', event => {
if (
$.contains(document, event.target) &&
!$.contains($('#contextPopover')[0], event.target) &&
($('.modal')[0].length === 0 || !$.contains($('.modal')[0], event.target))
) {
hidePopover();
}
});
}, 0);
self.disposals.push(() => {
$(document).off('click.context');
});
}
dispose() {
const self = this;
while (self.disposals.length) {
self.disposals.pop()();
}
if (self.contents && self.contents.dispose) {
self.contents.dispose();
}
huePubSub.publish('context.popover.dispose');
}
pin() {
const self = this;
hidePopover();
if (self.contents && typeof self.contents.sample !== 'undefined') {
self.contents.sample.fetchedData(undefined);
}
huePubSub.publish('sql.context.pin', self);
if (self.contents && self.contents.activeTab() === 'sample') {
self.contents.refetchSamples();
}
}
}
componentUtils
.registerComponent(
NAME,
ContextPopoverViewModel,
SUPPORT_TEMPLATES +
DOCUMENT_CONTEXT_TEMPLATE +
FUNCTION_CONTEXT_TEMPLATE +
PARTITION_CONTEXT_TEMPLATE +
CONTEXT_POPOVER_TEMPLATE
)
.then(() => {
huePubSub.subscribe(HIDE_CONTEXT_POPOVER_EVENT, hidePopover);
huePubSub.subscribe(SHOW_CONTEXT_POPOVER_EVENT, details => {
hidePopover();
const $contextPopover = $(
'<div id="contextPopover" data-bind="component: { name: \'context-popover\', params: $data }" ></div>'
);
$('body').append($contextPopover);
ko.applyBindings(details, $contextPopover[0]);
huePubSub.publish('context.popover.shown');
});
});
class SqlContextContentsGlobalSearch {
constructor(params) {
const self = this;
self.contents = ko.observable();
self.disposals = [];
self.isCatalogEntry =
params.data.type === 'catalogEntry' ||
params.data.type.toLowerCase() === 'database' ||
params.data.type.toLowerCase() === 'table' ||
params.data.type.toLowerCase() === 'field' ||
params.data.type.toLowerCase() === 'view';
self.isDocument = params.data.type.toLowerCase() === 'hue';
self.isPartition = params.data.type.toLowerCase() === 'partition';
self.close = params.globalSearch.close.bind(params.globalSearch);
// These are currently not in the global search results
self.isStorageEntry = false;
self.isAsterisk = false;
self.isComplex = false;
self.isFunction = false;
self.showInAssistEnabled = !self.isHueApp;
self.openInDashboardEnabled = false;
self.openInTableBrowserEnabled = false;
self.expandColumnsEnabled = self.isAsterisk;
self.closeDisabled = true; // Global search has it's own close
const adaptedData = { identifierChain: [] };
const path = params.data.originalName.split('.');
path.forEach(part => {
adaptedData.identifierChain.push({ name: part });
});
let connectorId = params.data.sourceType && params.data.sourceType.toLowerCase();
if (!connectorId || connectorId === 'hive') {
huePubSub.publish(GET_KNOWN_CONFIG_TOPIC, clusterConfig => {
if (clusterConfig) {
const defaultEditor = clusterConfig['default_sql_interpreter'];
if (!connectorId || (connectorId === 'hive' && defaultEditor === 'impala')) {
connectorId = defaultEditor;
}
}
});
}
if (self.isCatalogEntry) {
// TODO: Connector, Namespace and compute selection for global search results?
let connector = findEditorConnector(connector => connector.id === connectorId);
if (!connector) {
// TODO: Global search results are referring to dialect and not type
connector = findEditorConnector(connector => connector.dialect === connectorId);
}
getNamespaces({ connector })
.then(context => {
dataCatalog
.getEntry({
namespace: context.namespaces[0],
compute: context.namespaces[0].computes[0],
connector: connector,
path: path,
definition: { type: params.data.type.toLowerCase() }
})
.then(catalogEntry => {
catalogEntry.navigatorMeta = params.data;
catalogEntry.navigatorMetaPromise = CancellablePromise.resolve(
catalogEntry.navigatorMeta
);
catalogEntry.saveLater();
self.contents(new DataCatalogContext({ popover: self, catalogEntry: catalogEntry }));
});
})
.catch();
} else if (self.isDocument) {
self.contents(new DocumentContext(params.data));
} else if (self.isPartition) {
self.contents(new PartitionContext(params.data));
}
}
dispose() {
const self = this;
while (self.disposals.length) {
self.disposals.pop()();
}
if (self.contents && self.contents.dispose) {
self.contents.dispose();
}
huePubSub.publish('context.popover.dispose');
}
}
componentUtils.registerComponent(
'context-popover-contents-global-search',
SqlContextContentsGlobalSearch,
SUPPORT_TEMPLATES +
DOCUMENT_CONTEXT_TEMPLATE +
FUNCTION_CONTEXT_TEMPLATE +
PARTITION_CONTEXT_TEMPLATE +
GLOBAL_SEARCH_TEMPLATE
);