gethue
Version:
Hue is an Open source SQL Query Editor for Databases/Warehouses
333 lines (300 loc) • 13.3 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 * as ko from 'knockout';
import { CONFIG_REFRESHED_TOPIC } from '../../../config/events';
import { filterEditorConnectors } from '../../../config/hueConfig';
import componentUtils from '../../../ko/components/componentUtils';
import { HUE_DROP_DOWN_COMPONENT } from '../../../ko/components/ko.dropDown';
import sqlReferenceRepository from '../../../sql/reference/sqlReferenceRepository';
import {
CLEAR_UDF_CACHE_EVENT,
DESCRIBE_UDF_EVENT,
getUdfCategories,
UDF_DESCRIBED_EVENT
} from '../../../sql/reference/sqlUdfRepository';
import huePubSub from '../../../utils/huePubSub';
import I18n from '../../../utils/i18n';
import { getFromLocalStorage, setInLocalStorage } from '../../../utils/storageUtils';
export const NAME = 'assist-functions-panel';
// prettier-ignore
const TEMPLATE = `
<div class="assist-inner-panel">
<div class="assist-flex-panel">
<div class="assist-flex-header">
<div class="assist-inner-header">
<div class="function-dialect-dropdown" style="display: inline-block" data-bind="
component: {
name: '${ HUE_DROP_DOWN_COMPONENT }',
params: {
fixedPosition: true,
value: activeConnectorUdfs,
entries: availableConnectorUdfs,
linkTitle: '${I18n('Selected type')}'
}
}
"></div>
<div class="pull-right" style="margin-right: 15px;" data-bind="with: activeConnectorUdfs">
<!-- ko ifnot: loading -->
<a class="inactive-action" href="javascript:void(0)" data-bind="click: refresh" title="${I18n('Refresh')}">
<i class="pointer fa fa-refresh"></i>
</a>
<!-- /ko -->
<!-- ko if: loading -->
<i class="fa fa-refresh fa-spin blue"></i>
<!-- /ko -->
</div>
</div>
</div>
<!-- ko with: activeConnectorUdfs -->
<!-- ko hueSpinner: { spin: loading, center: true, size: 'large' } --><!-- /ko -->
<!-- ko ifnot: loading -->
<div class="assist-flex-search">
<div class="assist-filter">
<form autocomplete="off">
<input class="clearable" type="text" ${ window.PREVENT_AUTOFILL_INPUT_ATTRS } placeholder="Filter..." data-bind="clearable: query, value: query, valueUpdate: 'afterkeydown'">
</form>
</div>
</div>
<div data-bind="css: { 'assist-flex-fill': !selectedFunction(), 'assist-flex-half': selectedFunction() }">
<!-- ko ifnot: query -->
<ul class="assist-function-categories" data-bind="foreach: categories">
<li>
<a class="black-link" href="javascript: void(0);" data-bind="toggle: open"><i class="fa fa-fw" data-bind="css: { 'fa-chevron-right': !open(), 'fa-chevron-down': open }"></i> <span data-bind="text: name"></span></a>
<ul class="assist-functions" data-bind="slideVisible: open, foreach: functions">
<li data-bind="tooltip: { title: description, placement: 'left', delay: 1000 }">
<a class="assist-field-link" href="javascript: void(0);" data-bind="draggableText: { text: draggable, meta: { type: 'function' } }, css: { 'blue': $parents[1].selectedFunction() === $data }, multiClick: { click: function () { $parents[1].selectedFunction($data); }, dblClick: function () { huePubSub.publish('editor.insert.at.cursor', { text: draggable }); } }, text: signature"></a>
</li>
</ul>
</li>
</ul>
<!-- /ko -->
<!-- ko if: query -->
<!-- ko if: filteredFunctions().length > 0 -->
<ul class="assist-functions" data-bind="foreach: filteredFunctions">
<li data-bind="tooltip: { title: description, placement: 'left', delay: 1000 }">
<a class="assist-field-link" href="javascript: void(0);" data-bind="draggableText: { text: draggable, meta: { type: 'function' } }, css: { 'blue': $parent.selectedFunction() === $data }, multiClick: { click: function () { $parent.selectedFunction($data); }, dblClick: function () { huePubSub.publish('editor.insert.at.cursor', { text: draggable }); } }, html: signatureMatch"></a>
</li>
</ul>
<!-- /ko -->
<!-- ko if: filteredFunctions().length === 0 -->
<ul class="assist-functions">
<li class="assist-no-entries">${I18n('No results found.')}</li>
</ul>
<!-- /ko -->
<!-- /ko -->
</div>
<!-- ko if: selectedFunction -->
<div class="assist-flex-half assist-function-details" data-bind="with: selectedFunction">
<!-- ko hueSpinner: { spin: !loaded(), center: true, size: 'large' } --><!-- /ko -->
<!-- ko if: loaded -->
<div class="assist-panel-close"><button class="close" data-bind="click: function() { $parent.selectedFunction(null); }">×</button></div>
<div class="assist-function-signature blue" data-bind="draggableText: { text: draggable, meta: { type: 'function' } }, text: signature, event: { 'dblclick': function () { huePubSub.publish('editor.insert.at.cursor', { text: draggable }); } }"></div>
<!-- ko if: description -->
<div data-bind="html: descriptionMatch"></div>
<!-- /ko -->
<!-- /ko -->
</div>
<!-- /ko -->
<!-- /ko -->
<!-- /ko -->
</div>
</div>
`;
class ConnectorUdfCategories {
constructor(connector) {
this.connector = connector;
this.loading = ko.observable(true);
this.initialized = false;
this.label = this.connector.displayName;
this.categories = ko.observableArray([]);
this.query = ko.observable().extend({ rateLimit: 400 });
this.selectedFunction = ko.observable();
this.selectedFunction.subscribe(selectedUdf => {
if (selectedUdf) {
if (!selectedUdf.category.open()) {
selectedUdf.category.open(true);
}
if (!selectedUdf.loaded()) {
huePubSub.publish(DESCRIBE_UDF_EVENT, {
connector: this.connector,
udfName: selectedUdf.name
});
}
}
});
huePubSub.subscribe(UDF_DESCRIBED_EVENT, details => {
// TODO: Show UDFs from databases in the assist functions panel
if (!details.database && details.connector.id === this.connector.id) {
this.categories().some(category =>
category.functions.some(udf => {
if (udf.name === details.udf.name) {
udf.loaded(details.udf.described);
udf.signature(details.udf.signature);
udf.signatureMatch(details.udf.signature);
udf.description(details.udf.description);
udf.descriptionMatch(details.udf.description);
udf.loaded(details.udf.described);
return true;
}
})
);
}
});
this.filteredFunctions = ko.pureComputed(() => {
const result = [];
if (this.loading()) {
return result;
}
const lowerCaseQuery = this.query().toLowerCase();
const replaceRegexp = new RegExp('(' + lowerCaseQuery + ')', 'i');
this.categories().forEach(category => {
category.functions.forEach(fn => {
if (fn.signature().toLowerCase().indexOf(lowerCaseQuery) === 0) {
fn.weight = 2;
fn.signatureMatch(fn.signature().replace(replaceRegexp, '<b>$1</b>'));
fn.descriptionMatch(fn.description());
result.push(fn);
} else if (fn.signature().toLowerCase().indexOf(lowerCaseQuery) !== -1) {
fn.weight = 1;
fn.signatureMatch(fn.signature().replace(replaceRegexp, '<b>$1</b>'));
fn.descriptionMatch(fn.description());
result.push(fn);
} else if (
fn.description() &&
fn.description().toLowerCase().indexOf(lowerCaseQuery) !== -1
) {
fn.signatureMatch(fn.signature());
fn.descriptionMatch(fn.description().replace(replaceRegexp, '<b>$1</b>'));
fn.weight = 0;
result.push(fn);
} else {
if (fn.signatureMatch() !== fn.signature()) {
fn.signatureMatch(fn.signature());
}
if (fn.descriptionMatch() !== fn.description()) {
fn.descriptionMatch(fn.description());
}
}
});
});
result.sort((a, b) => {
if (a.weight !== b.weight) {
return b.weight - a.weight;
}
return a.signature().localeCompare(b.signature());
});
return result;
});
}
async init() {
if (this.initialized) {
return;
}
await this.getUdfs();
this.initialized = true;
}
async refresh() {
this.loading(true);
huePubSub.publish(CLEAR_UDF_CACHE_EVENT, {
connector: this.connector,
callback: this.getUdfs.bind(this)
});
}
async getUdfs() {
this.loading(true);
const categories = [];
const functions = await getUdfCategories(sqlReferenceRepository, this.connector);
functions.forEach(category => {
const koCategory = {
name: category.name,
open: ko.observable(false),
functions: Object.keys(category.functions).map(key => {
const fn = category.functions[key];
return {
name: fn.name,
draggable: fn.draggable,
signature: ko.observable(fn.signature),
signatureMatch: ko.observable(fn.signature),
description: ko.observable(fn.description),
descriptionMatch: ko.observable(fn.description),
loaded: ko.observable(fn.described)
};
})
};
koCategory.functions.forEach(fn => {
fn.category = koCategory;
});
categories.push(koCategory);
});
this.categories(categories);
this.loading(false);
}
}
class AssistFunctionsPanel {
constructor(params) {
this.categories = {};
this.activeConnector = params.activeConnector;
this.availableConnectorUdfs = ko.observableArray();
this.activeConnectorUdfs = ko.observable();
this.activeConnectorUdfs.subscribe(async activeConnectorUdfs => {
await activeConnectorUdfs.init();
setInLocalStorage(
'assist.function.panel.active.connector.type',
activeConnectorUdfs.connector.type
);
});
const setCategoriesFromConnector = connector => {
this.availableConnectorUdfs().some(availableConnectorUdfs => {
if (availableConnectorUdfs.connector.type === connector.type) {
this.activeConnectorUdfs(availableConnectorUdfs);
return true;
}
});
};
this.activeConnector.subscribe(connector => {
if (connector) {
setCategoriesFromConnector(connector);
}
});
const configUpdated = () => {
const lastActiveConnectorType =
(this.activeConnector() && this.activeConnector().type) ||
getFromLocalStorage('assist.function.panel.active.connector.type');
const availableConnectorUdfs = filterEditorConnectors(
connector => connector.dialect && sqlReferenceRepository.hasUdfCategories(connector.dialect)
).map(connector => new ConnectorUdfCategories(connector));
availableConnectorUdfs.sort((a, b) =>
a.connector.displayName.localeCompare(b.connector.displayName)
);
this.availableConnectorUdfs(availableConnectorUdfs);
let activeConnectorUdfs;
if (lastActiveConnectorType) {
activeConnectorUdfs = this.availableConnectorUdfs().find(
connectorUdf => connectorUdf.connector.type === lastActiveConnectorType
);
}
if (!activeConnectorUdfs && this.availableConnectorUdfs().length) {
activeConnectorUdfs = this.availableConnectorUdfs()[0];
}
this.activeConnectorUdfs(activeConnectorUdfs);
setCategoriesFromConnector(this.activeConnector());
};
configUpdated();
huePubSub.subscribe(CONFIG_REFRESHED_TOPIC, configUpdated);
}
}
componentUtils.registerStaticComponent(NAME, AssistFunctionsPanel, TEMPLATE);
export default AssistFunctionsPanel;