Merge pull request #135 from CatalystCode/plugin-docdb

CosmosDB Plugin
This commit is contained in:
Mor Shemesh 2017-05-23 14:39:01 +03:00 коммит произвёл GitHub
Родитель 36997c502d 6582ba54bb
Коммит c56f870a51
8 изменённых файлов: 350 добавлений и 8 удалений

71
docs/cosmos-db.md Normal file
Просмотреть файл

@ -0,0 +1,71 @@
# CosmosDB
CosmosDB can be added to the list of required connections.
## Config
To enable [Cosmos DB](https://azure.microsoft.com/en-us/services/cosmos-db/) data sources add 'cosmos-db' to the connections config. The host name and password key are required values.
```js
connections: {
'cosmos-db': {
'host': "",
'key': ""
}
}
```
NB. The host name excludes the '.documents.azure.com' suffix.
## Data Sources
| Property | Type | Value | Description
| :--------|:-----|:------|:------------
| `id`| `string` || ID of the element
| `type`| `string` | "CosmosDB/Query" | Data source plugin name
| `dependencies`| `object` || A collection of key values referenced by queries
| `params`| `object` || Contains `databaseId`, `collectionId`, `query` and `parameters`
| `calculated` | `function` || Result contains array of `Documents`, `_rid` and `_count` properties.
## Params
| Property | Type | Description
| :--------|:-----|:------------
| `databaseId`| `string` | Database Id (default is 'admin')
| `collectionId`| `string` | Collection Id
| `query`| `string` | SQL query string
| `parameters`| `object[]` | Parameterized SQL request
More info about SQL `query` string and `parameters` are available in the [CosmosDB documentation](https://docs.microsoft.com/en-us/rest/api/documentdb/querying-documentdb-resources-using-the-rest-api). You can try out Cosmos DB queries using the *Query Explorer* in the [Azure portal](https://portal.azure.com/), or learn using the [SQL demo](https://www.documentdb.com/sql/demo).
## Sample data source
```js
{
id: "botConversations",
type: "CosmosDB/Query",
dependencies: { timespan: "timespan", queryTimespan: "timespan:queryTimespan" },
params: {
databaseId: "admin",
collectionId: "conversations",
query: () => `SELECT * FROM conversations WHERE (conversations.state = 0)`,
parameters: []
},
calculated: (result) => {
return result;
}
}
```
## Sample element
```
{
id: "conversations",
type: "Scorecard",
title: "Conversations",
subtitle: "Total conversations",
size: { w: 4, h: 3 },
dependencies: {
card_conversations_value: "botConversations:_count",
card_conversations_heading: "::Conversations",
card_conversations_icon: "::chat"
}
}
```

Просмотреть файл

@ -8,6 +8,7 @@ const fs = require('fs');
const bodyParser = require('body-parser');
const authRouter = require('./routes/auth');
const apiRouter = require('./routes/api');
const cosmosDBRouter = require('./routes/cosmos-db');
const azureRouter = require('./routes/azure');
const app = express();
@ -22,6 +23,7 @@ app.use(morgan(':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:htt
app.use(authRouter.authenticationMiddleware('/auth', '/api/setup'));
app.use('/auth', authRouter.router);
app.use('/api', apiRouter.router);
app.use('/cosmosdb', cosmosDBRouter.router);
app.use('/azure', azureRouter.router);
app.use(express.static(path.resolve(__dirname, '..', 'build')));

Просмотреть файл

@ -0,0 +1,64 @@
const express = require('express');
const request = require('xhr-request');
const router = new express.Router();
const crypto = require('crypto');
router.post('/query', (req, res) => {
const { host, key, verb, resourceType, databaseId, collectionId, query, parameters } = req.body;
if ( !host || !key || !verb || !resourceType || !databaseId || !collectionId || !query ) {
console.log('Invalid request parameters');
return res.send({ error: 'Invalid request parameters' });
}
const date = new Date().toUTCString();
const resourceLink = `dbs/${databaseId}/colls/${collectionId}`;
const auth = getAuthorizationTokenUsingMasterKey(verb, resourceType, resourceLink, date, key);
let cosmosQuery = {
query: query,
parameters: parameters || [],
};
const url = `https://${host}.documents.azure.com/${resourceLink}/docs`;
request(url, {
method: 'POST',
headers: {
'Content-Type': 'application/query+json',
'Accept': 'application/json',
'Authorization': auth,
'x-ms-date': date,
'x-ms-version': '2015-04-08',
'x-ms-documentdb-isquery': true,
},
body: cosmosQuery,
responseType: 'json',
json: true,
}, (err, doc) => {
if (err) {
console.log(err);
return res.send({ error: err });
}
res.send(doc);
});
});
function getAuthorizationTokenUsingMasterKey(verb, resourceType, resourceLink, date, masterKey) {
var key = new Buffer(masterKey, "base64");
var text = (verb || "").toLowerCase() + "\n" +
(resourceType || "").toLowerCase() + "\n" +
(resourceLink || "") + "\n" +
date.toLowerCase() + "\n" +
"" + "\n";
var body = new Buffer(text, "utf8");
var signature = crypto.createHmac("sha256", key).update(body).digest("base64");
var MasterToken = "master";
var TokenVersion = "1.0";
return encodeURIComponent("type=" + MasterToken + "&ver=" + TokenVersion + "&sig=" + signature);
}
module.exports = {
router
}

Просмотреть файл

@ -0,0 +1,66 @@
import * as React from 'react';
import { IConnection, ConnectionEditor, IConnectionProps } from './Connection';
import InfoDrawer from '../../components/common/InfoDrawer';
import TextField from 'react-md/lib/TextFields';
import Checkbox from 'react-md/lib/SelectionControls/Checkbox';
export default class CosmosDBConnection implements IConnection {
type = 'cosmos-db';
params = ['host', 'key'];
editor = CosmosDBConnectionEditor;
}
class CosmosDBConnectionEditor extends ConnectionEditor<IConnectionProps, any> {
constructor(props: IConnectionProps) {
super(props);
this.onParamChange = this.onParamChange.bind(this);
}
onParamChange(value: string, event: any) {
if (typeof this.props.onParamChange === 'function') {
this.props.onParamChange('cosmos-db', event.target.id, value);
}
}
render() {
let { connection } = this.props;
// connection = connection || {'ssl':true };
return (
<div>
<h2 style={{ float: 'left', padding: 9 }}>CosmosDB</h2>
<InfoDrawer
width={300}
title="CosmosDB"
buttonIcon="help"
buttonTooltip="Click here to learn more about CosmosDB"
>
<div>
<a href="https://azure.microsoft.com/en-us/services/cosmos-db/" target="_blank">Create Cosmos DB</a>
<hr/>
<a href="https://www.documentdb.com/sql/demo" target="_blank">Try CosmosDB demo queries</a>
</div>
</InfoDrawer>
<TextField
id="host"
label={'Host'}
defaultValue={connection['host'] || ''}
lineDirection="center"
placeholder="Fill in hostname"
className="md-cell md-cell--bottom"
onChange={this.onParamChange}
/>
<TextField
id="key"
label={'Key'}
defaultValue={connection['key'] || ''}
lineDirection="center"
placeholder="Fill in Key"
className="md-cell md-cell--bottom"
onChange={this.onParamChange}
/>
</div>
);
}
}

Просмотреть файл

@ -1,10 +1,11 @@
import * as React from 'react';
import ApplicationInsightsConnection from './application-insights';
import CosmosDBConnection from './cosmos-db';
import AzureConnection from './azure';
import { IConnection } from './Connection';
var connectionTypes = [ ApplicationInsightsConnection, AzureConnection ];
var connectionTypes = [ ApplicationInsightsConnection, AzureConnection, CosmosDBConnection ];
var connections: IDict<IConnection> = {};
connectionTypes.forEach(connectionType => {

Просмотреть файл

@ -0,0 +1,136 @@
import * as request from 'xhr-request';
import { DataSourcePlugin, IOptions } from '../DataSourcePlugin';
import { DataSourceConnector } from '../../DataSourceConnector';
import CosmosDBConnection from '../../connections/cosmos-db';
let connectionType = new CosmosDBConnection();
interface IQueryParams {
query?: ((dependencies: any) => string) | string;
parameters?: (string | object)[];
mappings?: (string | object)[];
databaseId?: string;
collectionId?: string;
calculated?: (results: any) => object;
}
export default class CosmosDBQuery extends DataSourcePlugin<IQueryParams> {
type = 'CosmosDB-Query';
defaultProperty = 'Documents';
connectionType = connectionType.type;
constructor(options: IOptions<IQueryParams>, connections: IDict<IStringDictionary>) {
super(options, connections);
this.validateTimespan(this._props);
this.validateParams(this._props.params);
}
/**
* update - called when dependencies are created
* @param {object} dependencies
* @param {function} callback
*/
updateDependencies(dependencies: any) {
let emptyDependency = false;
Object.keys(this._props.dependencies).forEach((key) => {
if (typeof dependencies[key] === 'undefined') { emptyDependency = true; }
});
// If one of the dependencies is not supplied, do not run the query
if (emptyDependency) {
return (dispatch) => {
return dispatch();
};
}
// Validate connection
let connection = this.getConnection();
let { host, key } = connection;
if (!connection || !host || !key) {
return (dispatch) => {
return dispatch();
};
}
const params = this._props.params;
const query: string = this.compileQuery(params.query, dependencies);
const url = `/cosmosdb/query`;
const body = {
host: host,
key: key,
verb: 'POST',
databaseId: params.databaseId,
collectionId: params.collectionId,
resourceType: 'docs',
query: query,
parameters: params.parameters
};
return (dispatch) => {
request(url, {
method: 'POST',
json: true,
body: body,
}, (error, json) => {
if (error || !json.Documents) {
throw new Error(error);
}
let documents = json.Documents;
// NB: CosmosDB prefixes certain keys with '$' which will be removed for the returned result.
this.remap(documents);
let returnedResults = {
'Documents': documents || [],
'_count': json._count || 0,
'_rid': json._rid || undefined
};
return dispatch(returnedResults);
});
};
}
updateSelectedValues(dependencies: IDictionary, selectedValues: any) {
if (Array.isArray(selectedValues)) {
return Object.assign(dependencies, { 'selectedValues': selectedValues });
} else {
return Object.assign(dependencies, { ... selectedValues });
}
}
private compileQuery(query: any, dependencies: any): string {
return typeof query === 'function' ? query(dependencies) : query;
}
private validateTimespan(props: any) {
}
private validateParams(params: IQueryParams): void {
}
// Helper methods to strip dollar sign from JSON key names
private remap(json: any) {
if (typeof json === 'object') {
return this.remapObject(json);
} else if (Array.isArray(json)) {
return this.remapArray(json);
} else {
return json;
}
}
private remapArray(arr: any[]) {
arr.map(this.remap);
}
private remapObject(obj: Object) {
Object.keys(obj).forEach(key => {
const value = obj[key];
this.remap(value);
if (key.startsWith('$')) {
const newKey = key.substr(1);
Object.defineProperty(obj, newKey, Object.getOwnPropertyDescriptor(obj, key));
delete obj[key];
}
});
}
}

Просмотреть файл

@ -1,7 +1,9 @@
import ApplicationInsightsQuery from './ApplicationInsights/Query';
import CosmosDBQuery from './CosmosDB/Query';
import Azure from './Azure';
export default {
ApplicationInsightsQuery,
CosmosDBQuery,
Azure
};

Просмотреть файл

@ -35,7 +35,7 @@ class ConfigurationsStore extends AbstractStoreModel<IConfigurationsStoreState>
this.template = null;
this.templates = null;
this.creationState = null;
this.connections = {};
this.connections = {};
this.connectionsMissing = false;
this.loaded = false;
@ -52,13 +52,13 @@ class ConfigurationsStore extends AbstractStoreModel<IConfigurationsStoreState>
if (pathname === '/dashboard') {
configurationActions.loadDashboard('0');
}
if (pathname.startsWith('/dashboard/')) {
let dashboardId = pathname.substring('/dashboard/'.length);
configurationActions.loadDashboard(dashboardId);
}
}
loadConfiguration(result: { dashboards: IDashboardConfig[], templates: IDashboardConfig[] }) {
let { dashboards, templates } = result;
this.dashboards = dashboards;
@ -71,14 +71,14 @@ class ConfigurationsStore extends AbstractStoreModel<IConfigurationsStoreState>
if (this.dashboard && !this.loaded) {
DataSourceConnector.createDataSources(dashboard, dashboard.config.connections);
this.connections = this.getConnections(dashboard);
// Checking for missing connection params
this.connectionsMissing = Object.keys(this.connections).some(connectionKey => {
var connection = this.connections[connectionKey];
return Object.keys(connection).some(paramKey => !connection[paramKey]);
return Object.keys(connection).some(paramKey => connection[paramKey] !== false && !connection[paramKey]);
});
}
}
@ -92,7 +92,7 @@ class ConfigurationsStore extends AbstractStoreModel<IConfigurationsStoreState>
this.template = template;
if (this.template) {
this.connections = this.getConnections(template);
// Checking for missing connection params