Merge pull request #1 from Azure/master

get last changes
This commit is contained in:
Cristian 2018-05-04 14:04:01 +02:00 коммит произвёл GitHub
Родитель 2c1daf266a acabc27a33
Коммит 58a7b57f9f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
340 изменённых файлов: 22323 добавлений и 12736 удалений

22
.dockerignore Normal file
Просмотреть файл

@ -0,0 +1,22 @@
# dependencies
node_modules/
# testing
/coverage
# misc
.DS_Store
.env
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# generated files
client/**/*.css
# generated merge temp files
*.orig
# private files
*.private.*
!*sample.basic.private.js

11
.gitignore поставляемый
Просмотреть файл

@ -1,10 +1,17 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
node_modules/
# testing
/coverage
/client/coverage
# build files
/build
# client build files
/client/build
# misc
.DS_Store
@ -14,7 +21,7 @@ yarn-debug.log*
yarn-error.log*
# generated files
src/**/*.css
client/**/*.css
# generated merge temp files
*.orig

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

@ -4,13 +4,27 @@ language:
node_js:
- "7"
install:
- yarn install
- yarn run css:build
sudo:
required
env:
- YARN_COMMAND="lint"
- YARN_COMMAND="test"
before_install:
- curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
- sudo apt-key adv --keyserver pgp.mit.edu --recv D101F7899D41F3C3
- echo "deb http://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
- sudo apt-get update -qq
- sudo apt-get install -y -qq yarn
cache:
directories:
- $HOME/.yarn-cache
install:
- .travis/setup.sh
script:
- CI=true yarn $YARN_COMMAND
- .travis/ci.sh
- .travis/coverage.sh
after_success:
- .travis/build.sh
- .travis/push.sh

3
.travis/build.sh Executable file
Просмотреть файл

@ -0,0 +1,3 @@
#!/usr/bin/env bash
# yarn build

4
.travis/ci.sh Executable file
Просмотреть файл

@ -0,0 +1,4 @@
#!/usr/bin/env bash
(cd client; CI=true yarn lint)
(cd client; CI=true yarn test)

12
.travis/coverage.sh Executable file
Просмотреть файл

@ -0,0 +1,12 @@
#!/usr/bin/env bash
min_coverage="${MIN_COVERAGE:-61}"
line_coverage="$((cd client; CI=true yarn coverage) | grep '^All files *|' | cut -d'|' -f5 | tr -d ' ' | cut -d'.' -f1)"
if [ ${line_coverage} -lt ${min_coverage} ]; then
echo "Got test coverage of ${line_coverage} which is less than configured minimum of ${min_coverage}" >&2
exit 1
else
echo "Got test coverage of ${line_coverage}, well done" >&2
exit 0
fi

84
.travis/push.sh Executable file
Просмотреть файл

@ -0,0 +1,84 @@
#!/usr/bin/env bash
set -euo pipefail
readonly GITHUB_ORG="${GITHUB_ORG:-Azure}"
readonly GITHUB_REPO="${GITHUB_REPO:-ibex-dashboard}"
readonly TARGET_BRANCH="${TARGET_BRANCH:-master}"
# readonly SOURCE_BRANCH="${SOURCE_BRANCH:-ibex-version-1.0}"
readonly AUTOCOMMIT_NAME="Travis CI"
readonly AUTOCOMMIT_EMAIL="travis@travis-ci.org"
readonly AUTOCOMMIT_BRANCH="temp"
log() {
echo "$@" >&2
}
ensure_preconditions_met() {
log "TRAVIS_BRANCH: ${TRAVIS_BRANCH}"
log "TRAVIS_PULL_REQUEST: ${TRAVIS_PULL_REQUEST}"
log "TRAVIS_PULL_REQUEST_BRANCH: ${TRAVIS_PULL_REQUEST_BRANCH}"
log "TRAVIS_COMMIT: ${TRAVIS_COMMIT}"
log "TRAVIS_COMMIT_MESSAGE: ${TRAVIS_COMMIT_MESSAGE}"
log "TRAVIS_COMMIT_RANGE: ${TRAVIS_COMMIT_RANGE}"
log "TRAVIS_BUILD_NUMBER: ${TRAVIS_BUILD_NUMBER}"
# get last commit comment
ORIGINAL_COMMIT_ID="$(echo ${TRAVIS_COMMIT_RANGE} | cut -d '.' -f4)"
log "ORIGINAL_COMMIT_ID: ${ORIGINAL_COMMIT_ID}"
ORIGINAL_COMMIT_MESSAGE=$(git log --format=%B -n 1 $ORIGINAL_COMMIT_ID)
log "ORIGINAL_COMMIT_MESSAGE: ${ORIGINAL_COMMIT_MESSAGE}"
# If last commit was by travis build, ignore and don't push
if [ "${ORIGINAL_COMMIT_MESSAGE}" == "Travis build: "* ]; then
log "Last commit by Travis CI - Ignoring and existing"
exit 0
fi
if [ -z "${TRAVIS_PULL_REQUEST_BRANCH}" ]; then
log "Job is CI for a push, skipping creation of production build"
exit 0
fi
# Only if push is to master branch, include a build
if [ "${TRAVIS_BRANCH}" != "${TARGET_BRANCH}" ]; then
log "Skipping creation of production build"
log "We only create production builds for pull requests to '${TARGET_BRANCH}'"
log "but this pull request is to '${TRAVIS_BRANCH}'"
exit 0
fi
if [ -z "${GITHUB_TOKEN}" ]; then
log "GITHUB_TOKEN not set: won't be able to push production build"
log "Please configure the token in .travis.yml or the Travis UI"
exit 1
fi
}
create_production_build() {
yarn build
}
setup_git() {
git config user.name "${AUTOCOMMIT_NAME}"
git config user.email "${AUTOCOMMIT_EMAIL}"
git remote add origin-travis "https://${GITHUB_TOKEN}@github.com/${GITHUB_ORG}/${GITHUB_REPO}.git"
}
commit_build_files() {
git checkout -b "${AUTOCOMMIT_BRANCH}"
git add --all -f build
echo -e "Travis build: ${TRAVIS_BUILD_NUMBER}\n\nhttps://travis-ci.org/${GITHUB_ORG}/${GITHUB_REPO}/builds/${TRAVIS_BUILD_ID}" | git commit --file -
}
push_to_github() {
git push origin-travis "${AUTOCOMMIT_BRANCH}:${TRAVIS_PULL_REQUEST_BRANCH}"
}
ensure_preconditions_met
create_production_build
setup_git
commit_build_files
push_to_github

4
.travis/setup.sh Executable file
Просмотреть файл

@ -0,0 +1,4 @@
#!/usr/bin/env bash
(cd server; yarn install)
(cd client; yarn install; yarn run css:build)

6
.vscode/settings.json поставляемый
Просмотреть файл

@ -1,7 +1,7 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.exclude": {
"**/*.css": { "when": "$(basename).scss"}
"**/*.css": true
},
"editor.tabSize": 2,
"editor.insertSpaces": true,
@ -14,5 +14,7 @@
"**/node_modules": true,
"**/build": true,
"**/coverage": true
}
},
"tslint.configFile": "client/tslint.json",
"tslint.nodePath": "client/node_modules"
}

33
Dockerfile Normal file
Просмотреть файл

@ -0,0 +1,33 @@
FROM node:8.2.1-alpine
# Create app directory
RUN mkdir -p /usr/src/app
RUN mkdir -p /usr/src/app/client
RUN mkdir -p /usr/src/app/server
WORKDIR /usr/src/app
# Install app dependencies
COPY package.json /usr/src/app/
COPY package-lock.json /usr/src/app/
COPY yarn.lock /usr/src/app/
COPY server/yarn.lock /usr/src/app/server
COPY server/package.json /usr/src/app/server
COPY server/package-lock.json /usr/src/app/server
COPY client/yarn.lock /usr/src/app/client
COPY client/package.json /usr/src/app/client
COPY client/package-lock.json /usr/src/app/client
RUN npm install yarn -g
RUN yarn
# Bundle app source
COPY . /usr/src/app
# Build client assets
WORKDIR /usr/src/app/client
RUN yarn build
WORKDIR /usr/src/app/
CMD [ "npm", "start" ]

21
LICENSE Normal file
Просмотреть файл

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Elad Iwanir
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

191
README.md
Просмотреть файл

