php - 如何在 Google 数据洞察中嵌入报告过滤器?
问题描述
这是我试图实现的一个场景:我在 Google Data Studio 中创建了一个报告并嵌入到我的网站中。我激活了“任何有链接的人都可以查看”选项,以便我的网站用户可以看到此报告。
但我需要根据他们的用户 ID 向我的网站用户显示不同的数据。基本上根据用户ID显示过滤数据。但我在这里面临的挑战是,这些用户没有谷歌帐户。因此,使用社区连接器并在配置函数中传递电子邮件 ID 对我不起作用。
我尝试了书签选项,您可以在其中嵌入链接并为不同的用户更改参数。它将在嵌入代码中正确显示。但是一旦用户在 Google Data Studio 中点击或打开,这个过滤器就会关闭。
有没有人针对这种情况实施了解决方法?直接从嵌入图形的 Web 应用程序中显示不同的数据。
根据用户 ID 显示不同的数据对于我们的用例来说非常重要,没有它,数据工作室的所有功能都无法使用。
解决方案
在这种情况下,我们需要创建一个自定义数据源提供程序。
第 1 步:我们需要将自定义数据源写入https://script.google.com/。首先,编辑 Application.json。在这里,我们必须更新清单 JSON 和 Code.gs 脚本。
有关更多信息https://github.com/googledatastudio/community-connectors/tree/master/JSON-connect/src
应用脚本.json
{
"dataStudio": {
"name": "JSON Connect",
"logoUrl": "https://raw.githubusercontent.com/googledatastudio/community-connectors/master/JSON-connect/assets/logo.png",
"company": "Lingewoud",
"companyUrl": "https://lingewoud.nl/",
"addonUrl": "https://github.com/googledatastudio/community-connectors/blob/master/JSON-connect/README.md",
"supportUrl": "https://github.com/googledatastudio/community-connectors/issues/new?title=JSON%20Connect%20Connector%20Issue",
"shortDescription": "Connect to JSON data by URL",
"description": "Free JSON connector. Fetch data from any JSON data source by URL. Use caching to speed up rendering.",
"sources": ["CUSTOM_JSON"],
"authType": ["NONE"],
"feeType": ["FREE"]
}
}
main.js
// Copyright notice
//
// (c) 2019 Gabriël Ramaker <gabriel@lingewoud.nl>, Lingewoud
//
// All rights reserved
//
//
// Licensed 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.
//
// This copyright notice MUST APPEAR in all copies of the script!
/* eslint-disable prefer-rest-params */
/* eslint-disable prefer-spread */
/**
* Throws and logs script exceptions.
*
* @param {String} message The exception message
*/
function sendUserError(message) {
var cc = DataStudioApp.createCommunityConnector();
cc.newUserError()
.setText(message)
.throwException();
}
/**
* function `getAuthType()`
*
* @returns {Object} `AuthType` used by the connector.
*/
function getAuthType() {
return {type: 'NONE'};
}
/**
* function `isAdminUser()`
*
* @returns {Boolean} Currently just returns false. Should return true if the current authenticated user at the time
* of function execution is an admin user of the connector.
*/
function isAdminUser() {
return false;
}
/**
* Returns the user configurable options for the connector.
*
* Required function for Community Connector.
*
* @param {Object} request Config request parameters.
* @returns {Object} Connector configuration to be displayed to the user.
*/
function getConfig(request) {
var cc = DataStudioApp.createCommunityConnector();
var config = cc.getConfig();
var option1 = config
.newOptionBuilder()
.setLabel('Text')
.setValue('text');
var option2 = config
.newOptionBuilder()
.setLabel('Inline')
.setValue('inline');
config
.newInfo()
.setId('instructions')
.setText('Fill out the form to connect to a JSON data source.');
config
.newTextInput()
.setId('url')
.setName('Enter the URL of a JSON data source')
.setHelpText('e.g. https://my-url.org/json')
.setPlaceholder('https://my-url.org/json');
config
.newCheckbox()
.setId('cache')
.setName('Cache response')
.setHelpText('Usefull with big datasets. Response is cached for 10 minutes')
.setAllowOverride(true);
config
.newSelectSingle()
.setId('nestedData')
.setName('Nested data')
.setHelpText('How to import nested data, as text or inline.')
.setAllowOverride(true)
.addOption(option1)
.addOption(option2);
config.setDateRangeRequired(false);
return config.build();
}
/**
* Gets UrlFetch response and parses JSON
*
* @param {string} url The URL to get the data from
* @returns {Object} The response object
*/
function fetchJSON(url) {
try {
var response = UrlFetchApp.fetch(url);
} catch (e) {
sendUserError('"' + url + '" returned an error:' + e);
}
try {
var content = JSON.parse(response);
} catch (e) {
sendUserError('Invalid JSON format. ' + e);
}
return content;
}
/**
* Gets cached response. If the response has not been cached, make
* the fetchJSON call, then cache and return the response.
*
* @param {string} url The URL to get the data from
* @returns {Object} The response object
*/
function getCachedData(url) {
var cacheExpTime = 600;
var cache = CacheService.getUserCache();
var cacheKey = url.replace(/[^a-zA-Z0-9]+/g, '');
var cacheKeyString = cache.get(cacheKey + '.keys');
var cacheKeys = cacheKeyString !== null ? cacheKeyString.split(',') : [];
var cacheData = {};
var content = [];
if (cacheKeyString !== null && cacheKeys.length > 0) {
cacheData = cache.getAll(cacheKeys);
for (var key in cacheKeys) {
if (cacheData[cacheKeys[key]] != undefined) {
content.push(JSON.parse(cacheData[cacheKeys[key]]));
}
}
} else {
content = fetchJSON(url);
for (var key in content) {
cacheData[cacheKey + '.' + key] = JSON.stringify(content[key]);
}
cache.putAll(cacheData);
cache.put(cacheKey + '.keys', Object.keys(cacheData), cacheExpTime);
}
return content;
}
/**
* Fetches data. Either by calling getCachedData or fetchJSON, depending on the cache configuration parameter.
*
* @param {String} url The URL to get the data from
* @param {Boolean} cache Parameter to determine whether the request should be cached
* @returns {Object} The response object
*/
function fetchData(url, cache) {
if (!url || !url.match(/^https?:\/\/.+$/g)) {
sendUserError('"' + url + '" is not a valid url.');
}
try {
var content = cache ? getCachedData(url) : fetchJSON(url);
} catch (e) {
sendUserError(
'Your request could not be cached. The rows of your dataset probably exceed the 100KB cache limit.'
);
}
if (!content) sendUserError('"' + url + '" returned no content.');
return content;
}
/**
* Matches the field value to a semantic
*
* @param {Mixed} value The field value
* @param {Object} types The list of types
* @return {string} The semantic type
*/
function getSemanticType(value, types) {
if (!isNaN(parseFloat(value)) && isFinite(value)) {
return types.NUMBER;
} else if (value === true || value === false) {
return types.BOOLEAN;
} else if (typeof value != 'object' && value != null) {
if (
value.match(
new RegExp(
/[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/gi
)
)
) {
return types.URL;
} else if (!isNaN(Date.parse(value))) {
return types.YEAR_MONTH_DAY_HOUR;
}
}
return types.TEXT;
}
/**
* Creates the fields
*
* @param {Object} fields The list of fields
* @param {Object} types The list of types
* @param {String} key The key value of the current element
* @param {Mixed} value The value of the current element
*/
function createField(fields, types, key, value) {
var semanticType = getSemanticType(value, types);
var field =
semanticType == types.NUMBER ? fields.newMetric() : fields.newDimension();
field.setType(semanticType);
field.setId(key.replace(/\s/g, '_').toLowerCase());
field.setName(key);
}
/**
* Handles keys for recursive fields
*
* @param {String} currentKey The key value of the current element
* @param {Mixed} key The key value of the parent element
* @returns {String} if true
*/
function getElementKey(key, currentKey) {
if (currentKey == '' || currentKey == null) {
return;
}
if (key != null) {
return key + '.' + currentKey.replace('.', '_');
}
return currentKey.replace('.', '_');
}
/**
* Extracts the objects recursive fields and adds it to fields
*
* @param {Object} fields The list of fields
* @param {Object} types The list of types
* @param {String} key The key value of the current element
* @param {Mixed} value The value of the current element
* @param {boolean} isInline if true
*/
function createFields(fields, types, key, value, isInline) {
if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
Object.keys(value).forEach(function(currentKey) {
var elementKey = getElementKey(key, currentKey);
if (isInline && value[currentKey] != null) {
createFields(fields, types, elementKey, value[currentKey], isInline);
} else {
createField(fields, types, currentKey, value);
}
});
} else if (key !== null) {
createField(fields, types, key, value);
}
}
/**
* Parses first line of content to determine the data schema
*
* @param {Object} request getSchema/getData request parameter.
* @param {Object} content The content object
* @return {Object} An object with the connector configuration
*/
function getFields(request, content) {
var cc = DataStudioApp.createCommunityConnector();
var fields = cc.getFields();
var types = cc.FieldType;
var aggregations = cc.AggregationType;
var isInline = request.configParams.nestedData === 'inline';
if (!Array.isArray(content)) content = [content];
if (typeof content[0] !== 'object' || content[0] === null) {
sendUserError('Invalid JSON format');
}
try {
createFields(fields, types, null, content[0], isInline);
} catch (e) {
sendUserError('Unable to identify the data format of one of your fields.');
}
return fields;
}
/**
* Returns the schema for the given request.
*
* @param {Object} request Schema request parameters.
* @returns {Object} Schema for the given request.
*/
function getSchema(request) {
var content = fetchData(request.configParams.url, request.configParams.cache);
var fields = getFields(request, content).build();
return {schema: fields};
}
/**
* Performs a deep merge of objects and returns new object. Does not modify
* objects (immutable) and merges arrays via concatenation.
* Thanks to jhildenbiddle https://stackoverflow.com/users/4903063/jhildenbiddle
* https://stackoverflow.com/questions/27936772/how-to-deep-merge-instead-of-shallow-merge
*
* @param Objects to merge
* @returns {object} New object with merged key/values
*/
function mergeDeep() {
var objects = Array.prototype.slice.call(arguments);
return objects.reduce(function(prev, obj) {
Object.keys(obj).forEach(function(key) {
var pVal = prev[key];
var oVal = obj[key];
if (Array.isArray(pVal) && Array.isArray(oVal)) {
prev[key] = pVal.concat.apply(pVal, toConsumableArray(oVal));
} else if (pVal === Object(pVal) && oVal === Object(oVal)) {
prev[key] = mergeDeep(pVal, oVal);
} else {
prev[key] = oVal;
}
});
return prev;
}, {});
}
/**
* Converts date strings to YYYYMMDDHH:mm:ss
*
* @param {String} val Date string
* @returns {String} Converted date string
*/
function convertDate(val) {
var date = new Date(val);
return (
date.getUTCFullYear() +
('0' + (date.getUTCMonth() + 1)).slice(-2) +
('0' + date.getUTCDate()).slice(-2) +
('0' + date.getUTCHours()).slice(-2)
);
}
/**
* Validates the row values. Only numbers, boolean, date and strings are allowed
*
* @param {Field} field The field declaration
* @param {Mixed} val The value to validate
* @returns {Mixed} Either a string or number
*/
function validateValue(field, val) {
if (field.getType() == 'YEAR_MONTH_DAY_HOUR') {
val = convertDate(val);
}
switch (typeof val) {
case 'string':
case 'number':
case 'boolean':
return val;
case 'object':
return JSON.stringify(val);
}
return '';
}
/**
* Returns the (nested) values for requested columns
*
* @param {Object} valuePaths Field name. If nested; field name and parent field name
* @param {Object} row Current content row
* @returns {Mixed} The field values for the columns
*/
function getColumnValue(valuePaths, row) {
for (var index in valuePaths) {
var currentPath = valuePaths[index];
if (row[currentPath] === null) {
return '';
}
if (row[currentPath] !== undefined) {
row = row[currentPath];
continue;
}
var keys = Object.keys(row);
for (var index_keys in keys) {
var key = keys[index_keys].replace(/\s/g, '_').toLowerCase();
if (key == currentPath) {
row = row[keys[index_keys]];
break;
}
}
}
return row;
}
/**
* Returns an object containing only the requested columns
*
* @param {Object} content The content object
* @param {Object} requestedFields Fields requested in the getData request.
* @returns {Object} An object only containing the requested columns.
*/
function getColumns(content, requestedFields) {
if (!Array.isArray(content)) content = [content];
return content.map(function(row) {
var rowValues = [];
requestedFields.asArray().forEach(function(field) {
var valuePaths = field.getId().split('.');
var fieldValue = row === null ? '' : getColumnValue(valuePaths, row);
rowValues.push(validateValue(field, fieldValue));
});
return {values: rowValues};
});
}
/**
* Returns the tabular data for the given request.
*
* @param {Object} request Data request parameters.
* @returns {Object} Contains the schema and data for the given request.
*/
function getData(request) {
var content = fetchData(request.configParams.url, request.configParams.cache);
var fields = getFields(request, content);
var requestedFieldIds = request.fields.map(function(field) {
return field.name;
});
var requestedFields = fields.forIds(requestedFieldIds);
return {
schema: requestedFields.build(),
rows: getColumns(content, requestedFields)
};
}
第 2 步:部署您的代码。部署后,您将获得“部署 ID”和数据源的直接 URL。
第 3 步:需要提供您的源 JSON URL 并允许参数被 URL 覆盖。根据部门明智地允许我们对数据的需求的参数并连接到数据源。
https://drive.google.com/file/d/1ys4RCPyWYC8wP31jWtCJcZpqt6SIKnMC/view?usp=sharing
因此通过使用自定义连接器,我们可以根据角色显示数据。
推荐阅读
- javascript - 使用 javaScript 在 chrome explorer 等文本中查找单词
- c++ - Cygwin libzint(zint) 进入无限循环
- html - 如何使用网格使图像适合浏览器窗口?
- javascript - 无法使用 React JS 编辑输入文本
- javascript - 如何遍历模板文字中的对象数组并为每个对象使用函数?
- postgresql - Postgresql函数执行过程
- ios - 如何以编程方式快速保存pdf
- php - 简单的 xero api oauth2 请求
- java - 生成 PKCS7 编码的数字签名 - Hwcrypto、itextPdf、USB Token
- java - 如何在 Java 中解析和求解方程字符串?