@google/dscc
Advanced tools
Comparing version 0.0.9 to 0.1.0
@@ -16,88 +16,180 @@ /*! | ||
*/ | ||
import {iframeLoaded, subscribeToData} from '../src/impl'; | ||
import * as DS from '../src/data-studio-types'; | ||
import * as sut from '../src/index'; | ||
import * as DS from '../src/types'; | ||
const DEPLOYMENT_ID = 'my deployment id'; | ||
const testDimensionFields = (numRequested: number): DS.Field[] => { | ||
const fields = [ | ||
{ | ||
id: 'dimensionField1Id', | ||
name: 'dimensionField1Name', | ||
description: 'dimensionField1Description', | ||
type: DS.FieldType.TEXT, | ||
}, | ||
{ | ||
id: 'dimensionField2Id', | ||
name: 'dimensionField2Name', | ||
description: 'dimensionField2Description', | ||
type: DS.FieldType.BOOLEAN, | ||
}, | ||
]; | ||
if (numRequested > fields.length) { | ||
throw new Error(`Can't support ${numRequested} fields yet.`); | ||
} | ||
return fields.splice(0, numRequested); | ||
}; | ||
const testMessageRows = { | ||
primaryRows: [['(not set)', 196.5149696290909], ['feb', 0]], | ||
comparisonRows: [['(not set)', 198.2720811099253], ['feb', null]], | ||
const testMetricFields = (numRequested: number): DS.Field[] => { | ||
const fields = [ | ||
{ | ||
id: 'metricField1Id', | ||
name: 'metricField1Name', | ||
description: 'metricField1Description', | ||
type: DS.FieldType.NUMBER, | ||
}, | ||
{ | ||
id: 'metricField2Id', | ||
name: 'metricField2Name', | ||
description: 'metricField2Description', | ||
type: DS.FieldType.PERCENT, | ||
}, | ||
]; | ||
if (numRequested > fields.length) { | ||
throw new Error(`Can't support ${numRequested} fields yet.`); | ||
} | ||
return fields.splice(0, numRequested); | ||
}; | ||
const testPropertyConfig: DS.ConfigData[] = [ | ||
{ | ||
id: 'hi', | ||
heading: 'his', | ||
elements: [ | ||
const testMessage = (numDimensions: number, numMetrics: number): DS.Message => { | ||
const dimensionFields = testDimensionFields(numDimensions); | ||
const metricFields = testMetricFields(numMetrics); | ||
const fields = dimensionFields.concat(metricFields); | ||
return { | ||
type: DS.MessageType.RENDER, | ||
config: { | ||
data: { | ||
id: 'configId', | ||
label: 'configLabel', | ||
elements: [ | ||
{ | ||
type: DS.ConfigDataElementType.DIMENSION, | ||
id: 'dimensions', | ||
label: 'configDimension1Label', | ||
options: { | ||
min: 1, | ||
max: numDimensions, | ||
supportedTypes: [], | ||
}, | ||
values: dimensionFields.map((a) => a.id), | ||
}, | ||
{ | ||
type: DS.ConfigDataElementType.METRIC, | ||
id: 'metrics', | ||
label: 'configMetric1Label', | ||
options: { | ||
min: 1, | ||
max: numMetrics, | ||
supportedTypes: [], | ||
}, | ||
values: metricFields.map((a) => a.id), | ||
}, | ||
], | ||
}, | ||
style: { | ||
id: 'styleId', | ||
label: 'styleLabel', | ||
elements: [], | ||
}, | ||
}, | ||
fields, | ||
dataResponse: { | ||
tables: [ | ||
{ | ||
type: DS.TableType.DEFAULT, | ||
fields: fields.map((a) => a.id), | ||
rows: [1, 2].map((num) => { | ||
return fields.map((a) => { | ||
switch (a.type) { | ||
case DS.FieldType.TEXT: | ||
return '' + num; | ||
case DS.FieldType.NUMBER: | ||
return num; | ||
case DS.FieldType.BOOLEAN: | ||
return num % 2 === 0; | ||
case DS.FieldType.PERCENT: | ||
return num / 100.0; | ||
default: | ||
throw new Error(`${a.type} is not supported yet.`); | ||
} | ||
}); | ||
}), | ||
}, | ||
], | ||
}, | ||
}; | ||
}; | ||
test('rowsByConfigId test', () => { | ||
const message: DS.Message = testMessage(2, 2); | ||
const expected = { | ||
[DS.TableType.COMPARISON]: [], | ||
[DS.TableType.SUMMARY]: [], | ||
[DS.TableType.DEFAULT]: [ | ||
{ | ||
id: 'firstId', | ||
label: 'label for first one', | ||
type: DS.ConfigElementType.DIMENSION, | ||
options: { | ||
max: 1, | ||
min: 1, | ||
supportedTypes: [], | ||
options: [], | ||
}, | ||
dimensions: ['1', false], | ||
metrics: [1, 0.01], | ||
}, | ||
{ | ||
id: 'thirdId', | ||
label: 'label for first one', | ||
type: DS.ConfigElementType.METRIC, | ||
options: { | ||
max: 1, | ||
min: 1, | ||
supportedTypes: [], | ||
options: [], | ||
}, | ||
dimensions: ['2', true], | ||
metrics: [2, 0.02], | ||
}, | ||
], | ||
}, | ||
]; | ||
}; | ||
const actual = sut.rowsByConfigId(message); | ||
expect(actual).toEqual(expected); | ||
}); | ||
const testMessageSchema: DS.Schema = [ | ||
{ | ||
name: 'c_id', | ||
label: 'Campaign', | ||
concept: 'DIMENSION', | ||
dataType: 'STRING', | ||
semantic: 'TEXT', | ||
}, | ||
{ | ||
name: 'a_id', | ||
label: 'Avg. Order Value', | ||
concept: 'METRIC', | ||
dataType: 'NUMBER', | ||
semantic: 'CURRENCY', | ||
semanticOption: 'USD', | ||
}, | ||
]; | ||
test('fieldsById works correctly', () => { | ||
const message: DS.Message = testMessage(1, 1); | ||
const actual = sut.fieldsById(message); | ||
expect(actual).toMatchObject({[message.fields[0].id]: message.fields[0]}); | ||
expect(actual).toMatchObject({[message.fields[1].id]: message.fields[1]}); | ||
}); | ||
const testMessageStyle = { | ||
useSingleColor: true, | ||
reverseColor: false, | ||
showText: true, | ||
fontSize: 10, | ||
fontColor: { | ||
color: '#fff', | ||
}, | ||
font: 'auto', | ||
}; | ||
test('parseImage all three fields present', () => { | ||
const input = 'originalurl.com\u00a0\u00a0proxiedurl.com\u00a0\u00a0alt text'; | ||
const expected: DS.ParsedImage = { | ||
originalUrl: 'originalurl.com', | ||
proxiedUrl: 'proxiedurl.com', | ||
altText: 'alt text', | ||
}; | ||
const actual = sut.parseImage(input); | ||
expect(actual).toEqual(expected); | ||
}); | ||
const testMessageData: DS.Message = { | ||
type: 'RENDER', | ||
rows: testMessageRows, | ||
schema: testMessageSchema, | ||
style: testMessageStyle, | ||
data: testPropertyConfig, | ||
}; | ||
test('parseImage two fields present', () => { | ||
const input = 'originalurl.com\u00a0\u00a0proxiedurl.com'; | ||
const expected: DS.ParsedImage = { | ||
originalUrl: 'originalurl.com', | ||
proxiedUrl: 'proxiedurl.com', | ||
altText: undefined, | ||
}; | ||
const actual = sut.parseImage(input); | ||
expect(actual).toEqual(expected); | ||
}); | ||
const testMessage = { | ||
data: testMessageData, | ||
}; | ||
test('parseImage one fields present', () => { | ||
const input = 'originalurl.com'; | ||
const expected: DS.ParsedImage = { | ||
originalUrl: 'originalurl.com', | ||
proxiedUrl: undefined, | ||
altText: undefined, | ||
}; | ||
const actual = sut.parseImage(input); | ||
expect(actual).toEqual(expected); | ||
}); | ||
beforeEach(() => { | ||
test('subscribeToData works', () => { | ||
const addEventListenerMock = jest.fn((event, cb) => { | ||
if (event === 'message') { | ||
cb(testMessage); | ||
cb({data: testMessage(1, 1)}); | ||
} else { | ||
@@ -114,22 +206,8 @@ throw new Error('unsupported event type for testing'); | ||
window.removeEventListener = removeEventListenerMock; | ||
}); | ||
test('subscribeToData default transform works', async () => { | ||
const unSub = await subscribeToData((actual) => { | ||
expect(actual).not.toHaveProperty('data.rows.comparisonRows'); | ||
expect(actual).toHaveProperty('rows'); | ||
const unSub = sut.subscribeToData((actual) => { | ||
expect(actual).toBeTruthy(); | ||
}); | ||
unSub(); | ||
expect(window.removeEventListener.mock.calls.length).toBeGreaterThan(0); | ||
expect(removeEventListenerMock.mock.calls.length).toBeGreaterThan(0); | ||
}); | ||
test('subscribeToData no transform works', async () => { | ||
const unSub = await subscribeToData( | ||
(actual) => { | ||
expect(actual).toHaveProperty('rows.comparisonRows'); | ||
}, | ||
{transform: (a) => a} | ||
); | ||
unSub(); | ||
expect(window.removeEventListener.mock.calls.length).toBeGreaterThan(0); | ||
}); |
@@ -1,5 +0,83 @@ | ||
import * as impl from './impl'; | ||
import * as LibraryTypes from './library-types'; | ||
export declare const subscribeToData: (callback: (componentData: LibraryTypes.TransformedMessage) => void, subscriptionOptions?: impl.SubsciptionOptions) => Promise<() => void>; | ||
import { FieldsById, Message, ParsedImage, TablesByType } from './types'; | ||
export * from './types'; | ||
/** | ||
* Returns the width of the containing iframe of the vis. | ||
* | ||
* Usage: | ||
* ``` | ||
* var myWidth = dscc.getWidth(); | ||
* console.log('My width is: ', myWidth); | ||
* ``` | ||
*/ | ||
export declare const getWidth: () => number; | ||
/** | ||
* Returns the height of the containing iframe of the vis. | ||
* | ||
* Usage: | ||
* ``` | ||
* var myHeight = dscc.getHeight(); | ||
* console.log('My height is: ', myHeight); | ||
* ``` | ||
*/ | ||
export declare const getHeight: () => number; | ||
/** | ||
* Parses a `'\u00a0\u00a0'` delimited string into component parts. If any parts | ||
* are missing, they will be returned as undefined. | ||
* | ||
* Usage: | ||
* ``` | ||
* const myImage = parseImage('originalurl.com\u00a0\u00a0proxiedurl.com\u00a0\u00a0alt text'); | ||
* | ||
* expect(myImage).toEqual({ | ||
* originalUrl: 'originalurl.com', | ||
* proxiedUrl: 'proxiedurl.com', | ||
* altText: 'alt text', | ||
* }); | ||
* ``` | ||
*/ | ||
export declare const parseImage: (value: string) => ParsedImage; | ||
/** | ||
* Returns the fields indexed by their Data Studio id. This is useful if you | ||
* have to lookups many fields so you don't have to do a full search every time. | ||
* | ||
* Usage: | ||
* ``` | ||
* dscc.subscribeToData(function(message) { | ||
* var fieldsByDSId = dscc.fieldsByDSId(message); | ||
* var field1 = fieldsByDSId['field1Id']; | ||
* var field2 = fieldsByDSId['field2Id']; | ||
* }); | ||
* ``` | ||
*/ | ||
export declare const fieldsById: (message: Message) => FieldsById; | ||
/** | ||
* Returns all tables, with row values indexed by their configId. | ||
* | ||
* Usage: | ||
* ``` | ||
* dscc.subscribeToData(function(message) { | ||
* var rowsById = dscc.rowsByConfigId(message); | ||
* consale.log('rowsById', rowsById); | ||
* }) | ||
* ``` | ||
*/ | ||
export declare const rowsByConfigId: (message: Message) => TablesByType; | ||
/** | ||
* Subscribes to messages from Data Studio. Calls `cb` for every new | ||
* [[MessageType.RENDER]] message. Returns a function that will unsubscribe | ||
* `callback` from further invocations. | ||
* | ||
* Usage: | ||
* ``` | ||
* var unsubscribe = dscc.subscribeToData(function(message) { | ||
* console.log(message.fields) | ||
* console.log(message.config) | ||
* console.log(message.dataResponse) | ||
* }); | ||
* | ||
* setTimeout(function() { | ||
* unsubscribe(); | ||
* }, 3000) | ||
* ``` | ||
*/ | ||
export declare const subscribeToData: (cb: (componentData: Message) => void) => () => void; |
185
lib/index.js
"use strict"; | ||
function __export(m) { | ||
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; | ||
} | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
@@ -18,6 +21,180 @@ /*! | ||
*/ | ||
var impl = require("./impl"); | ||
exports.subscribeToData = impl.subscribeToData; | ||
exports.getWidth = impl.getWidth; | ||
exports.getHeight = impl.getHeight; | ||
var parse = require("url-parse"); | ||
var types_1 = require("./types"); | ||
// Make all exported types available to external users. | ||
__export(require("./types")); | ||
/** | ||
* Returns the width of the containing iframe of the vis. | ||
* | ||
* Usage: | ||
* ``` | ||
* var myWidth = dscc.getWidth(); | ||
* console.log('My width is: ', myWidth); | ||
* ``` | ||
*/ | ||
exports.getWidth = function () { return document.body.clientWidth; }; | ||
/** | ||
* Returns the height of the containing iframe of the vis. | ||
* | ||
* Usage: | ||
* ``` | ||
* var myHeight = dscc.getHeight(); | ||
* console.log('My height is: ', myHeight); | ||
* ``` | ||
*/ | ||
exports.getHeight = function () { return document.documentElement.clientHeight; }; | ||
/** | ||
* Returns the componentId of the vis. Component ids uniquely identify a vis to | ||
* Data Studio, and are sent with `postMessage`s to let Data Studio know which | ||
* vis sent data. | ||
* | ||
* Usage: | ||
* ``` | ||
* var myComponentId = dscc.getComponentId(); | ||
* console.log('My componentId is: ', myComponentId); | ||
* ``` | ||
*/ | ||
var getComponentId = function () { | ||
var parsed = parse(window.parent.location.href, true); | ||
return parsed.query.componentId; | ||
}; | ||
/** | ||
* Parses a `'\u00a0\u00a0'` delimited string into component parts. If any parts | ||
* are missing, they will be returned as undefined. | ||
* | ||
* Usage: | ||
* ``` | ||
* const myImage = parseImage('originalurl.com\u00a0\u00a0proxiedurl.com\u00a0\u00a0alt text'); | ||
* | ||
* expect(myImage).toEqual({ | ||
* originalUrl: 'originalurl.com', | ||
* proxiedUrl: 'proxiedurl.com', | ||
* altText: 'alt text', | ||
* }); | ||
* ``` | ||
*/ | ||
exports.parseImage = function (value) { | ||
var _a = value.split('\u00a0\u00a0'), originalUrl = _a[0], proxiedUrl = _a[1], altText = _a[2]; | ||
return { | ||
altText: altText, | ||
originalUrl: originalUrl, | ||
proxiedUrl: proxiedUrl, | ||
}; | ||
}; | ||
/** | ||
* Returns the fields indexed by their Data Studio id. This is useful if you | ||
* have to lookups many fields so you don't have to do a full search every time. | ||
* | ||
* Usage: | ||
* ``` | ||
* dscc.subscribeToData(function(message) { | ||
* var fieldsByDSId = dscc.fieldsByDSId(message); | ||
* var field1 = fieldsByDSId['field1Id']; | ||
* var field2 = fieldsByDSId['field2Id']; | ||
* }); | ||
* ``` | ||
*/ | ||
exports.fieldsById = function (message) { | ||
return message.fields.reduce(function (acc, field) { | ||
acc[field.id] = field; | ||
return acc; | ||
}, {}); | ||
}; | ||
var zip2 = function (t, u) { | ||
if (t.length < u.length) { | ||
return t.map(function (tEntry, idx) { return [tEntry, u[idx]]; }); | ||
} | ||
else { | ||
return u.map(function (uEntry, idx) { return [t[idx], uEntry]; }); | ||
} | ||
}; | ||
var toRowById = function (indexFields, row, fields) { | ||
var matched = zip2(fields, row); | ||
return matched.reduce(function (acc, _a) { | ||
var fieldId = _a[0], rowValue = _a[1]; | ||
var field = indexFields[fieldId]; | ||
var xformedValue = field.type === types_1.FieldType.IMAGE | ||
? exports.parseImage(rowValue) | ||
: rowValue; | ||
acc[fieldId] = xformedValue; | ||
return acc; | ||
}, {}); | ||
}; | ||
var toRowByConfigId = function (message, indexedFields, fieldIds, row) { | ||
var fieldRow = toRowById(indexedFields, row, fieldIds); | ||
var concepts = message.config.data.elements.filter(dimensionOrMetric); | ||
return concepts.reduce(function (rowObjects, element) { | ||
var rowData = element.values.map(function (fieldId) { return fieldRow[fieldId]; }); | ||
rowObjects[element.id] = rowData; | ||
return rowObjects; | ||
}, {}); | ||
}; | ||
var dimensionOrMetric = function (element) { | ||
return element.type === types_1.ConfigDataElementType.DIMENSION || | ||
element.type === types_1.ConfigDataElementType.METRIC; | ||
}; | ||
/** | ||
* Returns all tables, with row values indexed by their configId. | ||
* | ||
* Usage: | ||
* ``` | ||
* dscc.subscribeToData(function(message) { | ||
* var rowsById = dscc.rowsByConfigId(message); | ||
* consale.log('rowsById', rowsById); | ||
* }) | ||
* ``` | ||
*/ | ||
exports.rowsByConfigId = function (message) { | ||
var _a, _b; | ||
var indexFields = exports.fieldsById(message); | ||
var thing = (_a = {}, | ||
_a[types_1.TableType.COMPARISON] = [], | ||
_a[types_1.TableType.DEFAULT] = [], | ||
_a[types_1.TableType.SUMMARY] = [], | ||
_a); | ||
return message.dataResponse.tables.reduce(function (acc, table) { | ||
var tableData = table.rows.map(function (row) { | ||
return toRowByConfigId(message, indexFields, table.fields, row); | ||
}); | ||
acc[table.type] = tableData; | ||
return acc; | ||
}, (_b = {}, | ||
_b[types_1.TableType.COMPARISON] = [], | ||
_b[types_1.TableType.DEFAULT] = [], | ||
_b[types_1.TableType.SUMMARY] = [], | ||
_b)); | ||
}; | ||
/** | ||
* Subscribes to messages from Data Studio. Calls `cb` for every new | ||
* [[MessageType.RENDER]] message. Returns a function that will unsubscribe | ||
* `callback` from further invocations. | ||
* | ||
* Usage: | ||
* ``` | ||
* var unsubscribe = dscc.subscribeToData(function(message) { | ||
* console.log(message.fields) | ||
* console.log(message.config) | ||
* console.log(message.dataResponse) | ||
* }); | ||
* | ||
* setTimeout(function() { | ||
* unsubscribe(); | ||
* }, 3000) | ||
* ``` | ||
*/ | ||
exports.subscribeToData = function (cb) { | ||
var onMessage = function (message) { | ||
if (message.data.type === types_1.MessageType.RENDER) { | ||
cb(message.data); | ||
} | ||
else { | ||
console.error("MessageType: " + message.data.type + " is not supported by this version of the library."); | ||
} | ||
}; | ||
window.addEventListener('message', onMessage); | ||
var componentId = getComponentId(); | ||
// Tell DataStudio that the viz is ready to get events. | ||
window.parent.postMessage({ componentId: componentId, type: 'vizReady' }, '*'); | ||
return function () { return window.removeEventListener('message', onMessage); }; | ||
}; | ||
//# sourceMappingURL=index.js.map |
24
NPM.md
@@ -8,12 +8,16 @@ # Deploying New NPM Version | ||
+ Make sure the code has been built & versioned | ||
```bash | ||
yarn install && \ | ||
yarn build && \ | ||
yarn version | ||
``` | ||
1. Make sure the code has been built & versioned | ||
+ Publish the code to npm | ||
```bash | ||
npm publish --access public | ||
``` | ||
```bash | ||
yarn install | ||
yarn build | ||
yarn version | ||
``` | ||
1. Commit new changes. | ||
1. Publish the code to npm | ||
```bash | ||
npm publish --access public | ||
``` |
{ | ||
"name": "@google/dscc", | ||
"libraryName": "dscc", | ||
"version": "0.0.9", | ||
"version": "0.1.0", | ||
"main": "lib/index.js", | ||
@@ -14,7 +14,8 @@ "license": "Apache-2.0", | ||
"build-watch": "onchange \"{src,__tests__}/**/*.ts\" -- yarn build", | ||
"lint": "eslint src/**", | ||
"lint": "tslint -c tslint.json src/**/*.ts", | ||
"test": "jest", | ||
"prettier": "prettier --write \"{src,__tests__}/**/*.ts\"", | ||
"prettier-watch": "onchange \"{src,__tests__}/**/*.ts\" -- yarn prettier", | ||
"dev": "webpack --progress --colors --watch --env dev" | ||
"prettier": "prettier --write \"{src,__tests__,examples}/**/*.ts\"", | ||
"prettier-watch": "onchange \"{src,__tests__,examples}/**/*.ts\" -- yarn prettier", | ||
"dev": "webpack --progress --colors --watch --env dev", | ||
"doc": "typedoc --out docs/ src/" | ||
}, | ||
@@ -24,19 +25,10 @@ "devDependencies": { | ||
"@types/url-parse": "^1.1.0", | ||
"babel-cli": "^6.26.0", | ||
"babel-core": "^6.26.0", | ||
"babel-eslint": "^8.0.3", | ||
"babel-loader": "^7.1.2", | ||
"babel-plugin-add-module-exports": "^0.2.1", | ||
"babel-plugin-transform-object-rest-spread": "^6.26.0", | ||
"babel-polyfill": "^6.26.0", | ||
"babel-preset-env": "^1.6.1", | ||
"eslint": "^4.13.1", | ||
"eslint-config-google": "^0.9.1", | ||
"eslint-loader": "^1.9.0", | ||
"jest": "^22.4.2", | ||
"onchange": "^3.3.0", | ||
"prettier": "^1.12.1", | ||
"ts-jest": "^22.4.4", | ||
"ts-jest": "^23.10.1", | ||
"ts-loader": "^4.2.0", | ||
"typescript": "^2.8.1", | ||
"tslint": "^5.11.0", | ||
"typedoc": "^0.12.0", | ||
"typescript": "^3.0.3", | ||
"webpack": "^4.6.0", | ||
@@ -43,0 +35,0 @@ "webpack-cli": "^2.0.14", |
@@ -8,58 +8,11 @@ # dscc - Google Data Studio Community Component Helper Library | ||
## Public Interface | ||
## Documentation | ||
### `subscribeToData(callback, subscriptionOptions)` | ||
See [the doc directory][docs] for full documentation. | ||
Calls `callback` every time Data Studio pushes a new `Message` object to your | ||
component. `subscribeToData` returns a method to unsubscribe your callback. | ||
## Basic Usage | ||
`callback` should be a function that takes the type that `subscriptionOptions.transform` returns. For full | ||
details on the `Message` object, see [library-types.ts] | ||
`dscc` can be used through npm, or by copying the contents into the beginning of | ||
your javascript file. | ||
`subscriptionOptions` is an object with a `transform` property. This should be a | ||
function that takes a Data Studio `Message` type and performs an appropriate | ||
data transform. | ||
#### Usage | ||
```javascript | ||
var callback = function(message) { | ||
// Logs out a `Message` object. | ||
console.log(message) | ||
} | ||
var unsubscribe = dscc.subscribeToData(callback); | ||
setTimeout(function() { | ||
// Unsubscribe callback from being called after 3 seconds. | ||
unsubscribe(); | ||
}, 3000); | ||
``` | ||
### `getWidth()` | ||
Returns the width (in pixels of the containing iframe). | ||
#### Usage | ||
```javascript | ||
var width = dscc.getWidth(); | ||
// This will log out the width of the iframe. | ||
console.log(width); | ||
``` | ||
### `getHeight()` | ||
Returns the height (in pixels of the containing iframe). | ||
#### Usage | ||
```javascript | ||
var height = dscc.getHeight(); | ||
// This will log out the height of the iframe. | ||
console.log(height); | ||
``` | ||
## Using `dscc` from your component | ||
The dscc library can be used through npm, or by copying the contents into the | ||
beginning of your javascript file. | ||
### Through Npm | ||
@@ -87,3 +40,3 @@ | ||
console.log(message) | ||
// Create component as needed using componentData, height, and width... | ||
// Create component as needed using message, height, and width... | ||
}) | ||
@@ -108,3 +61,3 @@ } | ||
console.log(message) | ||
// Create component as needed using componentData, height, and width... | ||
// Create component as needed using message, height, and width... | ||
}); | ||
@@ -132,2 +85,2 @@ ``` | ||
[dscv-devsite]: https://developers.google.com/datastudio/visualization/ | ||
[library-types.ts]: https://github.com/googledatastudio/ds-component/blob/master/src/library-types.ts#L13-L19 | ||
[docs]: https://googledatastudio.github.io/ds-component/ |
238
src/index.ts
@@ -16,8 +16,234 @@ /*! | ||
*/ | ||
import * as impl from './impl'; | ||
import * as LibraryTypes from './library-types'; | ||
import * as DataStudioTypes from './data-studio-types'; | ||
import * as parse from 'url-parse'; | ||
import { | ||
ConfigDataElement, | ||
ConfigDataElementType, | ||
Field, | ||
FieldId, | ||
FieldsById, | ||
FieldType, | ||
Message, | ||
MessageType, | ||
ParsedImage, | ||
ParsedRowValue, | ||
PostMessage, | ||
Row, | ||
RowByConfigId, | ||
RowValue, | ||
Table, | ||
TablesByType, | ||
TableType, | ||
} from './types'; | ||
export const subscribeToData = impl.subscribeToData; | ||
export const getWidth = impl.getWidth; | ||
export const getHeight = impl.getHeight; | ||
// Make all exported types available to external users. | ||
export * from './types'; | ||
/** | ||
* Returns the width of the containing iframe of the vis. | ||
* | ||
* Usage: | ||
* ``` | ||
* var myWidth = dscc.getWidth(); | ||
* console.log('My width is: ', myWidth); | ||
* ``` | ||
*/ | ||
export const getWidth = (): number => document.body.clientWidth; | ||
/** | ||
* Returns the height of the containing iframe of the vis. | ||
* | ||
* Usage: | ||
* ``` | ||
* var myHeight = dscc.getHeight(); | ||
* console.log('My height is: ', myHeight); | ||
* ``` | ||
*/ | ||
export const getHeight = (): number => document.documentElement.clientHeight; | ||
/** | ||
* Returns the componentId of the vis. Component ids uniquely identify a vis to | ||
* Data Studio, and are sent with `postMessage`s to let Data Studio know which | ||
* vis sent data. | ||
* | ||
* Usage: | ||
* ``` | ||
* var myComponentId = dscc.getComponentId(); | ||
* console.log('My componentId is: ', myComponentId); | ||
* ``` | ||
*/ | ||
const getComponentId = (): string => { | ||
const parsed = parse(window.parent.location.href, true); | ||
return parsed.query.componentId; | ||
}; | ||
/** | ||
* Parses a `'\u00a0\u00a0'` delimited string into component parts. If any parts | ||
* are missing, they will be returned as undefined. | ||
* | ||
* Usage: | ||
* ``` | ||
* const myImage = parseImage('originalurl.com\u00a0\u00a0proxiedurl.com\u00a0\u00a0alt text'); | ||
* | ||
* expect(myImage).toEqual({ | ||
* originalUrl: 'originalurl.com', | ||
* proxiedUrl: 'proxiedurl.com', | ||
* altText: 'alt text', | ||
* }); | ||
* ``` | ||
*/ | ||
export const parseImage = (value: string): ParsedImage => { | ||
const [originalUrl, proxiedUrl, altText] = value.split('\u00a0\u00a0'); | ||
return { | ||
altText, | ||
originalUrl, | ||
proxiedUrl, | ||
}; | ||
}; | ||
/** | ||
* Returns the fields indexed by their Data Studio id. This is useful if you | ||
* have to lookups many fields so you don't have to do a full search every time. | ||
* | ||
* Usage: | ||
* ``` | ||
* dscc.subscribeToData(function(message) { | ||
* var fieldsByDSId = dscc.fieldsByDSId(message); | ||
* var field1 = fieldsByDSId['field1Id']; | ||
* var field2 = fieldsByDSId['field2Id']; | ||
* }); | ||
* ``` | ||
*/ | ||
export const fieldsById = (message: Message): FieldsById => | ||
message.fields.reduce((acc: FieldsById, field: Field) => { | ||
acc[field.id] = field; | ||
return acc; | ||
}, {}); | ||
const zip2 = <T, U>(t: T[], u: U[]): Array<[T, U]> => { | ||
if (t.length < u.length) { | ||
return t.map((tEntry: T, idx: number): [T, U] => [tEntry, u[idx]]); | ||
} else { | ||
return u.map((uEntry: U, idx: number): [T, U] => [t[idx], uEntry]); | ||
} | ||
}; | ||
interface RowById { | ||
[fieldId: string]: ParsedRowValue; | ||
} | ||
const toRowById = ( | ||
indexFields: FieldsById, | ||
row: Row, | ||
fields: FieldId[] | ||
): RowById => { | ||
const matched = zip2(fields, row); | ||
return matched.reduce( | ||
(acc: RowById, [fieldId, rowValue]: [FieldId, RowValue]) => { | ||
const field: Field = indexFields[fieldId]; | ||
const xformedValue = | ||
field.type === FieldType.IMAGE | ||
? parseImage(rowValue as string) | ||
: rowValue; | ||
acc[fieldId] = xformedValue; | ||
return acc; | ||
}, | ||
{} | ||
); | ||
}; | ||
const toRowByConfigId = ( | ||
message: Message, | ||
indexedFields: FieldsById, | ||
fieldIds: FieldId[], | ||
row: Row | ||
): RowByConfigId => { | ||
const fieldRow: RowById = toRowById(indexedFields, row, fieldIds); | ||
const concepts = message.config.data.elements.filter(dimensionOrMetric); | ||
return concepts.reduce( | ||
(rowObjects: RowByConfigId, element: ConfigDataElement) => { | ||
const rowData = element.values.map( | ||
(fieldId: FieldId) => fieldRow[fieldId] | ||
); | ||
rowObjects[element.id] = rowData; | ||
return rowObjects; | ||
}, | ||
{} | ||
); | ||
}; | ||
const dimensionOrMetric = (element: ConfigDataElement): boolean => | ||
element.type === ConfigDataElementType.DIMENSION || | ||
element.type === ConfigDataElementType.METRIC; | ||
/** | ||
* Returns all tables, with row values indexed by their configId. | ||
* | ||
* Usage: | ||
* ``` | ||
* dscc.subscribeToData(function(message) { | ||
* var rowsById = dscc.rowsByConfigId(message); | ||
* consale.log('rowsById', rowsById); | ||
* }) | ||
* ``` | ||
*/ | ||
export const rowsByConfigId = (message: Message): TablesByType => { | ||
const indexFields = fieldsById(message); | ||
const thing: TablesByType = { | ||
[TableType.COMPARISON]: [], | ||
[TableType.DEFAULT]: [], | ||
[TableType.SUMMARY]: [], | ||
}; | ||
return message.dataResponse.tables.reduce( | ||
(acc: TablesByType, table: Table) => { | ||
const tableData: RowByConfigId[] = table.rows.map((row) => | ||
toRowByConfigId(message, indexFields, table.fields, row) | ||
); | ||
acc[table.type] = tableData; | ||
return acc; | ||
}, | ||
{ | ||
[TableType.COMPARISON]: [], | ||
[TableType.DEFAULT]: [], | ||
[TableType.SUMMARY]: [], | ||
} | ||
); | ||
}; | ||
/** | ||
* Subscribes to messages from Data Studio. Calls `cb` for every new | ||
* [[MessageType.RENDER]] message. Returns a function that will unsubscribe | ||
* `callback` from further invocations. | ||
* | ||
* Usage: | ||
* ``` | ||
* var unsubscribe = dscc.subscribeToData(function(message) { | ||
* console.log(message.fields) | ||
* console.log(message.config) | ||
* console.log(message.dataResponse) | ||
* }); | ||
* | ||
* setTimeout(function() { | ||
* unsubscribe(); | ||
* }, 3000) | ||
* ``` | ||
*/ | ||
export const subscribeToData = ( | ||
cb: (componentData: Message) => void | ||
): (() => void) => { | ||
const onMessage = (message: PostMessage) => { | ||
if (message.data.type === MessageType.RENDER) { | ||
cb(message.data); | ||
} else { | ||
console.error( | ||
`MessageType: ${ | ||
message.data.type | ||
} is not supported by this version of the library.` | ||
); | ||
} | ||
}; | ||
window.addEventListener('message', onMessage); | ||
const componentId = getComponentId(); | ||
// Tell DataStudio that the viz is ready to get events. | ||
window.parent.postMessage({componentId, type: 'vizReady'}, '*'); | ||
return () => window.removeEventListener('message', onMessage); | ||
}; |
@@ -27,3 +27,10 @@ /*! | ||
outputFile = libraryName + '.min.js'; | ||
plugins.push(new UglifyJsPlugin({sourceMap: false})); | ||
plugins.push(new UglifyJsPlugin({ | ||
sourceMap: false, | ||
uglifyOptions: { | ||
output: { | ||
comments: false | ||
} | ||
} | ||
})); | ||
} else { | ||
@@ -30,0 +37,0 @@ outputFile = libraryName + '.js'; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 2 instances in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
2149058
13
77
4420
3
83
6