由于格式错误的响应,画面扩展服务器无法响应
tableau extension server could not respond due to malformed response
我正在尝试为 Tableau 服务器开发我的第一个 Tableau 扩展。我正在本地开发它。
它几乎是一种 Hello World 扩展。
当我尝试添加扩展时,它抛出一个错误“请求被服务器拒绝”
这是一个 node.js 应用程序并且 运行 非常好。
这是我的 server.js
const express = require('express');
const app = express();
const process = require('process');
app.get('/', async function(req, res){
res.sendFile(__dirname + "/public/datasources.html");
});
const listener = app.listen(3030, () =>{
console.log("App is listening on port " + listener.address().port);
这是我的 datasources.html..这是作为 tableau 示例提供的其中一个。我只是在最后添加了它的 js 部分。全部在一个文件中
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Datasources Sample</title>
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<!-- Bootstrap -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" >
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" ></script>
<!-- Extensions Library (this will be hosted on a CDN eventually) -->
<script src="https://extensions.tableauusercontent.com/resources/tableau.extensions.1.latest.min.js"></script>
<!-- Our extension's code -->
</head>
<body>
<div class="container">
<!-- DataSources Table -->
<div id="dataSources">
<h4>All DataSources</h4>
<div class="table-responsive">
<table id="loading" class="table">
<tbody><tr><td>Loading...</td></tr></tbody>
</table>
<table id="dataSourcesTable" class="table table-striped hidden">
<thead>
<tr>
<th>DataSource Name</th>
<th>Auto Refresh</th>
<th style="width: 100%">Info</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
<!-- More dataSource info modal -->
<div class="modal fade" id="infoModal" role="dialog">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">×</button>
<h4 class="modal-title">DataSource Details</h4>
</div>
<div id="dataSourceDetails" class="modal-body">
<div class="table-responsive">
<table id="detailsTable" class="table">
<tbody>
<tr>
<td>DataSource Name</td>
<td id="nameDetail"></td>
</tr>
<tr>
<td>DataSource Id</td>
<td id="idDetail"></td>
</tr>
<tr>
<td>Type</td>
<td id="typeDetail"></td>
</tr>
<tr>
<td>Fields</td>
<td id="fieldsDetail"></td>
</tr>
<tr>
<td>Connections</td>
<td id="connectionsDetail"></td>
</tr>
<tr>
<td>Active Tables</td>
<td id="activeTablesDetail"></td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Wrap everything in an anonymous function to avoid polluting the global namespace
(function () {
$(document).ready(function () {
tableau.extensions.initializeAsync().then(function () {
// Since dataSource info is attached to the worksheet, we will perform
// one async call per worksheet to get every dataSource used in this
// dashboard. This demonstrates the use of Promise.all to combine
// promises together and wait for each of them to resolve.
let dataSourceFetchPromises = [];
// Maps dataSource id to dataSource so we can keep track of unique dataSources.
let dashboardDataSources = {};
// To get dataSource info, first get the dashboard.
const dashboard = tableau.extensions.dashboardContent.dashboard;
// Then loop through each worksheet and get its dataSources, save promise for later.
dashboard.worksheets.forEach(function (worksheet) {
dataSourceFetchPromises.push(worksheet.getDataSourcesAsync());
});
Promise.all(dataSourceFetchPromises).then(function (fetchResults) {
fetchResults.forEach(function (dataSourcesForWorksheet) {
dataSourcesForWorksheet.forEach(function (dataSource) {
if (!dashboardDataSources[dataSource.id]) { // We've already seen it, skip it.
dashboardDataSources[dataSource.id] = dataSource;
}
});
});
buildDataSourcesTable(dashboardDataSources);
// This just modifies the UI by removing the loading banner and showing the dataSources table.
$('#loading').addClass('hidden');
$('#dataSourcesTable').removeClass('hidden').addClass('show');
});
}, function (err) {
// Something went wrong in initialization.
console.log('Error while Initializing: ' + err.toString());
});
});
// Refreshes the given dataSource.
function refreshDataSource (dataSource) {
dataSource.refreshAsync().then(function () {
console.log(dataSource.name + ': Refreshed Successfully');
});
}
// Displays a modal dialog with more details about the given dataSource.
function showModal (dataSource) {
let modal = $('#infoModal');
$('#nameDetail').text(dataSource.name);
$('#idDetail').text(dataSource.id);
$('#typeDetail').text((dataSource.isExtract) ? 'Extract' : 'Live');
// Loop through every field in the dataSource and concat it to a string.
let fieldNamesStr = '';
dataSource.fields.forEach(function (field) {
fieldNamesStr += field.name + ', ';
});
// Slice off the last ", " for formatting.
$('#fieldsDetail').text(fieldNamesStr.slice(0, -2));
dataSource.getConnectionSummariesAsync().then(function (connectionSummaries) {
// Loop through each connection summary and list the connection's
// name and type in the info field
let connectionsStr = '';
connectionSummaries.forEach(function (summary) {
connectionsStr += summary.name + ': ' + summary.type + ', ';
});
// Slice of the last ", " for formatting.
$('#connectionsDetail').text(connectionsStr.slice(0, -2));
});
dataSource.getActiveTablesAsync().then(function (activeTables) {
// Loop through each table that was used in creating this datasource
let tableStr = '';
activeTables.forEach(function (table) {
tableStr += table.name + ', ';
});
// Slice of the last ", " for formatting.
$('#activeTablesDetail').text(tableStr.slice(0, -2));
});
modal.modal('show');
}
// Constructs UI that displays all the dataSources in this dashboard
// given a mapping from dataSourceId to dataSource objects.
function buildDataSourcesTable (dataSources) {
// Clear the table first.
$('#dataSourcesTable > tbody tr').remove();
const dataSourcesTable = $('#dataSourcesTable > tbody')[0];
// Add an entry to the dataSources table for each dataSource.
for (let dataSourceId in dataSources) {
const dataSource = dataSources[dataSourceId];
let newRow = dataSourcesTable.insertRow(dataSourcesTable.rows.length);
let nameCell = newRow.insertCell(0);
let refreshCell = newRow.insertCell(1);
let infoCell = newRow.insertCell(2);
let refreshButton = document.createElement('button');
refreshButton.innerHTML = ('Refresh Now');
refreshButton.type = 'button';
refreshButton.className = 'btn btn-primary';
refreshButton.addEventListener('click', function () { refreshDataSource(dataSource); });
let infoSpan = document.createElement('span');
infoSpan.className = 'glyphicon glyphicon-info-sign';
infoSpan.addEventListener('click', function () { showModal(dataSource); });
nameCell.innerHTML = dataSource.name;
refreshCell.appendChild(refreshButton);
infoCell.appendChild(infoSpan);
}
}
})();
</script>
</body>
</html>
这是我的 trex 扩展文件。
<?xml version="1.0" encoding="utf-8"?>
<manifest manifest-version="0.1" xmlns="http://www.tableau.com/xml/extension_manifest">
<dashboard-extension id="com.tableau.extensions.samples.datasources" extension-version="0.6.0">
<default-locale>en_US</default-locale>
<name resource-id="name"/>
<description>DataSources Sample</description>
<author name="tableau" email="github@tableau.com" organization="tableau" website="https://www.tableau.com"/>
<min-api-version>0.8</min-api-version>
<source-location>
<url>http://MACHINE_NAME:3030/</url>
</source-location>
<icon>iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAlhJREFUOI2Nkt9vy1EYh5/3bbsvRSySCZbIxI+ZCKsN2TKtSFyIrV2WuRCJuBiJWxfuxCVXbvwFgiEtposgLFJElnbU1SxIZIIRJDKTrdu+53Uhra4mce7Oe57Pcz7JOULFisViwZ+29LAzOSjQYDgz1ZcCvWuXV11MJpN+OS/lm6179teqH0yDqxPTCyKSA8DcDsyOmOprnCaeP7459pdgy969i0LTC3IO/RQMyoHcQN+3cnljW3dNIFC47qDaK3g7BwdTkwBaBELT4ZPOUVWgKl4ZBnjxJPUlMDnTDrp0pmr6RHFeEjjcUUXPDGeSEwDN0Xg8sivxMhJNjGzbHd8PkM3eHRfkrBM5NkcQaY2vUnTlrDIA0NoaX+KLXFFlowr14tvVpqb2MICzmQcKqxvbumv+NAhZGCCIPwEw6QWXKYRL/VUXO0+rAUJiPwAk5MIlgVfwPjjHLCL1APmHN94ZdqeYN+NW/mn6I4BvwQYchcLnwFhJMDiYmlRxAzjpKWZkYkUCcZ2I61wi37tLbYyjiN0fHk5Oz3nGSLSzBbNHCF35R7f6K1/hN9PRhek11FrymfQQQKB4+Gl05P2qNRtmETlXW7e+b2z01dfycGNbfFMAbqNyKp9Jp4rzOT8RYFs0njJkc2iqsCObvTsOsDWWqA5C1uFy+Uz/oXJeKwVT4h0RmPUXhi79vuC0Ku6yOffTK3g9lfxfDQAisY516sg5kfOCiJk7HoLt2cf9b/9LANAc7dznm98PagG1fUOZ9IP5uMB8Q4CPoyNvausapkTt3rNMuvdf3C/o6+czhtdwmwAAAABJRU5ErkJggg==</icon>
<permissions>
<permission>full data</permission>
</permissions>
</dashboard-extension>
<resources>
<resource id="name">
<text locale="en_US">DataSources Sample</text>
</resource>
</resources>
</manifest>
我是否必须在 Tableau 服务器端执行某些操作才能使其正常工作?
这可能是因为您使用的是没有本地主机的 http://。那是不允许的。我不确定为什么错误消息如此无用,但可能是从服务器返回的错误没有为客户端错误框正确格式化。没有日志很难知道。 :)
尝试从 http://localhost 切换到托管您的扩展程序,或者使用 SAN 创建一个自签名证书,将其添加到您的信任存储区并改为从 https:// 托管。
我正在尝试为 Tableau 服务器开发我的第一个 Tableau 扩展。我正在本地开发它。 它几乎是一种 Hello World 扩展。 当我尝试添加扩展时,它抛出一个错误“请求被服务器拒绝”
这是一个 node.js 应用程序并且 运行 非常好。
这是我的 server.js
const express = require('express');
const app = express();
const process = require('process');
app.get('/', async function(req, res){
res.sendFile(__dirname + "/public/datasources.html");
});
const listener = app.listen(3030, () =>{
console.log("App is listening on port " + listener.address().port);
这是我的 datasources.html..这是作为 tableau 示例提供的其中一个。我只是在最后添加了它的 js 部分。全部在一个文件中
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Datasources Sample</title>
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<!-- Bootstrap -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" >
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" ></script>
<!-- Extensions Library (this will be hosted on a CDN eventually) -->
<script src="https://extensions.tableauusercontent.com/resources/tableau.extensions.1.latest.min.js"></script>
<!-- Our extension's code -->
</head>
<body>
<div class="container">
<!-- DataSources Table -->
<div id="dataSources">
<h4>All DataSources</h4>
<div class="table-responsive">
<table id="loading" class="table">
<tbody><tr><td>Loading...</td></tr></tbody>
</table>
<table id="dataSourcesTable" class="table table-striped hidden">
<thead>
<tr>
<th>DataSource Name</th>
<th>Auto Refresh</th>
<th style="width: 100%">Info</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
<!-- More dataSource info modal -->
<div class="modal fade" id="infoModal" role="dialog">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">×</button>
<h4 class="modal-title">DataSource Details</h4>
</div>
<div id="dataSourceDetails" class="modal-body">
<div class="table-responsive">
<table id="detailsTable" class="table">
<tbody>
<tr>
<td>DataSource Name</td>
<td id="nameDetail"></td>
</tr>
<tr>
<td>DataSource Id</td>
<td id="idDetail"></td>
</tr>
<tr>
<td>Type</td>
<td id="typeDetail"></td>
</tr>
<tr>
<td>Fields</td>
<td id="fieldsDetail"></td>
</tr>
<tr>
<td>Connections</td>
<td id="connectionsDetail"></td>
</tr>
<tr>
<td>Active Tables</td>
<td id="activeTablesDetail"></td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Wrap everything in an anonymous function to avoid polluting the global namespace
(function () {
$(document).ready(function () {
tableau.extensions.initializeAsync().then(function () {
// Since dataSource info is attached to the worksheet, we will perform
// one async call per worksheet to get every dataSource used in this
// dashboard. This demonstrates the use of Promise.all to combine
// promises together and wait for each of them to resolve.
let dataSourceFetchPromises = [];
// Maps dataSource id to dataSource so we can keep track of unique dataSources.
let dashboardDataSources = {};
// To get dataSource info, first get the dashboard.
const dashboard = tableau.extensions.dashboardContent.dashboard;
// Then loop through each worksheet and get its dataSources, save promise for later.
dashboard.worksheets.forEach(function (worksheet) {
dataSourceFetchPromises.push(worksheet.getDataSourcesAsync());
});
Promise.all(dataSourceFetchPromises).then(function (fetchResults) {
fetchResults.forEach(function (dataSourcesForWorksheet) {
dataSourcesForWorksheet.forEach(function (dataSource) {
if (!dashboardDataSources[dataSource.id]) { // We've already seen it, skip it.
dashboardDataSources[dataSource.id] = dataSource;
}
});
});
buildDataSourcesTable(dashboardDataSources);
// This just modifies the UI by removing the loading banner and showing the dataSources table.
$('#loading').addClass('hidden');
$('#dataSourcesTable').removeClass('hidden').addClass('show');
});
}, function (err) {
// Something went wrong in initialization.
console.log('Error while Initializing: ' + err.toString());
});
});
// Refreshes the given dataSource.
function refreshDataSource (dataSource) {
dataSource.refreshAsync().then(function () {
console.log(dataSource.name + ': Refreshed Successfully');
});
}
// Displays a modal dialog with more details about the given dataSource.
function showModal (dataSource) {
let modal = $('#infoModal');
$('#nameDetail').text(dataSource.name);
$('#idDetail').text(dataSource.id);
$('#typeDetail').text((dataSource.isExtract) ? 'Extract' : 'Live');
// Loop through every field in the dataSource and concat it to a string.
let fieldNamesStr = '';
dataSource.fields.forEach(function (field) {
fieldNamesStr += field.name + ', ';
});
// Slice off the last ", " for formatting.
$('#fieldsDetail').text(fieldNamesStr.slice(0, -2));
dataSource.getConnectionSummariesAsync().then(function (connectionSummaries) {
// Loop through each connection summary and list the connection's
// name and type in the info field
let connectionsStr = '';
connectionSummaries.forEach(function (summary) {
connectionsStr += summary.name + ': ' + summary.type + ', ';
});
// Slice of the last ", " for formatting.
$('#connectionsDetail').text(connectionsStr.slice(0, -2));
});
dataSource.getActiveTablesAsync().then(function (activeTables) {
// Loop through each table that was used in creating this datasource
let tableStr = '';
activeTables.forEach(function (table) {
tableStr += table.name + ', ';
});
// Slice of the last ", " for formatting.
$('#activeTablesDetail').text(tableStr.slice(0, -2));
});
modal.modal('show');
}
// Constructs UI that displays all the dataSources in this dashboard
// given a mapping from dataSourceId to dataSource objects.
function buildDataSourcesTable (dataSources) {
// Clear the table first.
$('#dataSourcesTable > tbody tr').remove();
const dataSourcesTable = $('#dataSourcesTable > tbody')[0];
// Add an entry to the dataSources table for each dataSource.
for (let dataSourceId in dataSources) {
const dataSource = dataSources[dataSourceId];
let newRow = dataSourcesTable.insertRow(dataSourcesTable.rows.length);
let nameCell = newRow.insertCell(0);
let refreshCell = newRow.insertCell(1);
let infoCell = newRow.insertCell(2);
let refreshButton = document.createElement('button');
refreshButton.innerHTML = ('Refresh Now');
refreshButton.type = 'button';
refreshButton.className = 'btn btn-primary';
refreshButton.addEventListener('click', function () { refreshDataSource(dataSource); });
let infoSpan = document.createElement('span');
infoSpan.className = 'glyphicon glyphicon-info-sign';
infoSpan.addEventListener('click', function () { showModal(dataSource); });
nameCell.innerHTML = dataSource.name;
refreshCell.appendChild(refreshButton);
infoCell.appendChild(infoSpan);
}
}
})();
</script>
</body>
</html>
这是我的 trex 扩展文件。
<?xml version="1.0" encoding="utf-8"?>
<manifest manifest-version="0.1" xmlns="http://www.tableau.com/xml/extension_manifest">
<dashboard-extension id="com.tableau.extensions.samples.datasources" extension-version="0.6.0">
<default-locale>en_US</default-locale>
<name resource-id="name"/>
<description>DataSources Sample</description>
<author name="tableau" email="github@tableau.com" organization="tableau" website="https://www.tableau.com"/>
<min-api-version>0.8</min-api-version>
<source-location>
<url>http://MACHINE_NAME:3030/</url>
</source-location>
<icon>iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAlhJREFUOI2Nkt9vy1EYh5/3bbsvRSySCZbIxI+ZCKsN2TKtSFyIrV2WuRCJuBiJWxfuxCVXbvwFgiEtposgLFJElnbU1SxIZIIRJDKTrdu+53Uhra4mce7Oe57Pcz7JOULFisViwZ+29LAzOSjQYDgz1ZcCvWuXV11MJpN+OS/lm6179teqH0yDqxPTCyKSA8DcDsyOmOprnCaeP7459pdgy969i0LTC3IO/RQMyoHcQN+3cnljW3dNIFC47qDaK3g7BwdTkwBaBELT4ZPOUVWgKl4ZBnjxJPUlMDnTDrp0pmr6RHFeEjjcUUXPDGeSEwDN0Xg8sivxMhJNjGzbHd8PkM3eHRfkrBM5NkcQaY2vUnTlrDIA0NoaX+KLXFFlowr14tvVpqb2MICzmQcKqxvbumv+NAhZGCCIPwEw6QWXKYRL/VUXO0+rAUJiPwAk5MIlgVfwPjjHLCL1APmHN94ZdqeYN+NW/mn6I4BvwQYchcLnwFhJMDiYmlRxAzjpKWZkYkUCcZ2I61wi37tLbYyjiN0fHk5Oz3nGSLSzBbNHCF35R7f6K1/hN9PRhek11FrymfQQQKB4+Gl05P2qNRtmETlXW7e+b2z01dfycGNbfFMAbqNyKp9Jp4rzOT8RYFs0njJkc2iqsCObvTsOsDWWqA5C1uFy+Uz/oXJeKwVT4h0RmPUXhi79vuC0Ku6yOffTK3g9lfxfDQAisY516sg5kfOCiJk7HoLt2cf9b/9LANAc7dznm98PagG1fUOZ9IP5uMB8Q4CPoyNvausapkTt3rNMuvdf3C/o6+czhtdwmwAAAABJRU5ErkJggg==</icon>
<permissions>
<permission>full data</permission>
</permissions>
</dashboard-extension>
<resources>
<resource id="name">
<text locale="en_US">DataSources Sample</text>
</resource>
</resources>
</manifest>
我是否必须在 Tableau 服务器端执行某些操作才能使其正常工作?
这可能是因为您使用的是没有本地主机的 http://。那是不允许的。我不确定为什么错误消息如此无用,但可能是从服务器返回的错误没有为客户端错误框正确格式化。没有日志很难知道。 :)
尝试从 http://localhost 切换到托管您的扩展程序,或者使用 SAN 创建一个自签名证书,将其添加到您的信任存储区并改为从 https:// 托管。