@ -1,30 +1,112 @@
# Ibex Dashboard
This is an application insights based project that displays a bots analytics dashboard.
# Ibex Dashboard [![Build Status](https://travis-ci.org/Azure/ibex-dashboard.png?branch=master)](https://travis-ci.org/Azure/ibex-dashboard)
[Ibex](http://aka.ms/ibex) is a dashboarding application that enables building dashboard and templates.
It mainly supports **Application Insights** but data sources and visual components are easily extendable.
## Changes
### Version 1.3 (January 22, 2018)
Version 1.3 contains the following changes:
* Moving application insights queries from client to server
* Updated tests to answer some security risks presented by GitHub
* Updated tests to accommodate the new approach
* Added masking/unmasking of connection parameters (so that client side can only update API KEY but not see what it is)
* Fixed small bugs with Firefox rendering
### Version 1.2 (October 16, 2017)
Version 1.2 breaks the persitency paths of dashboard files and custom templates. If you are upgrading to this version, copy your private dashboards from `/dashboards` into `/dashboards/persistent/` as follows:
> Private Files: Move files from `/dashboards/*.private.js` to `/dashboards/persistent/private`.
> Custom Templates: Move files from `/dashboards/customTemplates/*.private.ts` to `/dashboards/persistent/customTemplates`.
# Preview
[![Preview](/docs/bot-framedash.png)](/docs/bot-framedash.png)
[![Preview](/docs/bot-framedash-msgs.png)](/docs/bot-framedash-msgs.png)
[![Preview](/docs/images/bot-fmk-dashboard.png)](/docs/images/bot-fmk-dashboard.png)
[![Preview](/docs/images/bot-fmk-dashboard-msgs.png)](/docs/images/bot-fmk-dashboard-msgs.png)
[![Preview](/docs/images/bot-fmk-dashboard-intent.png)](/docs/images/bot-fmk-dashboard-intent.png)
### Show With Your Own Data
# Installation
1. Clone
2. [Get an Application Insights App ID and Api Key](https://dev.applicationinsights.io/documentation/Authorization/API-key-and-App-ID)
```bash
npm install yarn -g
4. Run `npm run start:dev`
5. Open **http://localhost:3000/**
6. Run through setup and afterwards, fill in **API Key** and **Application ID**
git clone https://github.com/Azure/ibex-dashboard
cd ibex-dashboard
yarn
yarn start
```
## Deploy To Azure
### Using Bot Analytics Instrumented Dashboard
1. Open **http://localhost:4000**
2. Create a new template from **Bot Analytics Instrumented Dashboard**
3. Run through the **Application Insights** setup and fill in **API Key** and **Application ID** according to the application insights account associated with your registered bot.
### Installation on Ubuntu
Use the following to install yarn on Ubuntu:
```bash
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get update && sudo apt-get install yarn
```
# Development
```bash
yarn start:dev
```
Open **http://localhost:3000**
For contribution and code documentation follow:
[DEVELOPMENT & CONTRIBUTION](/docs/README.md).
# Deploy To Azure
There are 3 ways to deploy to Azure:
### 1. Web App - Automated
1. Fork this repo (to be able to automatically create github deployment key)
2. Copy the fork url and use it with the following deployment button:
<a href="https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Ftorosent%2Fibex-dashboard%2Fmaster%2Fscripts%2Fdeployment%2Fwebapp%2Fazuredeploy.json" target="_blank">
<img src="http://azuredeploy.net/deploybutton.png"/>
</a>
### 2. Web App On Linux - Automated with Docker Hub
<a href="https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Fibex-dashboard%2Fmaster%2Fscripts%2Fdeployment%2Fwebapponlinux%2Fazuredeploy.json" target="_blank">
<img src="http://azuredeploy.net/deploybutton.png"/>
</a>
### 3. Manual
1. Fork this repo (to be able to automatically create github deployment key)
2. Clone & Deploy:
3. [Create a new Web App in Azure](https://docs.microsoft.com/en-us/azure/app-service-web/app-service-continuous-deployment)
Since application insights API doesn't support ARM yet, we need to manually [create an API Key](https://dev.applicationinsights.io/documentation/Authorization/API-key-and-App-ID) for the application insights service.
Once you created the api key, copy and paste it into the **Dashboard settings screen**.
# Deploy With Docker
## Create new API Key and Application ID
1. `docker build -t **image name** .`
2. `docker run -d -e PORT=80 **image name** `
3. Docker image is also available at Docker Hub - `docker pull morshemesh/ibex-dashboard`
# Application Insights Integration
Since application insights API doesn't support ARM yet, we need to manually [create an API Key](https://dev.applicationinsights.io/documentation/Authorization/API-key-and-App-ID) for the application insights service.
The full instructions are also available when you create a new dashboard.
You can also follow the next headline.
### Create new API Key and Application ID
The following steps explain how to connect **Application Insights** bot with your bot and your dashboard:
[you can also follow the [official Application Insights article](https://dev.applicationinsights.io/documentation/Authorization/API-key-and-App-ID)].
@ -37,14 +119,52 @@ The following steps explain how to connect **Application Insights** bot with you
6. Open the URL of your web app
7. Under **AppId**/**ApiKey** set the values you created.
### Adding Application Insights instrumentation to your bot
- [Instrumentation for Node.js bots](https://github.com/Azure/botbuilder-instrumentation)
- [Instrumentation for C# bots](https://github.com/Azure/botbuilder-instrumentation-cs)
# Testing
The test watcher is integrated into the create-react-app mechanism and runs tests related to files changes since the last commit.
To run the test watcher in an interactive mode:
```bash
cd client
yarn test
```
Alternatively, you can also run the full commands that the Travis CI server
will run to validate any changes.
```bash
.travis/ci.sh
```
# Build
Our CI server Travis creates new production builds automatically for changes
to master. If you need to create a build locally, you can execute the same
commands as the CI server.
```bash
yarn build
```
Or
```bash
.travis/build.sh
```
# Resources
### Technologies In Use
* https://facebook.github.io/react/
* https://github.com/facebookincubator/create-react-app
* http://recharts.org/
* http://www.material-ui.com/
* https://react-md.mlaursen.com/
### Resources
### Design and Patterns
This project is built using:
* https://github.com/facebookincubator/create-react-app
@ -52,46 +172,15 @@ This project is built using:
The server approach was added using:
* https://www.fullstackreact.com/articles/using-create-react-app-with-a-server/
* https://github.com/fullstackreact/food-lookup-demo
* https://medium.com/@patriciolpezjuri/using-create-react-app-with-react-router-express-js-8fa658bf892d#.14dex6478
Thinking about integrating with:
* https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md
### Assumptions
1. Running node version 4.5 or above.
### Engines
### Installation
```bash
git clone https://github.com/CatalystCode/ibex-dashboard.git
cd ibex-dashboard
npm install
```
* Running node version 6.11 or above.
### Dev
```bash
npm run start:dev
```
### Test Watcher
Runs the test watcher in an interactive mode.
By default, runs tests related to files changes since the last commit.
```bash
npm test
```
### Build for Production
```bash
npm run build
```
## Whats Inside?
* [webpack](https://webpack.github.io/) with [webpack-dev-server](https://github.com/webpack/webpack-dev-server), [html-webpack-plugin](https://github.com/ampedandwired/html-webpack-plugin) and [style-loader](https://github.com/webpack/style-loader)
* [Babel](http://babeljs.io/) with ES6 and extensions used by Facebook (JSX, [object spread](https://github.com/sebmarkbage/ecmascript-rest-spread/commits/master), [class properties](https://github.com/jeffmo/es-class-public-fields))
* [Autoprefixer](https://github.com/postcss/autoprefixer)
* [ESLint](http://eslint.org/)
* [Jest](http://facebook.github.io/jest)
# License
MIT

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

@ -1,6 +1,6 @@
{
"main.css": "static/css/main.a7a824cb.css",
"main.css.map": "static/css/main.a7a824cb.css.map",
"main.js": "static/js/main.d9fdb0ac.js",
"main.js.map": "static/js/main.d9fdb0ac.js.map"
"main.css": "static/css/main.a4e6627c.css",
"main.css.map": "static/css/main.a4e6627c.css.map",
"main.js": "static/js/main.b9c79aba.js",
"main.js.map": "static/js/main.b9c79aba.js.map"
}

Двоичные данные
build/images/application-insights.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 7.7 KiB

Двоичные данные
build/images/azure.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 4.1 KiB

Двоичные данные
build/images/bot-ai-base.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 8.7 KiB

Двоичные данные
build/images/bot-ai-cs.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 16 KiB

Двоичные данные
build/images/bot-instrumented.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 19 KiB

Двоичные данные
build/images/default.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 36 KiB

Двоичные данные
build/images/human-to-bot-handoff.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 13 KiB

Двоичные данные
build/images/sample.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 7.4 KiB

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

@ -1 +1 @@
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="shortcut icon" href="/favicon.ico"><link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500"/><title>React App</title><link href="/static/css/main.a7a824cb.css" rel="stylesheet"></head><body><div id="root"></div><script type="text/javascript" src="/static/js/main.d9fdb0ac.js"></script></body></html>
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="shortcut icon" href="/favicon.ico"><link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500"/><title>React App</title><link href="/static/css/main.a4e6627c.css" rel="stylesheet"></head><body><div id="root"></div><script type="text/javascript" src="/static/js/main.b9c79aba.js"></script></body></html>

1
build/service-worker.js Normal file
Просмотреть файл

@ -0,0 +1 @@
"use strict";function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}var precacheConfig=[["/index.html","eaea5d349f488d4e9591c4d5a68b45ee"],["/static/css/main.a4e6627c.css","ecd21127029852aa6f1e4171b80d37c3"]],cacheName="sw-precache-v3-sw-precache-webpack-plugin-"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var n=new URL(e);return"/"===n.pathname.slice(-1)&&(n.pathname+=t),n.toString()},cleanResponse=function(e){return e.redirected?("body"in e?Promise.resolve(e.body):e.blob()).then(function(t){return new Response(t,{headers:e.headers,status:e.status,statusText:e.statusText})}):Promise.resolve(e)},createCacheKey=function(e,t,n,r){var a=new URL(e);return r&&a.pathname.match(r)||(a.search+=(a.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(n)),a.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var n=new URL(t).pathname;return e.some(function(e){return n.match(e)})},stripIgnoredUrlParameters=function(e,t){var n=new URL(e);return n.hash="",n.search=n.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(e){return t.every(function(t){return!t.test(e[0])})}).map(function(e){return e.join("=")}).join("&"),n.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],n=e[1],r=new URL(t,self.location),a=createCacheKey(r,hashParamName,n,/\.\w{8}\./);return[r.toString(),a]}));self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(e){return setOfCachedUrls(e).then(function(t){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(n){if(!t.has(n)){var r=new Request(n,{credentials:"same-origin"});return fetch(r).then(function(t){if(!t.ok)throw new Error("Request for "+n+" returned a response with status "+t.status);return cleanResponse(t).then(function(t){return e.put(n,t)})})}}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var t=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(e){return e.keys().then(function(n){return Promise.all(n.map(function(n){if(!t.has(n.url))return e.delete(n)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t,n=stripIgnoredUrlParameters(e.request.url,ignoreUrlParametersMatching);(t=urlsToCacheKeys.has(n))||(n=addDirectoryIndex(n,"index.html"),t=urlsToCacheKeys.has(n));!t&&"navigate"===e.request.mode&&isPathWhitelisted(["^(?!\\/__).*"],e.request.url)&&(n=new URL("/index.html",self.location).toString(),t=urlsToCacheKeys.has(n)),t&&e.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(n)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetch(e.request)}))}});

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -1 +0,0 @@
{"version":3,"sources":[],"names":[],"mappings":"","file":"static/css/main.a7a824cb.css","sourceRoot":""}

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

75
client/@types/ai.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,75 @@
interface IQueryResults {
Tables: IQueryResult[]
}
interface IQueryResult {
TableName: string,
Columns: {
ColumnName: string,
DataType: string,
ColumnType: string
}[],
Rows: any[][]
}
interface IQueryStatus {
Ordinal: number,
Kind: string,
Name: string,
Id: string
}
/**
* ====================================
* Template definitions
* ====================================
*/
/**
* Application Insights data source definition
*/
interface AIDataSource extends IDataSource {
type: 'ApplicationInsights/Query',
dependencies: {
/**
* Required - to use in all queries to app insights as a basic timespan parameter
*/
queryTimespan: string,
/**
* Optional - from which 'queryTimespan' is derived
*/
timespan?: string,
/**
* Used for queries that require granularity (like timeline)
*/
granularity?: string
},
params: AIQueryParams | AIForkedQueryParams;
}
/**
* A simple query on application insights data source
*/
interface AIQueryParams {
query: AIQuery,
mappings?: AIMapping
}
/**
* A forked query that aggregates several queries into a single API call
*/
interface AIForkedQueryParams {
/**
* The table on which to perform the forken query
*/
table: string,
queries: IDict<{
query: AIQuery,
mappings?: AIMapping,
filters?: Array<IStringDictionary>,
calculated?: (state: any, dependencies?: any, prevState?: any) => any
}>,
}
type AIQuery = string | (() => string) | ((dependencies: any) => string);
type AIMapping = IDict<(value: any, row: any, idx: number) => string | number | boolean>;

39
client/@types/botframework.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,39 @@
interface IQueryResult {
value: string
}
/**
* ====================================
* Template definitions
* ====================================
*/
/**
* Application Insights data source definition
*/
interface BotFrameworkDataSource extends IDataSource {
type: 'BotFramework/DirectLine',
dependencies: {
/**
* Required - to use in all queries to app insights as a basic timespan parameter
*/
queryTimespan: string,
/**
* Optional - from which 'queryTimespan' is derived
*/
timespan?: string,
/**
* Used for queries that require granularity (like timeline)
*/
granularity?: string
},
params: BotFrameworkQueryParams;
}
/**
* A simple query on application insights data source
*/
interface BotFrameworkQueryParams {
}
type BotFrameworkQuery = string | (() => string) | ((dependencies: any) => string);

16
client/@types/constant.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,16 @@
/**
* A data source to hold unchanging data
*/
interface ConstantDataSource extends IDataSource {
type: 'Constant',
params: {
/**
* List of values to choose from
*/
values: any[],
/**
* Current selected value (usually used in constant filters)
*/
selectedValue: string
}
}

39
client/@types/cosmosdb.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,39 @@
interface IQueryResult {
value: string
}
/**
* ====================================
* Template definitions
* ====================================
*/
/**
* Application Insights data source definition
*/
interface CosmosDBDataSource extends IDataSource {
type: 'CosmosDB/Query',
dependencies: {
/**
* Required - to use in all queries to app insights as a basic timespan parameter
*/
queryTimespan: string,
/**
* Optional - from which 'queryTimespan' is derived
*/
timespan?: string,
/**
* Used for queries that require granularity (like timeline)
*/
granularity?: string
},
params: CosmosDBQueryParams;
}
/**
* A simple query on application insights data source
*/
interface CosmosDBQueryParams {
}
type CosmosDBQuery = string | (() => string) | ((dependencies: any) => string);

18
client/@types/sample.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,18 @@
/**
* A data source to hold unchanging data
*/
interface SampleDataSource extends IDataSource {
type: 'Sample',
params?: {
/**
* All values in this dictionary will be available as dependencies from this data source
*/
samples: {
[key: string]: any,
/**
* List of sample values
*/
values?: any[],
}
}
}

309
client/@types/types.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,309 @@
/// <reference path="./constant.d.ts"/>
/// <reference path="./ai.d.ts"/>
/// <reference path="./sample.d.ts"/>
type IDict<T> = { [id: string]: T };
type IDictionary = IDict<any>;
type IStringDictionary = IDict<string>;
type IConnection = IStringDictionary;
type IConnections = IDict<IConnection>;
/**
* Dashboard configuration schema
*/
interface IDashboardConfig extends IDataSourceContainer, IElementsContainer {
/**
* Unique id for the template to be used when looking up a dashboard
*/
id: string,
/**
* Name to be displayed in navigation
*/
name: string,
/**
* Icon to be displayed in navigation (see https://material.io/icons/)
* Optional [Default: 'dashboard']
*/
icon?: string,
/**
* An image logo to be displayed next to the title of the dashboard in the navigation header.
* Optional [Default: None]
*/
logo?: string,
/**
* The string to be used in the url (should be identical to id)
*/
url: string,
/**
* A short description to display under the template in the dashboard creation screen
*/
description?: string,
/**
* An html full description to be displayed in a dialog in the dashboard creation screen
*/
html?: string,
/**
* A preview image to show on the background of the template in the dashboard creation screen
*/
preview?: string,
/**
* The category to put this template in the dashboard creation screen
*/
category?: string,
/**
* A flag indicates whether the template is featured at the top of the dashboard creation screen
*/
featured?: boolean,
/**
* Configuration relevant for the current dashboard
*/
config: {
/**
* A dictionary of connection parameters.
* connections: {
* "application-insights": { appId: "123456", apiKey: "123456" },
* "cosmos-db": { ... }
* }
*/
connections: IConnections,
/**
* react-grid-layout properties.
* To read more, see https://github.com/STRML/react-grid-layout#grid-layout-props
*/
layout: {
isDraggable?: boolean,
isResizable?: boolean,
rowHeight?: number,
verticalCompact?: boolean, // Turns off compaction so you can place items wherever.
cols: Sizes<number>,
breakpoints: Sizes<number>
}
},
filters: IFilter[]
dialogs: IDialog[],
layouts?: ILayouts,
}
/**
* =============================
* Data Sources
* =============================
*/
type DataSource = ConstantDataSource | AIDataSource | SampleDataSource | BotFrameworkDataSource | CosmosDBDataSource;
/**
* Data Source properties in a dashboard definition
*/
interface IDataSource {
/**
* The name/type of the data source - should be in the data source `type` property
*/
type: string,
/**
* id for the data source - used to associate to this data source as a dependency
*/
id: string,
/**
* Dependencies required for this data source to work (Optional).
*
* Format:
* dependencies: {
* "timespan": "data_source_id" // This will use the default property of the data source
* "timespan2": "data_source_id:data_source_property"
* }
*/
dependencies?: IStringDictionary,
/**
* Parameters required / optional by the specific data source (defined differently or each data source)
*/
params?: IDictionary,
/**
* The format to use for transforming the data into a visual component
*/
format?: string | {
type: string,
args?: IDictionary
}
/**
* An optional method to apply additional transformations to the data returned from this data source.
* The data returned will augment the data source's data and will be usable by external dependencies.
*
* {
* id: "dataSource1",
* ...,
* calculated: (state, dependencies, prevState) => {
* return {
* "more-data": [],
* "more-data2": 4
* };
* }
* }
*
* Then, in another data source:
* dependencies: {
* values: "dataSource1:more-data"
* }
*/
calculated?: (state: any, dependencies?: any, prevState?: any) => IDictionary
}
/**
* An element that can hold one or multiple data sourecs
*/
interface IDataSourceContainer {
dataSources: DataSource[]
}
/**
* ====================================
* Layouts definitions
* ====================================
*/
interface Sizes<T> {
lg?: T,
md?: T,
sm?: T,
xs?: T,
xxs?: T
}
interface ILayout {
"i": string,
"x": number,
"y": number,
"w": number,
"h": number,
minW: number,
maxW: number,
minH: number,
maxH: number,
moved: boolean,
static: boolean,
isDraggable: boolean,
isResizable: boolean
}
type ILayouts = Sizes<ILayout[]>;
/**
* ====================================
* Template schema definitions
* ====================================
*/
interface IElement {
/**
* Unique Id used to locate and save the location of an element on a dashboard
*/
id: string;
/**
* The name of the element type to be used
* For a complete list follow:
* https://github.com/Azure/ibex-dashboard/tree/master/docs#elements-plugins
*/
type: string;
/**
* How many units (width/height) should this element take.
* This property is overriden when the user plays with the layout in edit mode.
*/
size: { w: number, h: number };
/**
* This property can usually be used when wanting to pull an element "up" in a dashboard.
* For the following sizes (Width x Height) the last 4 columns have a vacancy of 6 rows:
* 4x8, 4x8, 4,2
*
* The last element on the next line can "pull up" by definition { x: 8, y: 2 }
*/
location?: { x: number, y: number };
/**
* Title to display at the top of the element on the dashboard
*/
title?: string;
/**
* A subtitle to display on a tooltip
*/
subtitle?: string;
/**
* An array of colors to override the default array supplied by the dashboard.
*/
theme?: string[];
/**
* Use this property on a source that uses a "format" attribute.
* Whenever the data source is updated, the data in this element will also update.
*
* Example:
* dataSource: {
* id: 'dataSource1',
* ...
* format: 'pie'
* }
*
* {
* id: 'element1',
* type: 'PieData',
* source: 'dataSource1',
* ...
* }
*/
source?: string | IStringDictionary;
/**
* If you don't supply a 'source' attribute, or want to override one of the attribute,
* You can use 'dependencies' to populate a property with data.
* Whenever the data source is updated, the data in this element will also update.
*
* Example:
* {
* values: 'dataSourceX:values'
* }
*/
dependencies?: IStringDictionary;
/**
* Use to define Element Type specific properties (like showLedged, compact, etc...)
*/
props?: IDictionary;
/**
* Define what should happen on certain actions like 'onclick'.
*/
actions?: IDictionary;
}
interface IFilter {
type: string,
source?: string,
dependencies?: IStringDictionary,
actions?: IStringDictionary,
title?: string,
subtitle?: string,
icon?: string,
first?: boolean
}
interface IElementsContainer {
elements: IElement[]
}
interface IDialog extends IDataSourceContainer, IElementsContainer {
id: string
width?: string | number
params: string[]
}
type IAction = string | {
action: string,
params: IStringDictionary
}
interface ISetupConfig {
stage: string;
admins: string[];
enableAuthentication: boolean;
allowHttp: boolean;
redirectUrl: string;
clientID: string;
clientSecret: string;
issuer: string;
}

67
client/package.json Normal file
Просмотреть файл

@ -0,0 +1,67 @@
{
"name": "ibex-dashboard-client",
"version": "0.1.0",
"private": true,
"proxy": "http://localhost:4000/",
"devDependencies": {
"@types/alt": "^0.16.32",
"@types/jest": "^19.2.2",
"@types/lodash": "^4.14.55",
"@types/nock": "^8.2.1",
"@types/node": "^7.0.8",
"@types/react": "^15.0.16",
"@types/react-addons-test-utils": "^0.14.17",
"@types/react-dom": "^0.14.23",
"@types/react-router": "^3.0.8",
"alt": "^0.18.6",
"alt-utils": "^1.0.0",
"leaflet": "^1.2.0",
"leaflet-geosearch": "^2.6.0",
"leaflet.markercluster": "^1.2.0",
"nock": "^9.0.9",
"node-sass": "^4.5.3",
"npm-run-all": "^4.0.2",
"react": "^15.4.2",
"react-addons-css-transition-group": "^15.4.2",
"react-addons-test-utils": "^15.4.2",
"react-addons-transition-group": "^15.4.2",
"react-dom": "^15.4.2",
"react-grid-layout": "^0.14.7",
"react-leaflet": "^1.7.8",
"react-leaflet-div-icon": "^1.1.0",
"react-leaflet-markercluster": "^1.1.8",
"react-md": "^1.0.18",
"react-render-html": "^0.1.6",
"react-router": "3.0.0",
"react-scripts-ts": "^2.6.0",
"recharts": "^0.21.2",
"tslint": "^5.8.0"
},
"dependencies": {
"body-parser": "^1.17.1",
"cookie-parser": "^1.4.3",
"express": "^4.15.2",
"express-session": "^1.15.2",
"lodash": "^4.17.4",
"lodash.throttle": "^4.1.1",
"material-colors": "^1.2.5",
"moment": "^2.18.0",
"morgan": "^1.8.1",
"ms-rest-azure": "^2.1.2",
"passport": "^0.3.2",
"passport-azure-ad": "^3.0.5",
"react-ace": "^5.0.1",
"react-json-tree": "^0.10.9",
"xhr-request": "^1.0.1"
},
"scripts": {
"css:build": "node-sass src/ -o src/",
"css:watch": "yarn run css:build && node-sass src/ -o src/ --watch --recursive",
"client:start": "react-scripts-ts start",
"start": "npm-run-all -p css:watch client:start",
"coverage": "react-scripts-ts test --env=jsdom --coverage --collectCoverageFrom=src/**/*.{ts,tsx} --collectCoverageFrom=!src/**/*.d.ts --collectCoverageFrom=!src/tests/**",
"lint": "tslint src",
"build": "yarn run css:build && react-scripts-ts build && rm -rf ../build/static && cp -rf build ../",
"test": "react-scripts-ts test --env=jsdom"
}
}

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

До

Ширина:  |  Высота:  |  Размер: 9.9 KiB

После

Ширина:  |  Высота:  |  Размер: 9.9 KiB

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

До

Ширина:  |  Высота:  |  Размер: 117 KiB

После

Ширина:  |  Высота:  |  Размер: 117 KiB

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

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

@ -1,5 +1,7 @@
import alt, { AbstractActions } from '../alt';
import * as request from 'xhr-request';
import { ToastActions } from '../components/Toast';
import utils from '../utils';
interface IAccountActions {
failure(error: any): any;
@ -7,17 +9,14 @@ interface IAccountActions {
}
class AccountActions extends AbstractActions implements IAccountActions {
constructor(alt: AltJS.Alt) {
super(alt);
}
updateAccount() {
return (dispatcher: (account: IDictionary) => void) => {
request('/auth/account', { json: true }, (error: any, result: any) => {
if (error) {
return this.failure(error);
if (error || result && result.error) {
return this.failure(error || result && result.error);
}
return dispatcher({ account: result.account });
}
@ -27,6 +26,7 @@ class AccountActions extends AbstractActions implements IAccountActions {
}
failure(error: any) {
ToastActions.addToast({ text: utils.errorToMessage(error) });
return { error };
}
}

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

@ -0,0 +1,234 @@
import alt, { AbstractActions } from '../alt';
import * as request from 'xhr-request';
import { ToastActions } from '../components/Toast';
import utils from '../utils';
interface IConfigurationsActions {
loadConfiguration(): any;
loadDashboard(id: string): any;
loadDashboardComplete(dashboard: IDashboardConfig): { dashboard: IDashboardConfig };
createDashboard(dashboard: IDashboardConfig): any;
loadTemplate(id: string): any;
saveConfiguration(dashboard: IDashboardConfig): any;
failure(error: any): any;
submitDashboardFile(content: string, fileName: string): void;
convertDashboardToString(dashboard: IDashboardConfig): string;
deleteDashboard(id: string): any;
saveAsTemplate(template: IDashboardConfig): any;
}
class ConfigurationsActions extends AbstractActions implements IConfigurationsActions {
submitDashboardFile(content: string, dashboardId: string) {
return (dispatcher: (json: any) => void) => {
// Replace both 'id' and 'url' with the requested id from the user
const idRegExPattern = /id: \".*\",/i;
const urlRegExPatternt = /url: \".*\",/i;
const updatedContent =
content.replace(idRegExPattern, 'id: \"' + dashboardId + '\",')
.replace(urlRegExPatternt, 'url: \"' + dashboardId + '\",');
request(
'/api/dashboards/' + dashboardId,
{
method: 'PUT',
json: true,
body: { script: updatedContent }
},
(error: any, json: any) => {
if (error || (json && json.errors)) {
return this.failure(error || json.errors);
}
// redirect to the newly imported dashboard
window.location.replace('dashboard/' + dashboardId);
return dispatcher(json);
}
);
};
}
loadConfiguration() {
return (dispatcher: (result: { dashboards: IDashboardConfig[], templates: IDashboardConfig[] }) => void) => {
this.getScript('/api/dashboards', () => {
let dashboards: IDashboardConfig[] = (window as any)['dashboardDefinitions'];
let templates: IDashboardConfig[] = (window as any)['dashboardTemplates'];
if ((!dashboards || !dashboards.length) && (!templates || !templates.length)) {
return this.failure(new Error('Could not load configuration'));
}
return dispatcher({ dashboards, templates });
});
};
}
loadDashboard(id: string) {
return (dispatcher: () => void) => {
(window as any)['dashboard'] = undefined;
this.getScript('/api/dashboards/' + id, () => {
let dashboard: IDashboardConfig = (window as any)['dashboard'];
if (!dashboard) {
return this.failure(new Error('Could not load configuration for dashboard ' + id));
}
this.loadDashboardComplete(dashboard);
return dispatcher();
});
};
}
loadDashboardComplete(dashboard: IDashboardConfig): { dashboard: IDashboardConfig } {
return { dashboard };
}
createDashboard(dashboard: IDashboardConfig) {
return (dispatcher: (dashboard: IDashboardConfig) => void) => {
let script = utils.objectToString(dashboard);
request('/api/dashboards/' + dashboard.id, {
method: 'PUT',
json: true,
body: { script: 'return ' + script }
},
(error: any, json: any) => {
if (error || (json && (json.error || json.errors))) {
return this.failure(error || json);
}
return dispatcher(json);
}
);
};
}
loadTemplate(id: string) {
return (dispatcher: (result: { template: IDashboardConfig }) => void) => {
(window as any)['template'] = undefined;
this.getScript('/api/templates/' + id, () => {
let template: IDashboardConfig = (window as any)['template'];
if (!template) {
return this.failure(new Error('Could not load configuration for template ' + id));
}
return dispatcher({ template });
});
};
}
saveAsTemplate(template: IDashboardConfig) {
return (dispatcher: (result: { template: IDashboardConfig }) => void) => {
let script = utils.objectToString(template);
script = '/// <reference path="../../../client/@types/types.d.ts"/>\n' +
'import * as _ from \'lodash\';\n\n' +
'export const config: IDashboardConfig = /*return*/ ' + script;
return request(
'/api/templates/' + template.id,
{
method: 'PUT',
json: true,
body: { script: script }
},
(error: any, json: any) => {
if (error || (json && json.errors)) {
return this.failure(error || json.errors);
}
return dispatcher(json);
}
);
};
}
saveConfiguration(dashboard: IDashboardConfig) {
return (dispatcher: (dashboard: IDashboardConfig) => void) => {
let stringDashboard = utils.objectToString(dashboard);
request('/api/dashboards/' + dashboard.id, {
method: 'POST',
json: true,
body: { script: 'return ' + stringDashboard }
},
(error: any, json: any) => {
if (error) {
return this.failure(error);
}
// Request a reload of the configuration
this.loadDashboard(dashboard.id);
return dispatcher(json);
}
);
};
}
failure(error: any) {
ToastActions.showText(JSON.stringify(error || 'There was an error'));
return error;
}
deleteDashboard(id: string) {
return (dispatcher: (result: any) => any) => {
request('/api/dashboards/' + id, {
method: 'DELETE',
json: true
},
(error: any, json: any) => {
if (error || (json && json.errors)) {
return this.failure(error || json.errors);
}
return dispatcher(json.ok);
}
);
};
}
convertDashboardToString(dashboard: IDashboardConfig) {
return utils.convertDashboardToString(dashboard);
}
private getScript(source: string, callback?: () => void): boolean {
let script: any = document.createElement('script');
let prior = document.getElementsByTagName('script')[0];
script.async = 1;
if (prior) {
prior.parentNode.insertBefore(script, prior);
} else {
document.getElementsByTagName('body')[0].appendChild(script);
}
script.onload = script.onreadystatechange = (_, isAbort) => {
if (isAbort || !script.readyState || /loaded|complete/.test(script.readyState) ) {
script.onload = script.onreadystatechange = null;
script = undefined;
if (!isAbort) { if (callback) { callback(); } }
}
};
script.src = source;
return true;
}
}
const configurationsActions = alt.createActions<IConfigurationsActions>(ConfigurationsActions);
export default configurationsActions;

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

@ -5,9 +5,6 @@ interface IConnectionsActions {
}
class ConnectionsActions extends AbstractActions implements IConnectionsActions {
constructor(alt: AltJS.Alt) {
super(alt);
}
updateConnection(connectionName: string, args: IDictionary) {
return { connectionName, args };

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

@ -6,9 +6,6 @@ interface ISettingsActions {
}
class SettingsActions extends AbstractActions implements ISettingsActions {
constructor(alt: AltJS.Alt) {
super(alt);
}
saveSettings() {
return { };

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

@ -1,6 +1,6 @@
import alt, { AbstractActions } from '../alt';
import * as request from 'xhr-request';
import {IToast, ToastActions} from '../components/Toast';
import { IToast, ToastActions } from '../components/Toast';
interface ISetupActions {
load(): any;
@ -9,9 +9,6 @@ interface ISetupActions {
}
class SetupActions extends AbstractActions implements ISetupActions {
constructor(alt: AltJS.Alt) {
super(alt);
}
load() {

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

@ -7,9 +7,6 @@ interface IVisibilityActions {
}
class VisibilityActions extends AbstractActions implements IVisibilityActions {
constructor(alt: AltJS.Alt) {
super(alt);
}
setFlags(flags: IDict<boolean>): any {
return flags;

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

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

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

@ -0,0 +1,66 @@
import * as request from 'xhr-request';
import * as React from 'react';
import * as _ from 'lodash';
import RefreshStore, { IRefreshStoreState } from './RefreshStore';
import SelectField from 'react-md/lib/SelectFields';
import RefreshActions from './RefreshActions';
import { DataSourceConnector, IDataSourceDictionary, IDataSource } from '../../data-sources/DataSourceConnector';
interface IRefreshState extends IRefreshStoreState {
refreshMenuVisible?: boolean;
}
export default class AutoRefreshSelector extends React.Component<any, IRefreshState> {
oneSecInMs = 1000;
oneMinInMs = 60 * this.oneSecInMs;
refreshIntervals = [
{text: 'None', intervalMs: -1},
{text: '30 Sec', intervalMs: 30 * this.oneSecInMs},
{text: '60 Sec', intervalMs: 60 * this.oneSecInMs},
{text: '90 Sec', intervalMs: 90 * this.oneSecInMs},
{text: '2 Min', intervalMs: 2 * this.oneMinInMs},
{text: '5 Min', intervalMs: 5 * this.oneMinInMs},
{text: '15 Min', intervalMs: 15 * this.oneMinInMs},
{text: '30 Min', intervalMs: 30 * this.oneMinInMs},
];
constructor(props: any) {
super(props);
this.state = RefreshStore.getState();
this.handleRefreshIntervalChange = this.handleRefreshIntervalChange.bind(this);
}
handleRefreshIntervalChange = (refreshInterval: string) => {
var oneSec = 1000;
var interval = this.refreshIntervals.find((x) => { return x.text === refreshInterval; }).intervalMs;
RefreshActions.updateInterval(interval);
RefreshActions.setRefreshTimer(
interval,
DataSourceConnector.refreshDs);
}
render () {
let refreshDropDownTexts = this.refreshIntervals.map((x) => { return x.text; });
return (
<SelectField
id="autorefresh"
label="Auto Refresh"
placeholder="0"
defaultValue={'None'}
position={SelectField.Positions.BELOW}
menuItems={refreshDropDownTexts}
toolbar={false}
onChange={this.handleRefreshIntervalChange}
className="md-select-field--toolbar"
/>
);
}
}

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

@ -0,0 +1,48 @@
import alt, { AbstractActions } from '../../alt';
import * as request from 'xhr-request';
interface IRefreshActions {
updateInterval(newInterval: number): { refreshInterval: number };
setRefreshTimer(newInterval: any, cb: any): void;
}
class RefreshActions extends AbstractActions implements IRefreshActions {
private runningRefreshInterval: any;
constructor(altobj: AltJS.Alt) {
super(altobj);
this.setRefreshTimer = this.setRefreshTimer.bind(this);
}
updateInterval(newInterval: number) {
return { refreshInterval: newInterval };
}
setRefreshTimer(newInterval: any, cb: any) {
return (dispatch) => {
// clear any previously scheduled interval
if (this.runningRefreshInterval) {
clearInterval(this.runningRefreshInterval);
this.runningRefreshInterval = null;
}
if (!newInterval || newInterval === -1) {
// don't auto refresh
return;
}
// setup a new interval
var interval = setInterval(
cb,
newInterval);
this.runningRefreshInterval = interval;
};
}
}
const refreshActions = alt.createActions<IRefreshActions>(RefreshActions);
export default refreshActions;

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

@ -0,0 +1,29 @@
import alt, { AbstractStoreModel } from '../../alt';
import refreshActions from './RefreshActions';
export interface IRefreshStoreState {
refreshInterval: number;
}
class RefreshStore extends AbstractStoreModel<IRefreshStoreState> implements IRefreshStoreState {
refreshInterval: number;
constructor() {
super();
this.bindListeners({
updateInterval: refreshActions.updateInterval
});
}
updateInterval(state: any) {
this.refreshInterval = state.refreshInterval;
}
}
const refreshStore = alt.createStore<IRefreshStoreState>(RefreshStore as AltJS.StoreModel<any>, 'RefreshStore');
export default refreshStore;

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

@ -0,0 +1,9 @@
import AutoRefreshSelector from './AutoRefreshSelector';
import RefreshActions from './RefreshActions';
import RefreshStore from './RefreshStore';
export {
AutoRefreshSelector,
RefreshActions,
RefreshStore
};

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

@ -0,0 +1,127 @@
import * as React from 'react';
import { Media } from 'react-md/lib/Media';
import { Card as MDCard, CardTitle } from 'react-md/lib/Cards';
import TooltipFontIcon from '../Tooltip/TooltipFontIcon';
import Button from 'react-md/lib/Buttons';
import { Settings, SettingsActions } from './Settings';
import { SpinnerActions } from '../Spinner';
const styles = {
noTitle: {
margin: 0,
padding: 0,
background: 'transparent',
} as React.CSSProperties,
noTitleContent: {
margin: 0,
padding: 0,
} as React.CSSProperties
};
interface ICardProps {
id?: string;
title?: string;
subtitle?: string;
widgets?: React.ReactNode;
className?: string;
style?: React.CSSProperties;
titleStyle?: React.CSSProperties;
contentStyle?: React.CSSProperties;
hideTitle?: boolean;
}
interface ICardState {
hover: boolean;
}
export default class Card extends React.PureComponent<ICardProps, ICardState> {
static defaultProps = {
hideTitle: false,
};
state = {
hover: false,
};
constructor(props: ICardProps) {
super(props);
}
render() {
const { id, title, subtitle, children, className, style, titleStyle, contentStyle, hideTitle } = this.props;
const { hover } = this.state;
let elements: React.ReactNode[] = [];
if (title && !hideTitle) {
elements.push(
<span key={0}>{title}</span>
);
}
if (subtitle) {
elements.push(
<TooltipFontIcon
key={1}
tooltipLabel={subtitle}
tooltipPosition="top"
forceIconFontSize={true}
forceIconSize={16}
className="card-icon"
>
info
</TooltipFontIcon>
);
}
if (hover) {
elements.push( this.renderWidgets() );
}
// NB: Fix for Card scroll content when no title
let cardTitleStyle = titleStyle || {};
let cardContentStyle = contentStyle || {};
if (hideTitle) {
Object.assign(cardTitleStyle, styles.noTitle);
Object.assign(cardContentStyle, styles.noTitleContent);
}
return (
<MDCard
onMouseOver={() => this.setState({ hover: true })}
onMouseLeave={() => this.setState({ hover: false })}
className={className}
style={style}>
<CardTitle title="" subtitle={elements} style={cardTitleStyle} />
<Media style={cardContentStyle}>
{children}
</Media>
</MDCard>
);
}
renderWidgets() {
const { id, title, widgets } = this.props;
const settingsButton = (
<Button
icon
key="settings"
onClick={() => SettingsActions.openDialog(title, id)}
className="card-settings-btn"
>
expand_more
</Button>
);
return !widgets ? (
<div className="card-settings" key="widgets">
{settingsButton}
</div>
) : (
<div className="card-settings" key="widgets">
<span>{widgets}</span>
<span>{settingsButton}</span>
</div>
);
}
}

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

@ -0,0 +1,37 @@
import alt, { AbstractActions } from '../../../alt';
interface ICardSettingsActions {
openDialog(title: string, elementId: string): IDict<string>;
closeDialog(): any;
selectIndex(index: number): number;
getExportData(dashboard: IDashboardConfig): IDashboardConfig;
downloadData(): void;
}
class CardSettingsActions extends AbstractActions implements ICardSettingsActions {
openDialog(title: string, elementId: string) {
return {title, elementId};
}
closeDialog() {
return {};
}
selectIndex(index: number) {
return index;
}
getExportData(dashboard: IDashboardConfig) {
return dashboard;
}
downloadData() {
return {};
}
}
const cardSettingsActions = alt.createActions<ICardSettingsActions>(CardSettingsActions);
export default cardSettingsActions;

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

@ -0,0 +1,314 @@
import * as React from 'react';
import alt, { AbstractStoreModel } from '../../../alt';
import { ToastActions } from '../../Toast';
import { DataSourceConnector, IDataSourceDictionary, IDataSource } from '../../../data-sources/DataSourceConnector';
import cardSettingsActions from './CardSettingsActions';
import { downloadBlob } from '../../Dashboard/DownloadFile';
export interface IExportData {
id: string;
data: any;
isJSON: boolean;
query: string;
group: string;
isGroupedJSON: boolean;
}
interface ICardSettingsStoreState {
visible: boolean;
title: string;
elementId: string;
dashboard?: IDashboardConfig;
selectedIndex: number;
exportData?: IExportData[];
result?: string;
}
class CardSettingsStore extends AbstractStoreModel<ICardSettingsStoreState> implements ICardSettingsStoreState {
visible: boolean;
title: string;
elementId: string;
dashboard?: IDashboardConfig;
selectedIndex: number;
exportData?: IExportData[];
result?: string;
constructor() {
super();
this.visible = false;
this.selectedIndex = 0;
this.bindListeners({
openDialog: cardSettingsActions.openDialog,
closeDialog: cardSettingsActions.closeDialog,
selectIndex: cardSettingsActions.selectIndex,
getExportData: cardSettingsActions.getExportData,
downloadData: cardSettingsActions.downloadData,
});
}
openDialog(state: IDict<string>) {
this.title = state.title;
this.elementId = state.elementId;
this.visible = true;
}
closeDialog() {
this.visible = false;
this.title = '';
this.exportData = null;
}
selectIndex(index: number) {
this.selectedIndex = index;
}
downloadData() {
const selected = this.exportData[this.selectedIndex];
if (!selected) {
return;
}
const text = selected.isJSON ? JSON.stringify(selected.data) : selected.data.toString();
const filename = selected.id + '.json';
downloadBlob(text, 'application/json', filename);
}
getExportData(dashboard: IDashboardConfig) {
if (!this.elementId) {
ToastActions.showText('Requires element "id" prop: ' + this.elementId);
return;
}
const matches = this.elementId.split('@');
if (matches.length !== 2) {
ToastActions.showText('Element index not found: ' + this.elementId);
return;
}
const id = matches[0];
const index = parseInt(matches[1], 10);
let elements = dashboard.elements;
if (isNaN(index) || index >= elements.length || index < 0) {
ToastActions.showText('Element index invalid value: ' + index);
return;
}
if (elements[index].id === id) {
this.getElement(elements, index);
return;
}
// handle dialog element
dashboard.dialogs.every(dialog => {
if (dialog.elements.length > index && dialog.elements[index].id === id) {
elements = dialog.elements;
this.getElement(elements, index);
return false;
} else {
return true;
}
});
}
private getElement(elements: IElement[], index: number) {
const element: IElement = elements[index];
this.exportData = this.extrapolateElementExportData(element.dependencies, element.source, element.id);
this.selectedIndex = 0; // resets dialog menu selection
}
private extrapolateElementExportData(elementDependencies: IStringDictionary,
sources: string | IStringDictionary,
elementId: string): IExportData[] {
let result: IExportData[] = [];
let dependencies = {};
Object.assign(dependencies, elementDependencies);
if (typeof sources === 'string') {
let source = {};
source[elementId] = sources;
Object.assign(dependencies, source);
} else if (sources && typeof sources === 'object' && Object.keys(sources).length > 0 ) {
Object.assign(dependencies, sources);
}
if (!dependencies || Object.keys(dependencies).length === 0 ) {
ToastActions.showText('Missing element dependencies');
return result;
}
const datasources = DataSourceConnector.getDataSources();
Object.keys(dependencies).forEach((id) => {
const dependency = dependencies[id];
let dependencySource = dependency;
let dependencyProperty = 'values';
const dependencyPath = dependency.split(':');
if (dependencyPath.length === 2) {
dependencySource = dependencyPath[0];
dependencyProperty = dependencyPath[1];
}
const datasource = Object.keys(datasources).find(key => dependencySource === key);
if (!datasource) {
return;
}
if (datasource === 'mode' || datasource === 'modes') {
return;
}
// Data (JSON)
let data: any;
let isJSON = false;
let isGroupedJSON = false;
let group = datasource;
const values = datasources[datasource].store.state[dependencyProperty];
if (values === null || typeof values === undefined) {
ToastActions.showText('Missing data: ' + datasource + ' ' + dependency);
return;
}
if (typeof values === 'object' || Array.isArray(values)) {
isJSON = true;
}
data = values;
// Query
let queryFn, queryFilters;
let queryId = dependencyProperty;
const forkedQueryComponents = dependencyProperty.split('-');
const params = datasources[datasource].config.params;
const isForked = params && !params.query && !!params.table;
if (!isForked) {
// unforked
queryFn = params && params.query || 'n/a';
} else {
// forked
if (!params.queries[queryId] && forkedQueryComponents.length === 2) {
queryId = forkedQueryComponents[0];
group = queryId;
}
if (params.queries[dependencySource]) {
queryId = dependencySource; // dialog case
}
if (!params.queries[queryId]) {
ToastActions.showText(`Unable to locate query id '${queryId}' in datasource '${dependencySource}'.`);
return;
}
queryFn = params.queries[queryId].query;
queryFilters = params.queries[queryId].filters;
}
// Query dependencies
let query, filter = '';
let queryDependencies: IDict<any> = {};
if (typeof queryFn === 'function') {
// Get query function dependencies
const queryDependenciesDict = datasources[datasource].config.dependencies || {};
Object.keys(queryDependenciesDict).forEach((dependenciesKey) => {
const value = queryDependenciesDict[dependenciesKey];
const path = value.split(':');
let source = value;
let property;
if (path.length === 2) {
source = path[0];
property = path[1];
}
if (source.startsWith('::') || source === 'connection') {
return;
}
if (source === 'args') {
const args = datasources[datasource].plugin['lastArgs'];
const arg = Object.keys(args).find(key => property === key);
if (!arg) {
ToastActions.showText('Unable to find arg property: ' + property);
return;
}
const argValue = args[arg] || '';
let append = {};
append[property] = argValue;
Object.assign(queryDependencies, append);
} else {
const datasourceId = Object.keys(datasources).find(key => source === key);
if (!datasourceId) {
ToastActions.showText('Unable to find data source id: ' + source);
return;
}
const resolvedValues = !property ? JSON.parse(JSON.stringify(datasources[datasourceId].store.state))
: datasources[datasourceId].store.state[property];
let append = {};
append[dependenciesKey] = resolvedValues;
Object.assign(queryDependencies, append);
}
});
query = queryFn(queryDependencies);
} else {
query = queryFn ? queryFn.toString() : 'n/a';
}
query = this.formatQueryString(query);
const dataSource: IDataSource = datasources[datasource];
const elementQuery = dataSource.plugin.getElementQuery(dataSource, queryDependencies, query, queryFilters);
if (elementQuery !== null) {
query = elementQuery;
}
const exportData: IExportData = { id, data, isJSON, query, group, isGroupedJSON };
result.push(exportData);
});
// Group primative (scorecard) results
result = result.reduce((a: IExportData[], c: IExportData) => {
if (c.isJSON) {
a.push(c);
return a;
}
const target = a.find((i) => i.group === c.group);
let data = {};
data[c.id] = c.data;
// new
if (!target) {
c.data = data;
c.isGroupedJSON = true;
c.isJSON = true;
c.id = c.group;
a.push(c);
return a;
}
// skip
if (target.isGroupedJSON !== true) {
a.push(c);
return a;
}
// merge
target.data = Object.assign(target.data, data);
return a;
}, []);
// Order by largest data set
result.sort((a, b) => b.data.toString().length - a.data.toString().length);
return result;
}
private formatQueryString(query: string) {
// Strip indent whitespaces
return query.replace(/(\s{2,})?(.+)?/gm, '$2\n').trim();
}
}
const cardSettingsStore =
alt.createStore<ICardSettingsStoreState>(CardSettingsStore as AltJS.StoreModel<any>, 'CardSettingsStore');
export default cardSettingsStore;

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

@ -0,0 +1,283 @@
import * as React from 'react';
import Dialog from 'react-md/lib/Dialogs';
import Button from 'react-md/lib/Buttons/Button';
import Toolbar from 'react-md/lib/Toolbars';
import CircularProgress from 'react-md/lib/Progress/CircularProgress';
import SelectField from 'react-md/lib/SelectFields';
import AceEditor, { EditorProps, Annotation } from 'react-ace';
import * as brace from 'brace';
import 'brace/mode/text';
import 'brace/mode/json';
import 'brace/theme/github';
import SettingsActions from './CardSettingsActions';
import SettingsStore, { IExportData } from './CardSettingsStore';
import { Toast, ToastActions, IToast } from '../../Toast';
const editorProps: EditorProps = {
$blockScrolling: 1
};
interface ISettingsProps {
offsetHeight?: number;
dashboard: IDashboardConfig;
}
interface ISettingsState {
visible: boolean;
title: string;
elementId: string;
dashboard?: IDashboardConfig;
selectedIndex: number;
exportData?: IExportData[];
result?: string;
}
export default class Edit extends React.PureComponent<ISettingsProps, ISettingsState> {
static defaultProps = {
offsetHeight: 64 // set to height of header / toolbar
};
constructor(props: ISettingsProps) {
super(props);
this.state = SettingsStore.getState();
this.onChange = this.onChange.bind(this);
this.copyData = this.copyData.bind(this);
this.copyQuery = this.copyQuery.bind(this);
}
componentDidMount() {
SettingsStore.listen(this.onChange);
}
componentWillUnmount() {
SettingsStore.unlisten(this.onChange);
}
onChange(state: ISettingsState) {
const { visible, elementId, title, selectedIndex, exportData } = state;
this.setState({ visible, elementId, title, selectedIndex, exportData });
}
openDialog = (title: string, elementId: string) => {
SettingsActions.openDialog(title, elementId);
}
closeDialog = () => {
SettingsActions.closeDialog();
}
copyData() {
const { exportData, selectedIndex} = this.state;
if (!exportData) {
return;
}
const selected = exportData[selectedIndex];
const text = selected.isJSON ? JSON.stringify(selected.data, null, 2) : selected.data.toString();
this.copyToClipboard(text);
}
copyQuery() {
const { exportData, selectedIndex} = this.state;
if (!exportData) {
return;
}
const selected = exportData[selectedIndex];
const text = selected.query;
this.copyToClipboard(text);
}
componentWillUpdate(nextProps: any, nextState: any) {
const { visible } = this.state;
const { dashboard } = this.props;
if (nextState.visible === true && visible !== nextState.visible) {
SettingsActions.getExportData(dashboard);
}
}
render() {
const { visible, title, elementId, selectedIndex, exportData } = this.state;
const { offsetHeight, dashboard } = this.props;
const aceHeight = 'calc(100vh - ' + (offsetHeight * 4) + 'px)';
const titleStyle = { height: offsetHeight + 'px)' } as React.CSSProperties;
let actions = null;
let json = '';
let query = '';
let mode = 'text';
let dataActions = null;
let queryActions = null;
if (exportData && exportData.length > 0) {
const options = exportData.map(item => item.id);
const selectedValue = options[selectedIndex];
actions = options.length > 1 ? [
(
<SelectField
id="theme"
placeholder="Theme"
position={SelectField.Positions.BELOW}
defaultValue={selectedValue}
menuItems={options}
onChange={(newValue, index) => SettingsActions.selectIndex(index)}
tabIndex={-1}
toolbar
/>
)
] : <Button flat disabled label={selectedValue} style={{ textTransform: 'none', fontWeight: 'normal' }} />;
const selected: IExportData = exportData[selectedIndex];
// data
const data = selected.data;
switch (typeof data) {
case 'object':
json = data ? JSON.stringify(data, null, 2) : 'null';
mode = 'json';
break;
case 'string':
json = data;
break;
case 'boolean':
json = (data === true) ? 'true' : 'false';
break;
case 'undefined':
json = 'undefined';
break;
case 'number':
default:
json = data.toString();
}
// query
if (selected.query) {
query = selected.query;
}
// actions
dataActions = [
(
<Button icon tooltipLabel="Copy" onClick={this.copyData} tabIndex={-1}>
content_copy
</Button>
),
(
<Button icon tooltipLabel="Download" onClick={SettingsActions.downloadData} tabIndex={-1}>
file_download
</Button>
)
];
queryActions = [
(
<Button icon tooltipLabel="Copy" onClick={this.copyQuery} tabIndex={-1}>
content_copy
</Button>
)
];
}
let id = 'Element id error';
if ( elementId) {
id = elementId.split('@')[0] || 'Element index error';
}
const content = !query ? (
<div className="md-toolbar-relative md-grid">
<div className="md-cell--12">
<h3>{id}</h3>
</div>
<div className="md-cell--12">
<p>Use the same id for the element and data source to unwind the query and data.</p>
</div>
</div>
) : (
<div className="md-toolbar-relative md-grid md-grid--no-spacing">
<div className="md-cell--6">
<Toolbar title="Data" actions={dataActions} themed style={{ width: '100%' }} />
<AceEditor className="md-cell--12"
name="ace"
mode={mode}
theme="github"
value={json}
readOnly={true}
showGutter={true}
showPrintMargin={false}
highlightActiveLine={true}
tabSize={2}
width="100%"
height={aceHeight}
editorProps={editorProps}
/>
</div>
<div className="md-cell--6">
<Toolbar title="Query" actions={queryActions} themed style={{ width: '100%' }} />
<AceEditor className="md-cell--12"
name="ace"
mode="text"
theme="github"
value={query}
readOnly={true}
showGutter={true}
showPrintMargin={false}
highlightActiveLine={true}
tabSize={2}
width="100%"
height={aceHeight}
editorProps={editorProps}
/>
</div>
</div>
);
return (
<Dialog
id="editElementDialog"
visible={visible}
aria-label="Element settings"
focusOnMount={false}
onHide={this.closeDialog}
dialogStyle={{ width: '80%' }}
contentStyle={{ margin: '0px', padding: '0px' }}
lastChild={true}
>
<Toolbar
colored
nav={<Button icon onClick={this.closeDialog} tabIndex={-1}>close</Button>}
actions={actions}
title={title}
fixed
style={{ width: '100%' }}
/>
{content}
</Dialog>
);
}
private toast(text: string) {
ToastActions.showText(text);
}
private copyToClipboard(text: string) {
if (!document.queryCommandSupported('copy')) {
this.toast('Browser not supported');
return;
}
const input = document.createElement('textarea');
input.style.position = 'fixed';
input.style.opacity = '0';
input.value = text;
document.body.appendChild(input);
input.select();
document.execCommand('copy');
document.body.removeChild(input);
}
}

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

@ -0,0 +1,9 @@
import Settings from './Settings';
import SettingsActions from './CardSettingsActions';
import SettingsStore from './CardSettingsStore';
export {
Settings,
SettingsActions,
SettingsStore
};

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

@ -0,0 +1,3 @@
import Card from './Card';
export default Card;

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

@ -0,0 +1,15 @@
function downloadBlob(data: string, mimeType: string, filename: string) {
const blob = new Blob([data], {
type: mimeType
});
var el = document.createElement('a');
el.setAttribute('href', window.URL.createObjectURL(blob));
el.setAttribute('download', filename);
el.style.display = 'none';
document.body.appendChild(el);
el.click();
document.body.removeChild(el);
}
export { downloadBlob };

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

@ -0,0 +1,275 @@
import * as React from 'react';
import Dialog from 'react-md/lib/Dialogs';
import Button from 'react-md/lib/Buttons/Button';
import Toolbar from 'react-md/lib/Toolbars';
import CircularProgress from 'react-md/lib/Progress/CircularProgress';
import SelectField from 'react-md/lib/SelectFields';
import AceEditor, { EditorProps, Annotation } from 'react-ace';
import * as brace from 'brace';
import 'brace/mode/javascript';
import 'brace/ext/searchbox';
import 'brace/ext/language_tools';
import EditorActions from './EditorActions';
import EditorStore from './EditorStore';
import { Toast, ToastActions, IToast } from '../../Toast';
import ConfigurationsActions from '../../../actions/ConfigurationsActions';
const themes: string[] = ['github', 'twilight'];
themes.forEach((theme) => {
require(`brace/theme/${theme}`);
});
const editorProps: EditorProps = {
$blockScrolling: 1
};
interface IEditorProps {
offsetHeight?: number;
dashboard?: IDashboardConfig;
}
interface IEditorState {
value?: string;
visible?: boolean;
selectedTheme: number;
// internal
saveDisabled: boolean;
}
export default class Editor extends React.PureComponent<IEditorProps, IEditorState> {
static defaultProps = {
offsetHeight: 64 // set to height of header / toolbar
};
private aceEditor?: AceEditor;
private originalValue: string;
constructor(props: IEditorProps) {
super(props);
this.state = EditorStore.getState();
this.onChange = this.onChange.bind(this);
this.undo = this.undo.bind(this);
this.redo = this.redo.bind(this);
this.copy = this.copy.bind(this);
this.trySave = this.trySave.bind(this);
this.onLint = this.onLint.bind(this);
}
componentDidMount() {
EditorStore.listen(this.onChange);
}
componentWillUnmount() {
EditorStore.unlisten(this.onChange);
}
onChange(state: IEditorState) {
const { value, visible, selectedTheme, saveDisabled } = state;
if (!this.originalValue) {
this.originalValue = value;
}
this.setState({ value, visible, selectedTheme, saveDisabled });
}
openDialog = (dashboardId: string) => {
EditorActions.openDialog();
EditorActions.loadDashboard(dashboardId);
}
closeDialog = () => {
EditorActions.closeDialog();
}
undo() {
this.aceEditor['editor'].undo();
}
redo() {
this.aceEditor['editor'].redo();
}
copy() {
if (!document.queryCommandSupported('copy')) {
this.toast('Browser not supported');
return;
}
const {value} = this.state;
const input = document.createElement('input');
input.style.position = 'fixed';
input.style.opacity = '0';
input.value = value;
document.body.appendChild(input);
input.select();
document.execCommand('copy');
document.body.removeChild(input);
this.toast('Copied to clipboard');
}
trySave() {
if (!this.isModified()) {
this.closeDialog(); // close if no changes
} else {
this.save();
}
}
onLint(annotations: Annotation[]) {
const { saveDisabled } = this.state;
const isLintPassed = this.isLintPassed();
if (isLintPassed && saveDisabled) {
this.setState({ saveDisabled: false });
} else if (!isLintPassed && !saveDisabled) {
this.setState({ saveDisabled: true });
}
}
render() {
const { visible, value, selectedTheme, saveDisabled } = this.state;
const theme = themes[selectedTheme];
const saveLabel = !saveDisabled ? 'Save' : 'Fix errors';
const saveClass = !saveDisabled ? 'pass' : 'fail';
const actionButtons = [
(
<SelectField
id="theme"
placeholder="Theme"
position={SelectField.Positions.BELOW}
defaultValue={theme}
menuItems={themes}
onChange={(newValue, index) => EditorActions.selectTheme(index)}
tabIndex={-1}
/>
),
<Button icon key={0} tooltipLabel="Undo" onClick={this.undo} tabIndex={-1}>undo</Button>,
<Button icon key={1} tooltipLabel="Redo" onClick={this.redo} tabIndex={-1}>redo</Button>,
<Button icon key={2} tooltipLabel="Copy document" onClick={this.copy} tabIndex={-1}>content_copy</Button>,
(
<Button
flat
label={saveLabel}
className={saveClass}
onClick={this.trySave}
tabIndex={-1}
accessKey="s"
disabled={saveDisabled}
/>
)
];
const actions = !value ? null : actionButtons;
const content = value !== null ? this.renderEditor(value, theme) : this.renderLoading();
return (
<Dialog
id="editDialog"
visible={visible}
aria-label="Edit Dashboard"
dialogStyle={{ overflow: 'hidden' }}
contentStyle={{ overflow: 'hidden' }}
fullPage
focusOnMount={false}
>
<Toolbar
colored
nav={<Button icon onClick={this.closeDialog} tabIndex={-1}>close</Button>}
actions={actions}
title="Edit dashboard"
fixed
/>
{content}
</Dialog>
);
}
private renderLoading() {
return (
<div className="layout">
<div className="center">
<CircularProgress id="loading" />
</div>
</div>
);
}
private renderEditor(value: string, theme: string) {
const { offsetHeight } = this.props;
const calculatedHeight = offsetHeight > 0 ? 'calc(100vh - ' + offsetHeight + 'px)' : '100vh';
return (
<div className="md-grid md-grid--no-spacing">
<form className="md-toolbar-relative" style={{ width: '100%' }}>
<AceEditor
ref={(self) => this.aceEditor = self}
value={value}
onLoad={(editor) => editor['session'].$worker.on('annotate', (e) => this.onLint(e.data))}
onChange={(newValue) => EditorActions.updateValue(newValue)}
mode="javascript"
theme={theme}
name="ace"
showGutter={true}
showPrintMargin={false}
highlightActiveLine={true}
tabSize={2}
enableBasicAutocompletion={true}
enableLiveAutocompletion={true}
width="100%"
height={calculatedHeight}
editorProps={editorProps}
/>
</form>
</div>
);
}
private isEditor(): boolean {
if (!this.aceEditor || !this.aceEditor['editor']) {
return false;
}
return true;
}
private isModified(): boolean {
if (!this.isEditor || !this.originalValue) {
return false;
}
const {value} = this.state;
return (this.originalValue !== value);
}
private isLintPassed(): boolean {
const annotations = this.aceEditor['editor'].getSession().getAnnotations();
return (annotations.findIndex(annotation => annotation.type === 'error') === -1);
}
private save() {
const {dashboard} = this.props;
const {value} = this.state;
const objectString = value.replace(/(^\s*return\s*)|(\s*$)/g, '');
let newDashboard: IDashboardConfig = null;
try {
// tslint:disable-next-line:no-eval
newDashboard = eval('(' + objectString + ')') as IDashboardConfig;
} catch (e) {
throw new Error('Failed to parse dashboard.');
}
// overwrites existing dashboard
if (dashboard && dashboard.id && dashboard.url) {
newDashboard.id = dashboard.id;
newDashboard.url = dashboard.url;
}
this.toast('Saving changes');
ConfigurationsActions.saveConfiguration(newDashboard);
}
private toast(text: string) {
ToastActions.showText(text);
}
}

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

@ -0,0 +1,46 @@
import alt, { AbstractActions } from '../../../alt';
import * as request from 'xhr-request';
interface IEditorActions {
openDialog(): any;
closeDialog(): any;
loadDashboard(dashboardId: string): any;
selectTheme(index: number): number;
updateValue(newValue: string): string;
}
class EditorActions extends AbstractActions implements IEditorActions {
openDialog() {
return {};
}
closeDialog() {
return {};
}
loadDashboard(dashboardId: string) {
this.openDialog();
return (dispatch) => {
request('/api/dashboards/' + dashboardId + '?format=raw', {}, function (err: any, data: any) {
if (err) {
throw err;
}
return dispatch( data );
});
};
}
selectTheme(index: number) {
return index;
}
updateValue(newValue: string): string {
return newValue;
}
}
const editorActions = alt.createActions<IEditorActions>(EditorActions);
export default editorActions;

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

@ -0,0 +1,63 @@
import * as React from 'react';
import alt, { AbstractStoreModel } from '../../../alt';
import editorActions from './EditorActions';
interface IEditorStoreState {
visible: boolean;
value: string;
selectedTheme: number;
// internal
saveDisabled: boolean;
}
class EditorStore extends AbstractStoreModel<IEditorStoreState> implements IEditorStoreState {
visible: boolean;
value: string;
selectedTheme: number;
// internal
saveDisabled: boolean;
constructor() {
super();
this.visible = false;
this.value = null;
this.selectedTheme = 0;
this.saveDisabled = false;
this.bindListeners({
openDialog: editorActions.openDialog,
closeDialog: editorActions.closeDialog,
loadDashboard: editorActions.loadDashboard,
selectTheme: editorActions.selectTheme,
updateValue: editorActions.updateValue,
});
}
openDialog() {
this.visible = true;
}
closeDialog() {
this.visible = false;
}
loadDashboard(value: string) {
this.value = value;
}
selectTheme(index: number) {
this.selectedTheme = index;
}
updateValue(newValue: string) {
this.value = newValue;
}
}
const editorStore = alt.createStore<IEditorStoreState>(EditorStore as AltJS.StoreModel<any>, 'EditorStore');
export default editorStore;

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

@ -0,0 +1,9 @@
import Editor from './Editor';
import EditorActions from './EditorActions';
import EditorStore from './EditorStore';
export {
Editor,
EditorActions,
EditorStore
};

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

@ -0,0 +1,456 @@
import * as React from 'react';
import * as _ from 'lodash';
import Toolbar from 'react-md/lib/Toolbars';
import Button from 'react-md/lib/Buttons';
import Dialog from 'react-md/lib/Dialogs';
import { Spinner } from '../Spinner';
import { AutoRefreshSelector } from '../AutoRefreshSelector';
import * as ReactGridLayout from 'react-grid-layout';
var ResponsiveReactGridLayout = ReactGridLayout.Responsive;
var WidthProvider = ReactGridLayout.WidthProvider;
ResponsiveReactGridLayout = WidthProvider(ResponsiveReactGridLayout);
import ElementConnector from '../ElementConnector';
import { loadDialogsFromDashboard } from '../generic/Dialogs';
import { downloadBlob } from './DownloadFile';
import { SettingsButton } from '../Settings';
import ConfigurationsActions from '../../actions/ConfigurationsActions';
import ConfigurationsStore from '../../stores/ConfigurationsStore';
import VisibilityStore from '../../stores/VisibilityStore';
import { Editor, EditorActions } from './Editor';
import { Settings } from '../Card/Settings';
const renderHTML = require('react-render-html');
import List from 'react-md/lib/Lists/List';
import ListItem from 'react-md/lib/Lists/ListItem';
import FontIcon from 'react-md/lib/FontIcons';
import Avatar from 'react-md/lib/Avatars';
import Subheader from 'react-md/lib/Subheaders';
import Divider from 'react-md/lib/Dividers';
import TextField from 'react-md/lib/TextFields';
import MenuButton from 'react-md/lib/Menus/MenuButton';
interface IDashboardProps {
dashboard?: IDashboardConfig;
}
interface IDashboardState {
editMode?: boolean;
askDelete?: boolean;
askSaveAsTemplate?: boolean;
mounted?: boolean;
currentBreakpoint?: string;
layouts?: ILayouts;
grid?: any;
askConfig?: boolean;
visibilityFlags?: IDict<boolean>;
infoVisible?: boolean;
infoHtml?: string;
newTemplateName?: string;
newTemplateDescription?: string;
}
export default class Dashboard extends React.Component<IDashboardProps, IDashboardState> {
layouts = {};
state = {
editMode: false,
askDelete: false,
askSaveAsTemplate: false,
currentBreakpoint: 'lg',
mounted: false,
layouts: {},
grid: null,
askConfig: false,
visibilityFlags: {},
infoVisible: false,
infoHtml: '',
newTemplateName: '',
newTemplateDescription: '',
};
constructor(props: IDashboardProps) {
super(props);
this.onBreakpointChange = this.onBreakpointChange.bind(this);
this.onLayoutChangeActive = this.onLayoutChangeActive.bind(this);
this.onLayoutChangeInactive = this.onLayoutChangeInactive.bind(this);
this.onConfigDashboard = this.onConfigDashboard.bind(this);
this.toggleEditMode = this.toggleEditMode.bind(this);
this.onDeleteDashboard = this.onDeleteDashboard.bind(this);
this.onDeleteDashboardApprove = this.onDeleteDashboardApprove.bind(this);
this.onDeleteDashboardCancel = this.onDeleteDashboardCancel.bind(this);
this.onUpdateLayout = this.onUpdateLayout.bind(this);
this.onOpenInfo = this.onOpenInfo.bind(this);
this.onCloseInfo = this.onCloseInfo.bind(this);
this.onDownloadDashboard = this.onDownloadDashboard.bind(this);
this.onSaveAsTemplate = this.onSaveAsTemplate.bind(this);
this.newTemplateNameChange = this.newTemplateNameChange.bind(this);
this.onSaveAsTemplateApprove = this.onSaveAsTemplateApprove.bind(this);
this.onSaveAsTemplateCancel = this.onSaveAsTemplateCancel.bind(this);
this.newTemplateDescriptionChange = this.newTemplateDescriptionChange.bind(this);
this.onVisibilityStoreChange = this.onVisibilityStoreChange.bind(this);
VisibilityStore.listen(this.onVisibilityStoreChange);
this.state.newTemplateName = this.props.dashboard.name;
this.state.newTemplateDescription = this.props.dashboard.description;
}
componentDidMount() {
let { dashboard } = this.props;
let { mounted } = this.state;
this.onLayoutChange = this.onLayoutChangeActive;
if (dashboard && !mounted) {
const layout = dashboard.config.layout;
// For each column, create a layout according to number of columns
let layouts = ElementConnector.loadLayoutFromDashboard(dashboard, dashboard);
layouts = _.extend(layouts, dashboard.layouts || {});
this.layouts = layouts;
this.setState({
mounted: true,
layouts: { lg: layouts['lg'] },
grid: {
className: 'layout',
rowHeight: layout.rowHeight || 30,
cols: layout.cols,
breakpoints: layout.breakpoints,
verticalCompact: false
}
});
}
}
componentDidUpdate() {
this.componentDidMount();
}
componentWillUnmount() {
this.onLayoutChange = this.onLayoutChangeInactive;
VisibilityStore.unlisten(this.onVisibilityStoreChange);
}
onVisibilityStoreChange(state: any) {
this.setState({ visibilityFlags: state.flags });
}
onBreakpointChange(breakpoint: any) {
var layouts = this.state.layouts;
layouts[breakpoint] = layouts[breakpoint] || this.layouts[breakpoint];
this.setState({
currentBreakpoint: breakpoint,
layouts: layouts
});
}
onLayoutChange(layout: any, layouts: any) { }
onLayoutChangeActive(layout: any, layouts: any) {
// Waiting for breakpoint to change
let breakpoint = this.state.currentBreakpoint;
let newLayouts = this.state.layouts;
newLayouts[breakpoint] = layout;
this.setState({
layouts: newLayouts
});
// Saving layout to API
let { dashboard } = this.props;
dashboard.layouts = dashboard.layouts || {};
dashboard.layouts[breakpoint] = layout;
if (this.state.editMode) {
ConfigurationsActions.saveConfiguration(dashboard);
}
}
onLayoutChangeInactive(layout: any, layouts: any) {
}
onConfigDashboard() {
this.setState({ askConfig: true });
}
toggleEditMode() {
this.setState({ editMode: !this.state.editMode });
}
onDeleteDashboard() {
this.setState({ askDelete: true });
}
onSaveAsTemplate() {
this.setState({ askSaveAsTemplate: true });
}
onSaveAsTemplateApprove() {
let { dashboard } = this.props;
var template = _.cloneDeep(dashboard);
template.name = this.state.newTemplateName;
template.description = this.state.newTemplateDescription;
template.category = 'Custom Templates';
template.id = template.url = dashboard.id + (Math.floor(Math.random() * 1000) + 1); // generate random id
// Removing connections so private info will not be included
template.config.connections = {};
ConfigurationsActions.saveAsTemplate(template);
window.location.href = '/';
this.setState({ askSaveAsTemplate: false });
}
onSaveAsTemplateCancel() {
this.setState({ askSaveAsTemplate: false });
}
newTemplateNameChange(value: string, e: any) {
this.setState({ newTemplateName: value });
}
newTemplateDescriptionChange(value: string, e: any) {
this.setState({ newTemplateDescription: value });
}
onDeleteDashboardApprove() {
let { dashboard } = this.props;
if (!dashboard) {
console.warn('Dashboard not found. Aborting delete.');
}
ConfigurationsActions.deleteDashboard(dashboard.id);
window.location.href = '/';
this.setState({ askDelete: false });
}
onDeleteDashboardCancel() {
this.setState({ askDelete: false });
}
onConfigDashboardCancel() {
this.setState({ askConfig: false });
}
onUpdateLayout() {
this.setState({ editMode: !this.state.editMode });
this.setState({ editMode: !this.state.editMode });
}
onOpenInfo(html: string) {
this.setState({ infoVisible: true, infoHtml: html });
}
onCloseInfo() {
this.setState({ infoVisible: false });
}
onDownloadDashboard() {
let { dashboard } = this.props;
dashboard.layouts = dashboard.layouts || {};
let stringDashboard = ConfigurationsActions.convertDashboardToString(dashboard);
var dashboardName = dashboard.id.replace(/ +/g, ' ');
dashboardName = dashboard.id.replace(/ +/g, '_');
downloadBlob('return ' + stringDashboard, 'application/json', dashboardName + '.private.js');
}
render() {
const { dashboard } = this.props;
const {
currentBreakpoint,
grid,
editMode,
askDelete,
askConfig ,
askSaveAsTemplate,
newTemplateName,
newTemplateDescription
} = this.state;
const { infoVisible, infoHtml } = this.state;
const layout = this.state.layouts[currentBreakpoint];
if (!grid) {
return null;
}
// Creating visual elements
var elements = ElementConnector.loadElementsFromDashboard(dashboard, layout);
// Creating filter elements
var { filters, /*additionalFilters*/ } = ElementConnector.loadFiltersFromDashboard(dashboard);
// Loading dialogs
var dialogs = loadDialogsFromDashboard(dashboard);
// Actions to perform on an active dashboard
let toolbarActions = [];
// Edit toggle button
const editLabel = editMode ? 'Save layout' : 'Edit layout' ;
toolbarActions.push(
(
<span>
<AutoRefreshSelector/>
</span>
),
(
<span>
<Button key="edit-grid" icon primary={editMode} tooltipLabel={editLabel} onClick={this.toggleEditMode}>
edit
</Button>
</span>
),
(
<SettingsButton onUpdateLayout={this.onUpdateLayout} />
),
(
<span>
<MenuButton
id="vert-menu"
icon
buttonChildren="more_vert"
tooltipLabel="More">
<ListItem
key="info"
primaryText="Info"
leftIcon={<FontIcon key="info">info</FontIcon>}
onClick={this.onOpenInfo.bind(this, dashboard.html)}
/>
<Divider />
<ListItem
key="edit"
primaryText="Edit code"
leftIcon={<FontIcon key="code">code</FontIcon>}
onClick={() => EditorActions.loadDashboard(dashboard.id)}
/>
<ListItem
key="down"
primaryText="Download dashboard"
leftIcon={<FontIcon key="file_download">file_download</FontIcon>}
onClick={this.onDownloadDashboard}
/>
<ListItem
key="temp"
primaryText="Save as template"
leftIcon={<FontIcon key="cloud_download">cloud_download</FontIcon>}
onClick={this.onSaveAsTemplate}
/>
<Divider />
<ListItem
key="del"
primaryText="Delete dashboard"
leftIcon={<FontIcon key="delete">delete</FontIcon>}
onClick={this.onDeleteDashboard}
/>
</MenuButton>
</span>
)
);
return (
<div style={{width: '100%'}}>
<Toolbar width={100} actions={toolbarActions}>
{filters}
<Spinner />
</Toolbar>
<ResponsiveReactGridLayout
{...grid}
isDraggable={editMode}
isResizable={editMode}
layouts={this.state.layouts}
onBreakpointChange={this.onBreakpointChange}
onLayoutChange={this.onLayoutChange}
// WidthProvider option
measureBeforeMount={false}
// I like to have it animate on mount. If you don't, delete `useCSSTransforms` (it's default `true`)
// and set `measureBeforeMount={true}`.
useCSSTransforms={this.state.mounted}
>
{elements}
</ResponsiveReactGridLayout>
{dialogs}
<Dialog
id="infoDialog"
visible={infoVisible}
onHide={this.onCloseInfo}
dialogStyle={{ width: '80%' }}
contentStyle={{ padding: '0', maxHeight: 'calc(100vh - 148px)' }}
aria-label="Info"
focusOnMount={false}
>
<div className="md-grid">
{renderHTML(infoHtml)}
</div>
</Dialog>
<Editor dashboard={dashboard} />
<Settings dashboard={dashboard} />
<Dialog
id="deleteDashboard"
visible={askDelete}
title="Are you sure?"
aria-labelledby="deleteDashboardDescription"
modal
actions={[
{ onClick: this.onDeleteDashboardApprove, primary: false, label: 'Permanently Delete', },
{ onClick: this.onDeleteDashboardCancel, primary: true, label: 'Cancel' }
]}
>
<p id="deleteDashboardDescription" className="md-color--secondary-text">
Deleting this dashboard will remove all Connections/Customization you have made to it.
Are you sure you want to permanently delete this dashboard?
</p>
</Dialog>
<Dialog
dialogStyle={{ width: '50%' }}
id="saveAsTemplateDialog"
visible={askSaveAsTemplate}
title="Save this dashoard as a custom template"
modal
actions={[
{ onClick: this.onSaveAsTemplateApprove, primary: false, label: 'Save as custom template', },
{ onClick: this.onSaveAsTemplateCancel, primary: true, label: 'Cancel' }
]}
>
<p>You can save this dashboard as a custom template for a future reuse</p>
<TextField
id="templateName"
label="New Template Name"
placeholder="Template Name"
className="md-cell md-cell--bottom"
value={newTemplateName}
onChange={this.newTemplateNameChange}
required
/>
<TextField
id="templateDescription"
label="New Template Description"
placeholder="Template Description"
className="md-cell md-cell--bottom"
value={newTemplateDescription}
onChange={this.newTemplateDescriptionChange}
required
/>
</Dialog>
</div>
);
}
}

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

@ -1,6 +1,7 @@
import * as React from 'react';
import * as _ from 'lodash';
import plugins from './generic/plugins';
import * as formats from '../utils/data-formats';
import { DataSourceConnector } from '../data-sources/DataSourceConnector';
import VisibilityActions from '../actions/VisibilityActions';
@ -51,6 +52,67 @@ export default class ElementConnector {
return layouts;
}
static createGenericFilter(
ReactElement: any,
idx?: number,
icon?: string,
source?: string | IStringDictionary,
dependencies?: IStringDictionary,
actions?: IDictionary,
title?: string,
subtitle?: string): JSX.Element {
return ElementConnector.createGenericElement(
ReactElement,
'__filter',
idx || 0,
source,
dependencies,
actions,
null,
title,
subtitle,
null,
null,
icon
);
}
static createGenericElement(
ReactElement: any,
id: string,
idx?: number,
source?: string | IStringDictionary,
dependencies?: IStringDictionary,
actions?: IDictionary,
props?: IDictionary,
title?: string,
subtitle?: string,
layout?: ILayout,
theme?: string[],
icon?: string): JSX.Element {
if (source && typeof ReactElement.fromSource === 'function') {
let fromSource = ReactElement.fromSource(source);
dependencies = _.extend({}, dependencies, fromSource);
}
return (
<ReactElement
key={idx}
id={id + '@' + (idx || 0)}
dependencies={dependencies}
actions={actions || {}}
props={props || {}}
title={title}
subtitle={subtitle}
layout={layout}
theme={theme}
icon={icon}
/>
);
}
static loadElementsFromDashboard(dashboard: IElementsContainer, layout: ILayout[]): React.Component<any, any>[] {
var elements = [];
var elementId = {};
@ -58,7 +120,7 @@ export default class ElementConnector {
dashboard.elements.forEach((element, idx) => {
var ReactElement = plugins[element.type];
var { id, dependencies, actions, props, title, subtitle, size, theme, location } = element;
var { id, dependencies, source, actions, props, title, subtitle, size, theme, location } = element;
var layoutProps = _.find(layout, { 'i': id });
if (dependencies && dependencies.visible && !visibilityFlags[dependencies.visible]) {
@ -78,16 +140,21 @@ export default class ElementConnector {
elementId[id] = true;
elements.push(
<div key={id}>
<ReactElement
id={id + idx}
dependencies={dependencies}
actions={actions || {}}
props={props || {}}
title={title}
subtitle={subtitle}
layout={layoutProps}
theme={theme}
/>
{
ElementConnector.createGenericElement(
ReactElement,
id,
idx,
source,
dependencies,
actions,
props,
title,
subtitle,
layoutProps,
theme
)
}
</div>
);
});
@ -99,19 +166,23 @@ export default class ElementConnector {
filters: React.Component<any, any>[],
additionalFilters: React.Component<any, any>[]
} {
var filters = [];
var additionalFilters = [];
let filters = [];
let additionalFilters = [];
dashboard.filters.forEach((element, idx) => {
var ReactElement = plugins[element.type];
let ReactElement = plugins[element.type];
let { dependencies, source, actions, title, subtitle, icon } = element;
(element.first ? filters : additionalFilters).push(
<ReactElement
key={idx}
dependencies={element.dependencies}
actions={element.actions}
title={element.title}
subtitle={element.subtitle}
icon={element.icon}
/>
ElementConnector.createGenericFilter(
ReactElement,
idx,
icon,
source,
dependencies,
actions,
title,
subtitle
)
);
});

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

@ -0,0 +1,77 @@
import * as React from 'react';
import Autocomplete from 'react-md/lib/Autocompletes';
import FontIcon from 'react-md/lib/FontIcons';
import icons from '../../constants/icons';
interface IIconPickerProps {
defaultLabel?: string;
defaultIcon?: string;
listStyle?: React.CSSProperties;
}
interface IIconPickerState {
label: string;
icon: string;
}
export default class IconPicker extends React.Component<IIconPickerProps, IIconPickerState> {
static defaultProps = {
defaultLabel: 'Search icons',
defaultIcon: 'dashboard',
listStyle: {},
};
static listItems: any = [];
constructor(props: any) {
super(props);
const { defaultLabel, defaultIcon } = props;
this.state = {
label: defaultLabel,
icon: defaultIcon,
};
this.onChange = this.onChange.bind(this);
}
getIcon() {
const { icon } = this.state;
// check icon value is valid
if (icons.findIndex(i => i === icon) > 0) {
return icon;
}
return 'dashboard';
}
componentWillMount() {
if (IconPicker.listItems.length === 0) {
IconPicker.listItems = icons.map((icon) => ({ icon, leftIcon: <FontIcon key="icon">{icon}</FontIcon> }));
}
}
render() {
const { label, icon } = this.state;
const { listStyle } = this.props;
return (
<Autocomplete
id="icon"
label={label}
className="md-cell--stretch"
data={IconPicker.listItems}
dataLabel={'icon'}
listStyle={listStyle}
value={icon}
onChange={this.onChange}
onAutocomplete={this.onChange}
/>
);
}
private onChange(icon: string) {
this.setState({ icon });
}
}

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

@ -0,0 +1,450 @@
import * as React from 'react';
import Toolbar from 'react-md/lib/Toolbars';
import Button from 'react-md/lib/Buttons/Button';
import CircularProgress from 'react-md/lib/Progress/CircularProgress';
import { Card, CardTitle, CardActions, CardText } from 'react-md/lib/Cards';
import Media, { MediaOverlay } from 'react-md/lib/Media';
import Dialog from 'react-md/lib/Dialogs';
import TextField from 'react-md/lib/TextFields';
import FileUpload from 'react-md/lib/FileInputs/FileUpload';
import { Link } from 'react-router';
import SetupActions from '../../actions/SetupActions';
import SetupStore from '../../stores/SetupStore';
import ConfigurationStore from '../../stores/ConfigurationsStore';
import ConfigurationsActions from '../../actions/ConfigurationsActions';
import utils from '../../utils';
import IconPicker from './IconPicker';
import { downloadBlob } from '../Dashboard/DownloadFile';
const renderHTML = require('react-render-html');
const MultipleSpacesRegex = / +/g;
const styles = {
card: {
width: 380,
height: 280,
marginBottom: 20
} as React.CSSProperties,
media: {
width: 380,
height: 150,
background: '#CCC',
margin: 0,
padding: 0
} as React.CSSProperties,
preview: {
width: 380,
height: 150,
backgroundRepeat: 'no-repeat',
backgroundPosition: '50% 50%',
backgroundSize: 'contain'
} as React.CSSProperties,
actions: {
position: 'absolute',
bottom: 0
} as React.CSSProperties
};
interface IHomeState extends ISetupConfig {
loaded?: boolean;
errors?: any;
templates?: IDashboardConfig[];
selectedTemplateId?: string;
template?: IDashboardConfig;
creationState?: string;
infoVisible?: boolean;
infoHtml?: string;
importVisible?: boolean;
importedFileContent?: any;
fileName?: string;
content?: string;
infoTitle?: string;
}
export default class Home extends React.Component<any, IHomeState> {
state: IHomeState = {
admins: null,
stage: 'none',
enableAuthentication: false,
allowHttp: false,
redirectUrl: '',
clientID: '',
clientSecret: '',
issuer: '',
loaded: false,
errors: null,
templates: [],
selectedTemplateId: null,
template: null,
creationState: null,
infoVisible: false,
infoHtml: '',
infoTitle: ''
};
private _fieldId;
private _fieldName;
private _fieldIcon;
constructor(props: any) {
super(props);
this.onNewTemplateSelected = this.onNewTemplateSelected.bind(this);
this.onNewTemplateCancel = this.onNewTemplateCancel.bind(this);
this.onNewTemplateSave = this.onNewTemplateSave.bind(this);
this.onOpenInfo = this.onOpenInfo.bind(this);
this.onCloseInfo = this.onCloseInfo.bind(this);
this.updateSetup = this.updateSetup.bind(this);
this.updateConfiguration = this.updateConfiguration.bind(this);
// import dashboard functionality
this.onOpenImport = this.onOpenImport.bind(this);
this.onCloseImport = this.onCloseImport.bind(this);
this.onSubmitImport = this.onSubmitImport.bind(this);
this.onLoad = this.onLoad.bind(this);
this.setFile = this.setFile.bind(this);
this.updateFileName = this.updateFileName.bind(this);
this.onExportTemplate = this.onExportTemplate.bind(this);
this.downloadTemplate = this.downloadTemplate.bind(this);
this.onOpenImport = this.onOpenImport.bind(this);
}
updateConfiguration(state: {
templates: IDashboardConfig[],
template: IDashboardConfig,
creationState: string,
errors: any
}) {
this.setState({
templates: state.templates || [],
template: state.template,
creationState: state.creationState,
errors: state.errors,
});
if (this.state.stage === 'requestDownloadTemplate') {
this.downloadTemplate(this.state.template);
}
}
updateSetup(state: IHomeState) {
this.setState(state);
// Setup hasn't been configured yet
if (state.stage === 'none') {
window.location.replace('/setup');
}
}
componentDidMount() {
this.setState(SetupStore.getState());
this.updateConfiguration(ConfigurationStore.getState());
SetupActions.load();
SetupStore.listen(this.updateSetup);
ConfigurationStore.listen(this.updateConfiguration);
}
componentWillUnmount() {
SetupStore.unlisten(this.updateSetup);
ConfigurationStore.unlisten(this.updateConfiguration);
}
componentDidUpdate() {
if (this.state.creationState === 'successful') {
window.location.replace('/dashboard/' + this._fieldId.getField().value);
}
}
onNewTemplateSelected(templateId: string) {
this.setState({ selectedTemplateId: templateId });
ConfigurationsActions.loadTemplate(templateId);
}
onNewTemplateCancel() {
this.setState({ selectedTemplateId: null });
}
deepObjectExtend(target: any, source: any) {
for (var prop in source) {
if (prop in target) {
this.deepObjectExtend(target[prop], source[prop]);
} else {
target[prop] = source[prop];
}
}
return target;
}
onNewTemplateSave() {
let createParams = {
id: this._fieldId.getField().value,
name: this._fieldName.getField().value,
icon: this._fieldIcon.getIcon(),
url: this._fieldId.getField().value
};
var dashboard: IDashboardConfig = this.deepObjectExtend({}, this.state.template);
dashboard.id = createParams.id;
dashboard.name = createParams.name;
dashboard.icon = createParams.icon;
dashboard.url = createParams.url;
ConfigurationsActions.createDashboard(dashboard);
}
onOpenInfo(html: string, title: string) {
this.setState({ infoVisible: true, infoHtml: html, infoTitle: title });
}
onCloseInfo() {
this.setState({ infoVisible: false });
}
onOpenImport() {
this.setState({ importVisible: true });
}
onCloseImport() {
this.setState({ importVisible: false });
}
updateFileName(value: string) {
this.setState({ fileName: value });
}
onLoad(importedFileContent: any, uploadResult: string) {
const { name, size, type, lastModifiedDate } = importedFileContent;
this.setState({ fileName: name.substr(0, name.indexOf('.')), content: uploadResult });
}
onSubmitImport() {
var dashboardId = this.state.fileName;
ConfigurationsActions.submitDashboardFile(this.state.content, dashboardId);
this.setState({ importVisible: false });
}
setFile(importedFileContent: string) {
this.setState({ importedFileContent });
}
onExportTemplate(templateId: string) {
this.setState({ stage: 'requestDownloadTemplate' });
ConfigurationsActions.loadTemplate(templateId);
}
downloadTemplate(template: IDashboardConfig) {
template.layouts = template.layouts || {};
let stringDashboard = utils.convertDashboardToString(template);
var dashboardName = template.id.replace(MultipleSpacesRegex, ' ');
dashboardName = template.id.replace(MultipleSpacesRegex, '_');
downloadBlob('return ' + stringDashboard, 'application/json', dashboardName + '.private.ts');
}
render() {
let { errors, loaded, redirectUrl, templates, selectedTemplateId, template } = this.state;
let { importVisible } = this.state;
let { importedFileContent, fileName } = this.state;
let { infoVisible, infoHtml, infoTitle } = this.state;
if (!redirectUrl) {
redirectUrl = window.location.protocol + '//' + window.location.host + '/auth/openid/return';
}
if (!loaded) {
return <CircularProgress key="progress" id="contentLoadingProgress" />;
}
if (!templates) {
return null;
}
// Create dashboard form validation
let error = false;
let errorText = null;
if (errors && errors.error && errors.type && errors.type === 'id') {
errorText = errors.error;
error = true;
}
let createCard = (tmpl, index) => (
<Card
key={index}
className="templates md-cell"
style={styles.card}>
<Media style={styles.media}>
<div className="preview" style={{ ...styles.preview, backgroundImage: `url(${tmpl.preview})` }} />
</Media>
<CardTitle title={tmpl.name} subtitle={tmpl.description} />
<CardActions style={styles.actions}>
<Button
label="Download"
tooltipLabel="Download template"
flat
onClick={this.onExportTemplate.bind(this, tmpl.id)}
>
file_download
</Button>
<Button
label="Info"
tooltipLabel="Show info"
flat
onClick={this.onOpenInfo.bind(this, tmpl.html || '<p>No info available</p>', tmpl.name)}
>
info
</Button>
<Button
label="Create"
tooltipLabel="Create dashboard"
flat
primary
onClick={this.onNewTemplateSelected.bind(this, tmpl.id)}
>
add_circle_outline
</Button>
</CardActions>
</Card>
);
// Dividing templates into categories
// General - All dashboards without any categories
// Features - Dashboards appearing at the top of the creation screen
let categories = { 'General': [], 'Featured': [] };
templates.forEach((tmpl, index) => {
let category = tmpl.category || 'General';
if (tmpl.featured) {
categories['Featured'].push(createCard(tmpl, index));
}
categories[category] = categories[category] || [];
categories[category].push(createCard(tmpl, index));
});
// Sort templates alphabetically
let sortedCategories = { 'General': categories.General, 'Featured': categories.Featured };
const keys = Object.keys(categories).filter(category => category !== 'Featured').sort();
keys.forEach(key => sortedCategories[key] = categories[key]);
categories = sortedCategories;
let toolbarActions = [];
toolbarActions.push(
(
<Button
flat
tooltipLabel="Import dashboard"
onClick={this.onOpenImport}
label="Import dashboard"
>file_upload
</Button>
)
);
return (
<div className="md-cell md-cell--12">
<Toolbar actions={toolbarActions} />
{
Object.keys(categories).map((category, index) => {
if (!categories[category].length) { return null; }
return (
<div key={index}>
<h1>{category}</h1>
<div className="md-grid">
{categories[category]}
</div>
</div>
);
})
}
<Dialog
id="ImportDashboard"
visible={importVisible || false}
title="Import dashboard"
modal
actions={[
{ onClick: this.onCloseImport, primary: false, label: 'Cancel' },
{ onClick: this.onSubmitImport, primary: true, label: 'Submit', disabled: !importedFileContent },
]}>
<FileUpload
id="dashboardDefenitionFile"
primary
label="Choose File"
accept="application/javascript"
onLoadStart={this.setFile}
onLoad={this.onLoad}
/>
<TextField
id="dashboardFileName"
label="Dashboard ID"
value={fileName || ''}
onChange={this.updateFileName}
disabled={!importedFileContent}
lineDirection="center"
placeholder="Choose an ID for the imported dashboard"
/>
</Dialog>
<Dialog
id="templateInfoDialog"
title={infoTitle}
visible={infoVisible || false}
onHide={this.onCloseInfo}
dialogStyle={{ width: '80%' }}
contentStyle={{ padding: '0', maxHeight: 'calc(100vh - 148px)' }}
aria-label="Info"
focusOnMount={false}
>
<div className="md-grid" style={{ padding: 20 }}>
{renderHTML(infoHtml)}
</div>
</Dialog>
<Dialog
id="configNewDashboard"
visible={selectedTemplateId !== null && template !== null}
title="Configure the new dashboard"
aria-labelledby="configNewDashboardDescription"
dialogStyle={{ width: '50%' }}
modal
actions={[
{ onClick: this.onNewTemplateCancel, primary: false, label: 'Cancel' },
{ onClick: this.onNewTemplateSave, primary: true, label: 'Create', },
]}
>
<IconPicker
ref={field => this._fieldIcon = field}
defaultLabel="Dashboard Icon"
defaultIcon={template && template.icon || 'dashboard'}
listStyle={{height: '136px'}} />
<TextField
id="id"
ref={field => this._fieldId = field}
label="Dashboard Id"
defaultValue={template && template.id || ''}
lineDirection="center"
placeholder="Choose an ID for the dashboard (will be used in the url)"
error={error}
errorText={errorText}
/>
<TextField
id="name"
ref={field => this._fieldName = field}
label="Dashboard Name"
defaultValue={template && template.name || ''}
lineDirection="center"
placeholder="Choose name for the dashboard (will be used in navigation)"
/>
</Dialog>
</div>
);
}
}

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

@ -6,7 +6,6 @@ import FontIcon from 'react-md/lib/FontIcons';
import ListItem from 'react-md/lib/Lists/ListItem';
import Avatar from 'react-md/lib/Avatars';
import SelectField from 'react-md/lib/SelectFields';
import NavigationLink from './NavigationLink';
import { Link } from 'react-router';
import Chip from 'react-md/lib/Chips';
import Menu from 'react-md/lib/Menus/Menu';
@ -55,7 +54,7 @@ export default class Navbar extends React.Component<any, any> {
if (!window['dashboardTemplates']) {
this.setState({ noTemplates: true });
}
},
},
5000
);
}
@ -63,6 +62,7 @@ export default class Navbar extends React.Component<any, any> {
render() {
let { dashboards, noTemplates } = this.state;
let { children, title } = this.props;
let pathname = '/';
try { pathname = window.location.pathname; } catch (e) { }
@ -133,12 +133,13 @@ export default class Navbar extends React.Component<any, any> {
const toolbarActions = [(
<Button
icon
tooltipLabel="Create Dashboard"
href="/"
component={Link}
icon
tooltipLabel="Create Dashboard"
href="/"
component={Link}
>add_box
</Button>), (
</Button>),
, (
<MenuButton
id="vert-menu"
icon
@ -154,7 +155,7 @@ export default class Navbar extends React.Component<any, any> {
/>
) : (
<ListItem
primaryText="Anon"
primaryText="Anonymous"
leftAvatar={<Avatar icon={<FontIcon>perm_identity</FontIcon>} />}
disabled
/>
@ -168,7 +169,7 @@ export default class Navbar extends React.Component<any, any> {
leftIcon={<FontIcon>lock</FontIcon>}
/>
</MenuButton>
)];
)];
if (noTemplates && !dashboards && window.location.pathname !== '/setup') {
children = (

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

@ -0,0 +1,40 @@
import * as React from 'react';
import { Media } from 'react-md/lib/Media';
import { ResponsiveContainer as RechartResponsiveContainer } from 'recharts';
interface IeContainerProps {
layout: {
x: number;
y: number;
w: number;
h: number;
};
}
interface IContainerState {
}
/**
* This class is used to remove warning from test cases
*/
export default class ResponsiveContainer extends React.PureComponent<IeContainerProps, IContainerState> {
render() {
const { children, layout } = this.props;
let containerProps = {};
if (!layout || !layout.w) {
containerProps['width'] = 100;
containerProps['aspect'] = 1;
}
return (
<RechartResponsiveContainer {...containerProps}>
{children}
</RechartResponsiveContainer>
);
}
}

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

@ -6,13 +6,11 @@ import Toolbar from 'react-md/lib/Toolbars';
import Dialog from 'react-md/lib/Dialogs';
import Paper from 'react-md/lib/Papers';
import SelectField from 'react-md/lib/SelectFields';
import { TabsContainer, Tabs, Tab } from 'react-md/lib/Tabs';
import FontIcon from 'react-md/lib/FontIcons';
import { ToastActions } from '../Toast';
import ConnectionsSettings from './ConnectionsSettings';
import ElementsSettings from './ElementsSettings';
import SettingsStore, { ISettingsStoreState } from '../../stores/SettingsStore';
import SettingsActions from '../../actions/SettingsActions';
import ConfigurationsActions from '../../actions/ConfigurationsActions';
@ -142,7 +140,8 @@ export default class SettingsButton extends React.Component<ISettingsButtonProps
id="settingsForm"
title="Edit Dashboard Settings"
visible={showSettingsDialog}
dialogStyle={{ width: '90%', height: '90%', overflowY: 'auto' }}
dialogStyle={{ width: '50%', height: '50%', overflowY: 'hidden' }}
contentStyle={{ height: 'calc(100% - 124px)', overflowY: 'auto' }}
className="dialog-toolbar-no-padding"
modal
actions={[
@ -150,25 +149,10 @@ export default class SettingsButton extends React.Component<ISettingsButtonProps
{ onClick: this.onCancel, primary: false, label: 'Cancel' }
]}
>
<TabsContainer colored panelClassName="md-grid">
<Tabs tabId="settings-tabs">
<Tab label={VIEWS.Connections}>
<div className="md-cell md-cell--6">
<ConnectionsSettings connections={dashboard.config.connections} />
</div>
</Tab>
<Tab label={VIEWS.Elements}>
<ElementsSettings settings={dashboard} />
</Tab>
<Tab label={VIEWS.DataSources}>
<h1>{VIEWS.DataSources} - is not implemented yet</h1>
</Tab>
<Tab label={VIEWS.Filters}>
<h1>{VIEWS.Filters} - is not implemented yet</h1>
</Tab>
</Tabs>
</TabsContainer>
</Dialog>
<div className="connections">
<ConnectionsSettings connections={dashboard.config.connections} />
</div>
</Dialog>
</span>
);
}

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

@ -8,7 +8,6 @@ import ConfigurationsStore, { IConfigurationsStoreState } from '../../stores/Con
import ConfigurationsActions from '../../actions/ConfigurationsActions';
import { DataSourceConnector, IDataSourceDictionary } from '../../data-sources';
import connections from '../../data-sources/connections';
import ConnectionsStore from '../../stores/ConnectionsStore';
import ConnectionsActions from '../../actions/ConnectionsActions';
@ -30,7 +29,7 @@ export default class SetupDashboard extends React.Component<ISetupDashboardProps
super(props);
this.onSave = this.onSave.bind(this);
this.onCancel = this.onCancel.bind(this);
this.onDelete = this.onDelete.bind(this);
this.onSaveGoToDashboard = this.onSaveGoToDashboard.bind(this);
this.redirectToHomepageIfStandalone = this.redirectToHomepageIfStandalone.bind(this);
@ -46,15 +45,15 @@ export default class SetupDashboard extends React.Component<ISetupDashboardProps
}
onParamChange(connectionKey: string, paramKey: string, value: any) {
let { connections } = this.state;
const { connections } = this.state;
connections[connectionKey] = connections[connectionKey] || {};
connections[connectionKey][paramKey] = value;
}
onSave() {
let { dashboard } = this.props;
let { connections } = this.state;
const { dashboard } = this.props;
const { connections } = this.state;
dashboard.config.connections = connections;
@ -66,18 +65,23 @@ export default class SetupDashboard extends React.Component<ISetupDashboardProps
setTimeout(this.redirectToHomepageIfStandalone, 2000);
}
onCancel() {
this.redirectToHomepageIfStandalone();
onDelete() {
const { dashboard } = this.props;
if (!dashboard) {
console.warn('Dashboard not found. Aborting delete.');
}
ConfigurationsActions.deleteDashboard(dashboard.id);
window.location.href = '/';
}
redirectToHomepageIfStandalone() {
let { dashboard } = this.props;
const { dashboard } = this.props;
window.location.replace(`/dashboard/${dashboard.url}`);
}
render() {
let { dashboard } = this.props;
let { connections } = this.state;
const { dashboard } = this.props;
const { connections } = this.state;
return (
<div style={{ width: '100%' }}>
@ -85,8 +89,8 @@ export default class SetupDashboard extends React.Component<ISetupDashboardProps
<div>
<Button flat primary label="Save" onClick={this.onSave}>save</Button>
<Button flat secondary label="Save and Go to Dashboard" onClick={this.onSaveGoToDashboard}>save</Button>
<Button flat secondary label="Cancel" onClick={this.onCancel}>cancel</Button>
<Button flat primary label="Save and Go to Dashboard" onClick={this.onSaveGoToDashboard}>save</Button>
<Button flat secondary label="Delete" onClick={this.onDelete}>delete</Button>
</div>
</div>

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

@ -105,12 +105,20 @@ export default class Setup extends React.Component<any, ISetupState> {
fixRedirectUrl(redirectUrl: string): string {
if (redirectUrl) { return redirectUrl; }
let host = window.location.host;
let host = '';
if (window && window.location) {
host = window.location.host;
}
let protocol = 'https:';
// On localhost, authentication requests go directly to port 4000
if (host === 'localhost:3000') { host = 'localhost:4000'; }
if (host === '' || host === 'localhost:3000' || host === 'localhost:4000') {
host = 'localhost:4000';
protocol = 'http:';
}
return window.location.protocol + '//' + host + '/auth/openid/return';
return protocol + '//' + host + '/auth/openid/return';
}
getAdminArray(): string[] {
@ -172,7 +180,9 @@ export default class Setup extends React.Component<any, ISetupState> {
}
redirectOut() {
window.location.replace('/');
if (window && window.location) {
window.location.replace('/');
}
}
onRemoveAdmin(admin: string) {
@ -187,11 +197,11 @@ export default class Setup extends React.Component<any, ISetupState> {
onSwitchAuthenticationEnables(checked: boolean) {
this.setState({ enableAuthentication: checked });
};
}
onSwitchAllowHttp(checked: boolean) {
this.setState({ allowHttp: checked });
};
}
onFieldChange(value: string, e: any) {
let state = {};
@ -237,6 +247,10 @@ export default class Setup extends React.Component<any, ISetupState> {
/>
));
const instructionsUrl = 'https://docs.microsoft.com' +
'/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal' +
'#create-an-azure-active-directory-application';
// tslint:disable:max-line-length
return (
<div style={{ width: '100%' }}>
@ -256,14 +270,19 @@ export default class Setup extends React.Component<any, ISetupState> {
buttonLabel="instructions"
>
<div>
Follow the instructions
in <a href="https://auth0.com/docs/connections/enterprise/azure-active-directory" target="_blank">this link</a> to
get <b>Client ID (Application ID)</b> and <b>Client Secret</b> (This process will require you to create a new application, add permissions, configure reply URL).
Follow the <a href={instructionsUrl} target="_blank">instructions</a> to get:
<br />
<ul>
<li>Application ID (to Client ID)</li>
<li>Client Secret</li>
<li>Tenant ID</li>
<li>You don't need to follow <b>Assign application to role</b></li>
<li>Make sure <b>Sign-on URL/Reply URL</b> is the same as <b>Redirect URL</b> in this screen</li>
</ul>
<br />
(This process will require you to create a new application, add permissions, configure reply URL).
<br />
<br />
The <b>Redirect Url</b> corresponds to the Reply URL.
<br/>
The <b>TenantID</b> is the 'Directory ID' token found in: Azure Portal > Active Directory > Properties.
<hr/>
Please add an administrator email and press the 'Add' button.
<hr/>

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

@ -0,0 +1,48 @@
import * as request from 'xhr-request';
import * as React from 'react';
import * as _ from 'lodash';
import CSSTransitionGroup from 'react-addons-css-transition-group';
import CircularProgress from 'react-md/lib/Progress/CircularProgress';
import Snackbar from 'react-md/lib/Snackbars';
import SpinnerStore, { ISpinnerStoreState } from './SpinnerStore';
import SpinnerActions from './SpinnerActions';
interface ISpinnerState extends ISpinnerStoreState {
}
export default class Spinner extends React.Component<any, ISpinnerState> {
constructor(props: any) {
super(props);
this.state = SpinnerStore.getState();
this.onChange = this.onChange.bind(this);
}
componentDidMount() {
this.onChange(SpinnerStore.getState());
SpinnerStore.listen(this.onChange);
}
componentWillUnmount() {
SpinnerStore.unlisten(this.onChange);
}
onChange(state: ISpinnerState) {
this.setState(state);
}
render () {
let refreshing = this.state.pageLoading || this.state.requestLoading || false;
return (
<div>
{refreshing && <CircularProgress key="progress" id="contentLoadingProgress" />}
</div>
);
}
}

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

@ -1,15 +1,15 @@
import alt, { AbstractActions } from '../../alt';
interface ISpinnerActions {
startPageLoading(): void;
endPageLoading(): void;
startRequestLoading(): void;
endRequestLoading(): void;
startPageLoading: AltJS.Action<any>;
endPageLoading: AltJS.Action<any>;
startRequestLoading: AltJS.Action<any>;
endRequestLoading: AltJS.Action<any>;
}
class SpinnerActions extends AbstractActions /*implements ISpinnerActions*/ {
constructor(alt: AltJS.Alt) {
super(alt);
constructor(altobj: AltJS.Alt) {
super(altobj);
this.generateActions(
'startPageLoading',

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

@ -1,31 +1,52 @@
import alt, { AbstractStoreModel } from '../../alt';
import { Toast, ToastActions, IToast } from '../Toast';
import spinnerActions from './SpinnerActions';
export interface ISpinnerStoreState {
pageLoading?: number;
requestLoading?: number;
mounted: boolean;
currentBreakpoint: string;
layouts: object;
}
const openOriginal = XMLHttpRequest.prototype.open;
const sendOriginal = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(method: string, url: string, async?: boolean, _?: string, __?: string) {
spinnerActions.startRequestLoading.defer(null);
openOriginal.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function(data: any) {
let _xhr: XMLHttpRequest = this;
_xhr.onreadystatechange = (response) => {
// readyState === 4: means the response is complete
if (_xhr.readyState === 4) {
spinnerActions.endRequestLoading.defer(null);
if (_xhr.status === 429) {
_429ApplicationInsights();
}
}
};
sendOriginal.apply(_xhr, arguments);
};
function _429ApplicationInsights() {
let toast: IToast = { text: 'You have reached the maximum number of Application Insights requests.' };
ToastActions.addToast(toast);
}
class SpinnerStore extends AbstractStoreModel<ISpinnerStoreState> implements ISpinnerStoreState {
pageLoading: number;
requestLoading: number;
mounted: boolean;
currentBreakpoint: string;
layouts: any;
constructor() {
super();
this.pageLoading = 0;
this.requestLoading = 0;
this.mounted = false;
this.currentBreakpoint = 'lg';
this.layouts = { };
this.bindListeners({
startPageLoading: spinnerActions.startPageLoading,
@ -52,6 +73,6 @@ class SpinnerStore extends AbstractStoreModel<ISpinnerStoreState> implements ISp
}
}
const spinnerStore = alt.createStore<ISpinnerStoreState>(SpinnerStore, 'SpinnerStore');
const spinnerStore = alt.createStore<ISpinnerStoreState>(SpinnerStore as any, 'SpinnerStore');
export default spinnerStore;

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

@ -6,4 +6,4 @@ export {
Spinner,
SpinnerActions,
SpinnerStore
}
};

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

@ -16,8 +16,13 @@ export default class Toast extends React.Component<any, IToastStoreState> {
this.removeToast = this.removeToast.bind(this);
}
onChange(state: any) {
this.setState(state);
onChange(state: IToastStoreState) {
let { toasts, autohide, autohideTimeout } = state;
this.setState({
toasts: toasts.map(o => ({ text: o.text })),
autohide,
autohideTimeout
});
}
componentDidMount() {
@ -25,12 +30,14 @@ export default class Toast extends React.Component<any, IToastStoreState> {
}
render() {
const {toasts, autohide, autohideTimeout} = this.state;
return (
<Snackbar
toasts={...this.state.toasts}
autohideTimeout={this.state.autohideTimeout}
autohide={this.state.autohide}
toasts={toasts}
autohide={autohide}
autohideTimeout={autohideTimeout}
onDismiss={this.removeToast}
lastChild={true}
/>
);
}

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

@ -2,14 +2,14 @@ import alt, { AbstractActions } from '../../alt';
import { IToast } from './ToastStore';
interface IToastActions {
showText(test: string): void;
showText(test: string): IToast;
addToast(toast: IToast): IToast;
removeToast(): void;
}
class ToastActions extends AbstractActions {
constructor(alt: AltJS.Alt) {
super(alt);
constructor(altobj: AltJS.Alt) {
super(altobj);
this.generateActions(
'addToast',
@ -21,8 +21,8 @@ class ToastActions extends AbstractActions {
return toast;
}
showText(text: string): void {
this.addToast({ text });
showText(text: string): IToast {
return this.addToast({ text });
}
}

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

@ -70,6 +70,6 @@ class ToastStore extends AbstractStoreModel<IToastStoreState> implements IToastS
}
}
const toastStore = alt.createStore<IToastStoreState>(ToastStore, 'ToastStore');
const toastStore = alt.createStore<IToastStoreState>(ToastStore as AltJS.StoreModel<any>, 'ToastStore');
export default toastStore;

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

@ -10,4 +10,4 @@ export {
ToastStore,
IToast,
IToastStoreState
}
};

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

@ -0,0 +1,18 @@
import * as React from 'react';
import injectTooltip from 'react-md/lib/Tooltips';
const Tooltip = injectTooltip(
({children, className, tooltip, ...props }) => (
<div {...props} className={(className || '') + ' inline-rel-container'} style={{position: 'relative'}}>
{tooltip}
{children}
</div>
));
Tooltip.propTypes = {
children: React.PropTypes.node,
className: React.PropTypes.string,
tooltip: React.PropTypes.node,
};
export default Tooltip;

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

@ -0,0 +1,3 @@
import Tooltip from './Tooltip';
export default Tooltip;

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

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

@ -12,16 +12,22 @@ import SelectField from 'react-md/lib/SelectFields';
import SettingsActions from '../../actions/SettingsActions';
export interface IBaseSettingsProps {
settings: IElement;
settings: DataSource;
}
export interface IBaseSettingsState {
}
export abstract class BaseSettings<T extends IBaseSettingsState> extends React.Component<IBaseSettingsProps, T> {
/**
* This base class encapsules shared methods for all types of data sources
*/
export abstract class BaseDataSourceSettings<T extends IBaseSettingsState>
extends React.Component<IBaseSettingsProps, T> {
// require derived classes to implement
// an icon to be displayed when selecting the specific data source settings in the settings dialog
abstract icon: string;
// Implement this menthod with your desired UI implementation.
abstract renderChildren();
constructor(props: IBaseSettingsProps) {
@ -34,6 +40,10 @@ export abstract class BaseSettings<T extends IBaseSettingsState> extends React.C
this.renderChildren = this.renderChildren.bind(this);
}
/**
* A helper method for finding an object by a given dot notation string representation.
* e.g: data.size.x
*/
protected getProperty(property: string, defaultValue: any = null): any {
let { settings } = this.props;
let arr = property.split('.');
@ -47,6 +57,10 @@ export abstract class BaseSettings<T extends IBaseSettingsState> extends React.C
return defaultValue;
}
/**
* A helper method for updaing an object by a given dot notation string representation.
* e.g: data.size.x
*/
protected updateProperty(property: string, value: any): void {
let { settings } = this.props;
let arr = property.split('.');
@ -58,13 +72,9 @@ export abstract class BaseSettings<T extends IBaseSettingsState> extends React.C
if (parent) { parent[key] = value; }
}
save() {
// tell the parents save ended
SettingsActions.saveSettingsCompleted();
}
onParamChange(value: string, event: any) {
this.updateProperty(event.target.id, value);
onParamChange(value: string, event: UIEvent) {
var t: any = event.target;
this.updateProperty(t.id, value);
}
onParamSelectChange(newValue: string, newActiveIndex: number, event: any) {
@ -86,7 +96,7 @@ export abstract class BaseSettings<T extends IBaseSettingsState> extends React.C
render() {
let { settings } = this.props;
let { id, props, title, subtitle, size, type } = settings;
let { id, type } = settings;
return (
<Card>
<CardTitle title={type} avatar={<Avatar random icon={<FontIcon>{this.icon}</FontIcon>} />} />
@ -100,50 +110,9 @@ export abstract class BaseSettings<T extends IBaseSettingsState> extends React.C
defaultValue={id}
onChange={this.onParamChange}
/>
<TextField
id="title"
label="Title"
placeholder="title"
leftIcon={<FontIcon>title</FontIcon>}
className="md-cell md-cell--bottom md-cell--6"
defaultValue={title}
onChange={this.onParamChange}
/>
<TextField
id="subtitle"
label="Subtitle"
placeholder="subtitle"
leftIcon={<FontIcon>text_fields</FontIcon>}
className="md-cell md-cell--bottom md-cell--6"
defaultValue={subtitle}
onChange={this.onParamChange}
/>
<div className="md-cell md-cell--bottom md-cell--6">
<div className="md-grid">
<SelectField
id="size.w"
name="size.w"
label="Width"
defaultValue={size.w || '1'}
menuItems={[1, 2, 3, 4, 5, 6, 7, 8, 9]}
onChange={this.onParamSelectChange}
className="md-cell md-cell--bottom ddl"
value={size.w}
/>
<SelectField
id="size.h"
name="size.h"
label="Width"
defaultValue={size.h || '1'}
menuItems={[1, 2, 3, 4, 5, 6, 7, 8, 9]}
onChange={this.onParamSelectChange}
className="md-cell md-cell--bottom ddl"
/>
</div>
</div>
{this.renderChildren()}
</div>
</Card>
);
}
};
}

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

@ -77,9 +77,10 @@ export default class InfoDrawer extends React.Component<IInfoDrawerProps, IInfoD
defaultVisible={false}
onVisibilityToggle={() => {}}
position={'right'}
type={Drawer.DrawerTypes.TEMPORARY}
type={Drawer.DrawerTypes.FLOATING}
header={drawerHeader}
style={{ zIndex: 100 }}
style={{ zIndex: 100, borderLeft: '1px solid lightgray' }}
onMediaTypeChange={() => {}}
>
<Media style={{ padding: 20, maxWidth: 300, width: width || 'auto', height: '100%' }}>
{this.props.children}

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

@ -0,0 +1,95 @@
import * as React from 'react';
import * as _ from 'lodash';
import Paper from 'react-md/lib/Papers';
import Button from 'react-md/lib/Buttons';
import TextField from 'react-md/lib/TextFields';
import Chip from 'react-md/lib/Chips';
import Divider from 'react-md/lib/Dividers';
interface ITokenInputProps {
tokens: any[];
zDepth: number;
onTokensChanged(): void;
}
interface ITokenInputState {
newToken: any;
}
/**
* This is a UI for editing a string array.
*/
export default class TokenInput extends React.Component<ITokenInputProps, ITokenInputState> {
state: ITokenInputState = {
newToken: ''
};
constructor(props: ITokenInputProps) {
super(props);
this.removeToken = this.removeToken.bind(this);
this.onNewTokenChange = this.onNewTokenChange.bind(this);
this.addToken = this.addToken.bind(this);
}
removeToken(token: any) {
let tokens = this.props.tokens;
_.remove(tokens, x => x === token);
if (this.props.onTokensChanged) {
this.props.onTokensChanged();
}
this.setState(this.state); // foce the component to update
}
onNewTokenChange(newData: any) {
this.setState({ newToken: newData });
}
addToken() {
if (this.state.newToken) {
let {tokens} = this.props;
tokens = tokens || [];
tokens.push(this.state.newToken);
this.setState({
newToken: ''
});
if (this.props.onTokensChanged) {
this.props.onTokensChanged();
}
}
}
render() {
let { tokens, zDepth } = this.props;
let { newToken } = this.state;
let chips = tokens.map((token: string, index: number) => (
<Chip
key={index}
onClick={this.removeToken.bind(this, token)}
removable
label={token}
/>
));
return (
<Paper zDepth={zDepth} >
<div style={{ padding: 5 }}>
{chips}
</div>
<Divider />
<div className="md-grid">
<TextField
id="addTokenInput"
lineDirection="center"
placeholder="Add a value"
className="md-cell md-cell--bottom"
value={newToken}
onChange={this.onNewTokenChange}
/>
<Button icon primary onClick={this.addToken} className="md-cell">add_circle</Button>
</div>
</Paper>
);
}
}

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

@ -3,16 +3,15 @@ import { GenericComponent, IGenericProps, IGenericState } from '../GenericCompon
import * as moment from 'moment';
import * as _ from 'lodash';
import { AreaChart, Area as AreaFill, XAxis, YAxis, CartesianGrid } from 'recharts';
import { Tooltip, ResponsiveContainer, Legend, defs } from 'recharts';
import { Tooltip, Legend, defs } from 'recharts';
import Card from '../../Card';
import ResponsiveContainer from '../../ResponsiveContainer';
import Switch from 'react-md/lib/SelectionControls/Switch';
import colors from '../../colors';
var { ThemeColors } = colors;
import '../generic.css';
import AreaSettings from './Settings';
interface IAreaProps extends IGenericProps {
theme?: string[];
showLegend?: boolean;
@ -28,12 +27,36 @@ interface IAreaState extends IGenericState {
export default class Area extends GenericComponent<IAreaProps, IAreaState> {
static editor = AreaSettings;
static defaultProps = {
isStacked: true
isStacked: false
};
state = {
timeFormat: '',
values: [],
lines: [],
isStacked: this.props.isStacked
};
static fromSource(source: string) {
return {
values: GenericComponent.sourceFormat(source, 'graphData'),
lines: GenericComponent.sourceFormat(source, 'lines'),
timeFormat: GenericComponent.sourceFormat(source, 'timeFormat')
};
}
constructor(props: IAreaProps) {
super(props);
// apply nested props
if (props && props.props) {
if (props.props.isStacked !== undefined) {
this.state.isStacked = props.props.isStacked as boolean;
}
}
}
dateFormat(time: string): string {
return moment(time).format('MMM-DD');
}
@ -43,18 +66,16 @@ export default class Area extends GenericComponent<IAreaProps, IAreaState> {
}
generateWidgets() {
let checked = this.is('isStacked');
const { isStacked } = this.state;
return (
<div className="widgets">
<Switch
id="stack"
name="stack"
label="Stack"
checked={checked}
defaultChecked
onChange={this.handleStackChange}
/>
</div>
<Switch
id="stack"
name="stack"
label="Stack"
checked={isStacked}
defaultChecked
onChange={this.handleStackChange}
/>
);
}
@ -64,23 +85,18 @@ export default class Area extends GenericComponent<IAreaProps, IAreaState> {
}
render() {
var { timeFormat, values, lines } = this.state;
var { title, subtitle, theme, props } = this.props;
var { showLegend, areaProps } = props;
const { timeFormat, values, lines, isStacked } = this.state;
const { id, title, subtitle, theme, props, layout } = this.props;
const { showLegend, areaProps } = props;
var format = timeFormat === 'hour' ? this.hourFormat : this.dateFormat;
var themeColors = theme || ThemeColors;
const format = timeFormat === 'hour' ? this.hourFormat : this.dateFormat;
const themeColors = theme || ThemeColors;
// gets the 'isStacked' boolean option from state, passed props or default values (in that order).
var isStacked = this.is('isStacked');
let stackProps = {};
if (isStacked) {
stackProps['stackId'] = '1';
}
const stackProps = !isStacked ? {} : { stackId : 1 };
var widgets = this.generateWidgets();
const widgets = this.generateWidgets();
var fillElements = [];
let fillElements = [];
if (values && values.length && lines) {
fillElements = lines.map((line, idx) => {
return (
@ -97,9 +113,8 @@ export default class Area extends GenericComponent<IAreaProps, IAreaState> {
}
return (
<Card title={title} subtitle={subtitle}>
{widgets}
<ResponsiveContainer>
<Card id={id} title={title} subtitle={subtitle} widgets={widgets}>
<ResponsiveContainer layout={layout}>
<AreaChart
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
data={values}

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

@ -3,14 +3,13 @@ import * as _ from 'lodash';
import * as moment from 'moment';
import Card from '../../Card';
import ResponsiveContainer from '../../ResponsiveContainer';
import { GenericComponent, IGenericProps, IGenericState } from '../GenericComponent';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts';
import colors from '../../colors';
const { ThemeColors } = colors;
import settings from './Settings';
interface IBarProps extends IGenericProps {
props: {
barProps: { [key: string]: Object };
@ -18,26 +17,30 @@ interface IBarProps extends IGenericProps {
/** The name of the property in the data source that contains the name for the X axis */
nameKey: string;
};
};
}
interface IBarState extends IGenericState {
values: Object[];
bars: Object[];
values: any[];
bars: any[];
}
export default class BarData extends GenericComponent<IBarProps, IBarState> {
static editor = settings;
state = {
values: [],
bars: []
};
static fromSource(source: string) {
return {
values: GenericComponent.sourceFormat(source, 'bar-values'),
bars: GenericComponent.sourceFormat(source, 'bars')
};
}
constructor(props: any) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.state = {
values: [],
bars: []
};
}
handleClick(data: any, index: number) {
@ -45,9 +48,11 @@ export default class BarData extends GenericComponent<IBarProps, IBarState> {
}
render() {
var { values, bars } = this.state;
var { title, subtitle, props } = this.props;
var { barProps, showLegend, nameKey } = props;
let { values, bars } = this.state;
let { id, title, subtitle, props, layout } = this.props;
let { barProps, showLegend, nameKey } = props;
nameKey = nameKey || 'value';
if (!values) {
return null;
@ -55,7 +60,7 @@ export default class BarData extends GenericComponent<IBarProps, IBarState> {
if (!values || !values.length) {
return (
<Card title={title} subtitle={subtitle}>
<Card id={id} title={title} subtitle={subtitle}>
<div style={{ padding: 20 }}>No data is available</div>
</Card>
);
@ -78,8 +83,8 @@ export default class BarData extends GenericComponent<IBarProps, IBarState> {
// Todo: Receive the width of the SVG component from the container
return (
<Card title={title} subtitle={subtitle}>
<ResponsiveContainer>
<Card id={id} title={title} subtitle={subtitle}>
<ResponsiveContainer layout={layout}>
<BarChart
data={values}
margin={{ top: 5, right: 30, left: 0, bottom: 5 }}

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

@ -1,11 +1,17 @@
import * as React from 'react';
import { GenericComponent, IGenericProps, IGenericState } from '../GenericComponent';
import * as _ from 'lodash';
import * as moment from 'moment';
import { Card, CardText } from 'react-md/lib/Cards';
import FontIcon from 'react-md/lib/FontIcons';
import Button from 'react-md/lib/Buttons/Button';
import CircularProgress from 'react-md/lib/Progress/CircularProgress';
import { GenericComponent, IGenericProps, IGenericState } from '../GenericComponent';
import Card from '../../Card';
const styles = {
autoscroll: {
overflow: 'auto',
} as React.CSSProperties
};
export interface IDetailProps extends IGenericProps {
props: {
@ -15,6 +21,7 @@ export interface IDetailProps extends IGenericProps {
value?: string,
type?: 'text' | 'time' | 'icon' | 'button',
}[]
hideBorders?: boolean;
};
}
@ -33,9 +40,9 @@ export default class Detail extends GenericComponent<IDetailProps, IDetailState>
}
render() {
var { props } = this.props;
var { cols } = props;
var { values } = this.state;
const { props, id, title } = this.props;
const { cols, hideBorders } = props;
const { values } = this.state;
if (!values) {
return <CircularProgress key="loading" id="spinner" />;
@ -67,7 +74,12 @@ export default class Detail extends GenericComponent<IDetailProps, IDetailState>
});
return (
<Card>
<Card
id={id}
title={title}
hideTitle={true}
className={hideBorders ? 'hide-borders' : ''}
contentStyle={styles.autoscroll}>
{lists}
</Card>
);

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

@ -40,10 +40,11 @@ export default class Dialog extends React.PureComponent<IDialogProps, IDialogSta
this.onChange = this.onChange.bind(this);
// Create dialog data source
var dialogDS: IDataSource = {
var dialogDS: DataSource = {
id: 'dialog_' + this.props.dialogData.id,
type: 'Constant',
params: {
values: [],
selectedValue: null
}
};
@ -63,6 +64,10 @@ export default class Dialog extends React.PureComponent<IDialogProps, IDialogSta
DialogsStore.listen(this.onChange);
}
componentWillUnmount() {
DialogsStore.unlisten(this.onChange);
}
componentDidUpdate() {
const { dialogData } = this.props;
var { dialogId, dialogArgs } = this.state;
@ -132,8 +137,8 @@ export default class Dialog extends React.PureComponent<IDialogProps, IDialogSta
title={title}
focusOnMount={false}
onHide={this.closeDialog}
dialogStyle={{ width: dialogData.width || '80%' }}
contentStyle={{ padding: '0', maxHeight: 'calc(100vh - 148px)' }}
dialogStyle={{ width: dialogData.width || '80%', overflow: 'auto' }}
contentStyle={{ padding: '0' }}
>
<ResponsiveReactGridLayout
{...grid}

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

@ -6,9 +6,6 @@ interface IDialogsActions {
}
class DialogsActions extends AbstractActions implements IDialogsActions {
constructor(alt: AltJS.Alt) {
super(alt);
}
openDialog(dialogName: string, args: { [id: string]: Object }) {
return { dialogName, args };

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

@ -43,6 +43,6 @@ class DialogsStore extends AbstractStoreModel<IDialogsStoreState> implements IDi
}
}
const dialogsStore = alt.createStore<IDialogsStoreState>(DialogsStore, 'DialogsStore');
const dialogsStore = alt.createStore<IDialogsStoreState>(DialogsStore as AltJS.StoreModel<any>, 'DialogsStore');
export default dialogsStore;

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

@ -22,4 +22,4 @@ export {
Dialog,
DialogsActions,
DialogsStore
}
};

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

@ -23,6 +23,15 @@ export abstract class GenericComponent<T1 extends IGenericProps, T2 extends IGen
private id: string = null;
static sourceFormat(source: string, variable: string) {
return source + ((source || '').indexOf(':') >= 0 ? '-' : ':') + variable;
}
static sourceAction(source: string, variable: string, action: string) {
let sourceFormat = GenericComponent.sourceFormat(source, variable).split(':');
return sourceFormat.join(`:${action}:`);
}
constructor(props: T1) {
super(props);
@ -87,24 +96,6 @@ export abstract class GenericComponent<T1 extends IGenericProps, T2 extends IGen
abstract render();
/**
* returns boolean option from state, passed props or default values (in that order).
* @param property name of property
*/
protected is(property: string): boolean {
if (this.state[property] !== undefined && typeof(this.state[property]) === 'boolean') {
return this.state[property];
}
let { props } = this.props;
if (props && props[property] !== undefined && typeof(props[property]) === 'boolean') {
return props[property] as boolean;
}
if (this.props[property] !== undefined && typeof(this.props[property]) === 'boolean') {
return this.props[property];
}
return false;
}
private onStateChange(state: any) {
var result = DataSourceConnector.extrapolateDependencies(this.props.dependencies);
var updatedState: IGenericState = {};

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше