|
@ -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
|
|
@ -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
|
||||
|
|
28
.travis.yml
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# yarn build
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
(cd client; CI=true yarn lint)
|
||||
(cd client; CI=true yarn test)
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
(cd server; yarn install)
|
||||
(cd client; yarn install; yarn run css:build)
|
|
@ -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"
|
||||
}
|
|
@ -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" ]
|
|
@ -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
|
||||
```
|
||||
|
||||
## What’s 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"
|
||||
}
|
После Ширина: | Высота: | Размер: 7.7 KiB |
После Ширина: | Высота: | Размер: 4.1 KiB |
После Ширина: | Высота: | Размер: 8.7 KiB |
После Ширина: | Высота: | Размер: 16 KiB |
После Ширина: | Высота: | Размер: 19 KiB |
После Ширина: | Высота: | Размер: 36 KiB |
После Ширина: | Высота: | Размер: 13 KiB |
После Ширина: | Высота: | Размер: 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>
|
|
@ -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":""}
|
|
@ -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>;
|
|
@ -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);
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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[],
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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 = {};
|