Коммит
c56f870a51
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче