├── .DS_Store ├── .deployment ├── .editorconfig ├── .travis.yml ├── .travis ├── ci.sh ├── coverage.sh ├── push.sh └── setup.sh ├── .vscode ├── launch.json └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── build ├── asset-manifest.json ├── favicon.ico ├── images │ ├── application-insights.png │ ├── azure.png │ ├── bot-ai-base.png │ ├── bot-ai-cs.png │ ├── bot-framework-preview.png │ ├── bot-instrumented.png │ ├── default.png │ ├── human-to-bot-handoff.png │ └── sample.png ├── index.html ├── service-worker.js └── static │ ├── css │ ├── main.526645cd.css │ └── main.526645cd.css.map │ └── js │ ├── main.462c17f8.js │ └── main.462c17f8.js.map ├── client ├── @types │ ├── ai.d.ts │ ├── botframework.d.ts │ ├── constant.d.ts │ ├── cosmosdb.d.ts │ ├── sample.d.ts │ └── types.d.ts ├── package.json ├── public │ ├── favicon.ico │ ├── images │ │ └── bot-framework-preview.png │ └── index.html ├── src │ ├── actions │ │ ├── AccountActions.ts │ │ ├── ConfigurationsActions.ts │ │ ├── ConnectionsActions.ts │ │ ├── SettingsActions.ts │ │ ├── SetupActions.ts │ │ └── VisibilityActions.ts │ ├── alt.ts │ ├── components │ │ ├── App.tsx │ │ ├── AutoRefreshSelector │ │ │ ├── AutoRefreshSelector.tsx │ │ │ ├── RefreshActions.ts │ │ │ ├── RefreshStore.ts │ │ │ └── index.tsx │ │ ├── Card │ │ │ ├── Card.tsx │ │ │ ├── Settings │ │ │ │ ├── CardSettingsActions.tsx │ │ │ │ ├── CardSettingsStore.tsx │ │ │ │ ├── Settings.tsx │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── Dashboard │ │ │ ├── DownloadFile.tsx │ │ │ ├── Editor │ │ │ │ ├── Editor.tsx │ │ │ │ ├── EditorActions.ts │ │ │ │ ├── EditorStore.ts │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── ElementConnector.tsx │ │ ├── Home │ │ │ ├── IconPicker.tsx │ │ │ └── index.tsx │ │ ├── Navbar │ │ │ ├── index.tsx │ │ │ └── style.scss │ │ ├── ResponsiveContainer.tsx │ │ ├── Settings │ │ │ ├── ConnectionsSettings │ │ │ │ └── index.tsx │ │ │ ├── SettingsButton.tsx │ │ │ ├── SetupDashboard.tsx │ │ │ └── index.tsx │ │ ├── Setup │ │ │ └── index.tsx │ │ ├── Spinner │ │ │ ├── Spinner.tsx │ │ │ ├── SpinnerActions.ts │ │ │ ├── SpinnerStore.ts │ │ │ └── index.tsx │ │ ├── Toast │ │ │ ├── Toast.tsx │ │ │ ├── ToastActions.ts │ │ │ ├── ToastStore.ts │ │ │ └── index.ts │ │ ├── Tooltip │ │ │ ├── Tooltip.tsx │ │ │ ├── TooltipFontIcon.tsx │ │ │ └── index.tsx │ │ ├── colors │ │ │ └── index.ts │ │ ├── common │ │ │ ├── BaseDatasourceSettings.tsx │ │ │ ├── InfoDrawer.tsx │ │ │ └── TokenInput.tsx │ │ └── generic │ │ │ ├── Area │ │ │ └── index.tsx │ │ │ ├── BarData │ │ │ └── index.tsx │ │ │ ├── CheckboxFilter.tsx │ │ │ ├── DatePickerFilter.tsx │ │ │ ├── Detail │ │ │ ├── Detail.tsx │ │ │ └── index.ts │ │ │ ├── Dialogs │ │ │ ├── Dialog.tsx │ │ │ ├── DialogsActions.ts │ │ │ ├── DialogsStore.ts │ │ │ └── index.tsx │ │ │ ├── GenericComponent.tsx │ │ │ ├── MapData.tsx │ │ │ ├── MenuFilter.tsx │ │ │ ├── PieData │ │ │ └── index.tsx │ │ │ ├── RadarChartCard.tsx │ │ │ ├── RadialBarChartCard.tsx │ │ │ ├── RequestButton.tsx │ │ │ ├── Scatter │ │ │ └── index.tsx │ │ │ ├── Scorecard │ │ │ └── index.tsx │ │ │ ├── SimpleRadialBarChartCard.tsx │ │ │ ├── SplitPanel │ │ │ ├── SplitPanel.tsx │ │ │ └── index.ts │ │ │ ├── Table │ │ │ ├── Table.tsx │ │ │ └── index.ts │ │ │ ├── TextFilter.tsx │ │ │ ├── Timeline │ │ │ └── index.tsx │ │ │ ├── generic.scss │ │ │ └── plugins.tsx │ ├── constants │ │ └── icons.tsx │ ├── data-sources │ │ ├── DataSourceConnector.ts │ │ ├── connections │ │ │ ├── Connection.tsx │ │ │ ├── application-insights │ │ │ │ ├── QueryTester.tsx │ │ │ │ ├── application-insights.tsx │ │ │ │ └── index.tsx │ │ │ ├── azure.tsx │ │ │ ├── bot-framework.tsx │ │ │ ├── cosmos-db.tsx │ │ │ ├── graphql.tsx │ │ │ └── index.tsx │ │ ├── index.ts │ │ └── plugins │ │ │ ├── ApplicationInsights │ │ │ ├── ApplicationInsightsApi.ts │ │ │ ├── Query.ts │ │ │ └── common.ts │ │ │ ├── Azure.ts │ │ │ ├── BotFramework │ │ │ └── DirectLine.ts │ │ │ ├── Constant │ │ │ ├── Settings.tsx │ │ │ └── index.ts │ │ │ ├── CosmosDB │ │ │ └── Query.ts │ │ │ ├── DataSourcePlugin.ts │ │ │ ├── GraphQL.ts │ │ │ ├── PluginsMapping.ts │ │ │ ├── Sample.ts │ │ │ └── index.ts │ ├── index.scss │ ├── index.tsx │ ├── logo.svg │ ├── pages │ │ ├── Dashboard.tsx │ │ ├── Home.tsx │ │ ├── NotFound.tsx │ │ └── Setup.tsx │ ├── routes.tsx │ ├── setupTests.ts │ ├── stores │ │ ├── AccountStore.ts │ │ ├── ConfigurationsStore.ts │ │ ├── ConnectionsStore.ts │ │ ├── SettingsStore.ts │ │ ├── SetupStore.ts │ │ └── VisibilityStore.ts │ ├── tests │ │ ├── common │ │ │ ├── TokenInput.test.tsx │ │ │ └── infoDrawer.test.tsx │ │ ├── data-formats │ │ │ ├── data-formats.test.ts │ │ │ └── formats │ │ │ │ ├── bars.ts │ │ │ │ ├── filter.ts │ │ │ │ ├── filtered_samples.ts │ │ │ │ ├── flags.ts │ │ │ │ ├── formats.d.ts │ │ │ │ ├── index.ts │ │ │ │ ├── retention.ts │ │ │ │ ├── scorecard.ts │ │ │ │ ├── timeline.ts │ │ │ │ └── timespan.ts │ │ ├── data-sources │ │ │ ├── ApplicationInsights.Fail.test.tsx │ │ │ ├── ApplicationInsights.Query.Forked.test.ts │ │ │ ├── ApplicationInsights.Query.test.ts │ │ │ ├── Constants.test.ts │ │ │ ├── CosmosDB.Query.test.ts │ │ │ ├── DirectLine.Query.test.ts │ │ │ ├── Samples.test.ts │ │ │ └── __snapshots__ │ │ │ │ ├── CosmosDB.Query.test.ts.snap │ │ │ │ └── DirectLine.Query.test.ts.snap │ │ ├── elements │ │ │ ├── Area.test.tsx │ │ │ ├── AutoRefreshSelector.test.tsx │ │ │ ├── CardSettings.test.tsx │ │ │ ├── CardSettingsStore.test.tsx │ │ │ ├── Dialog.test.tsx │ │ │ ├── IconPicker.test.tsx │ │ │ ├── Pie.test.tsx │ │ │ ├── Scorecard.test.tsx │ │ │ ├── Spinner.test.tsx │ │ │ ├── SplitPanel.test.tsx │ │ │ ├── Table.test.tsx │ │ │ ├── Timeline.test.tsx │ │ │ └── Toast.test.tsx │ │ ├── mocks │ │ │ ├── dashboards │ │ │ │ ├── application-insights-fail.ts │ │ │ │ ├── application-insights-forked.ts │ │ │ │ ├── application-insights.ts │ │ │ │ ├── area.ts │ │ │ │ ├── bot-framework-directline.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── cosmosdb.ts │ │ │ │ ├── dashboard.ts │ │ │ │ ├── dialogs.ts │ │ │ │ ├── pie.ts │ │ │ │ ├── rich-samples.ts │ │ │ │ ├── samples.ts │ │ │ │ ├── scorecard.ts │ │ │ │ ├── splitpanel.ts │ │ │ │ ├── table.ts │ │ │ │ ├── timeline.ts │ │ │ │ ├── timespan.ts │ │ │ │ └── utils.ts │ │ │ ├── dialog.ts │ │ │ └── requests │ │ │ │ ├── account.ts │ │ │ │ ├── application-insights │ │ │ │ ├── index.ts │ │ │ │ ├── query.24h.mock.ts │ │ │ │ └── query.30d.mock.ts │ │ │ │ ├── configuration.ts │ │ │ │ ├── cosmosdb │ │ │ │ └── index.ts │ │ │ │ └── directline │ │ │ │ └── index.ts │ │ ├── pages │ │ │ ├── Dashboard.test.tsx │ │ │ ├── Home.test.tsx │ │ │ ├── HomePage.test.tsx │ │ │ ├── NavBar.test.tsx │ │ │ ├── NotFound.test.tsx │ │ │ └── Setup.test.tsx │ │ ├── stores │ │ │ ├── Account.test.tsx │ │ │ ├── Configuration.test.tsx │ │ │ ├── Connection.test.ts │ │ │ ├── Settings.test.ts │ │ │ ├── Setup.test.ts │ │ │ └── __snapshots__ │ │ │ │ └── Setup.test.ts.snap │ │ └── utils │ │ │ ├── __snapshots__ │ │ │ └── utils.test.ts.snap │ │ │ ├── setup.ts │ │ │ ├── utils.test.ts │ │ │ └── utils.test.tsx │ └── utils │ │ ├── data-formats │ │ ├── common.ts │ │ ├── formats │ │ │ ├── bars.ts │ │ │ ├── filter.ts │ │ │ ├── filtered-samples.ts │ │ │ ├── flags.ts │ │ │ ├── pie.ts │ │ │ ├── retention.ts │ │ │ ├── scorecard.ts │ │ │ ├── timeline.ts │ │ │ └── timespan.ts │ │ └── index.ts │ │ └── index.ts ├── tsconfig.json ├── tsconfig.test.json ├── tslint.json └── yarn.lock ├── docs ├── DASHBOARD-SCHEMA.md ├── README.md ├── actions.md ├── add-new-data-source.md ├── add-new-element.md ├── bot-framedash-msgs.png ├── bot-framedash.png ├── components │ ├── area.md │ ├── bar.md │ ├── detail.md │ ├── pie.md │ ├── scatter.md │ ├── scorecard.md │ ├── splitpanel.md │ ├── table.md │ └── timeline.md ├── dashboard-creation.md ├── data-formats │ └── README.md ├── data-sources │ ├── bot-framework.md │ └── cosmos-db.md ├── dialog.md ├── filter.md ├── images │ ├── bot-fmk-dashboard-intent.png │ ├── bot-fmk-dashboard-msgs.png │ └── bot-fmk-dashboard.png ├── requestbutton.md ├── scorecard.md └── two-modes-element.md ├── package.json ├── scripts └── deployment │ ├── azure-deploy.cmd │ ├── dashboard.template.json │ ├── webapp │ └── azuredeploy.json │ └── webapponlinux │ └── azuredeploy.json ├── server ├── app.js ├── common │ └── paths.js ├── config │ ├── auth.basic.json │ └── setup.initial.json ├── dashboards │ └── preconfigured │ │ ├── MBF.Advanced.Analytics.ts │ │ ├── MBF.Advanced.Health.ts │ │ ├── app-insights-web-app.ts │ │ ├── application-insights-sample.ts │ │ ├── azure.ts │ │ ├── bot-framework-goals.ts │ │ ├── bot-framework-inst.ts │ │ ├── bot-framework.ts │ │ ├── human-handoff.ts │ │ ├── qna.dashboard.ts │ │ ├── sample-with-dialog.ts │ │ ├── sample-with-filter.ts │ │ └── sample.ts ├── helpers │ ├── resourceFieldProvider.js │ └── resourceFileProvider.js ├── index.js ├── package.json ├── routes │ ├── api.js │ ├── applicationInsights.js │ ├── auth.js │ ├── azure.js │ ├── cosmos-db.js │ └── graphql.js └── yarn.lock └── yarn.lock /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevstar/Dashborad/84d34bee2227de8742a0990deaf0e8a1941ce619/.DS_Store -------------------------------------------------------------------------------- /.deployment: -------------------------------------------------------------------------------- 1 | [config] 2 | SCM_COMMAND_IDLE_TIMEOUT=7200 3 | command = ./scripts/deployment/azure-deploy.cmd -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Indentation override for all JS under src directory 7 | [src/**.{js,jsx,ts,tsx}] 8 | indent_style = space 9 | indent_size = 2 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: 2 | node_js 3 | 4 | node_js: 5 | - "7" 6 | 7 | install: 8 | - .travis/setup.sh 9 | 10 | script: 11 | - .travis/ci.sh 12 | - .travis/coverage.sh 13 | 14 | after_success: 15 | - .travis/push.sh -------------------------------------------------------------------------------- /.travis/ci.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CI=true yarn lint 4 | CI=true yarn test -------------------------------------------------------------------------------- /.travis/coverage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | min_coverage="${MIN_COVERAGE:-42}" 4 | line_coverage="$(CI=true yarn coverage | grep '^All files *|' | cut -d'|' -f5 | tr -d ' ' | cut -d'.' -f1)" 5 | 6 | if [ ${line_coverage} -lt ${min_coverage} ]; then 7 | echo "Got test coverage of ${line_coverage} which is less than configured minimum of ${min_coverage}" >&2 8 | exit 1 9 | else 10 | echo "Got test coverage of ${line_coverage}, well done" >&2 11 | exit 0 12 | fi -------------------------------------------------------------------------------- /.travis/push.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | readonly GITHUB_ORG="${GITHUB_ORG:-CatalystCode}" 6 | readonly GITHUB_REPO="${GITHUB_REPO:-ibex-dashboard}" 7 | readonly TARGET_BRANCH="${TARGET_BRANCH:-master}" 8 | readonly SOURCE_BRANCH="${SOURCE_BRANCH:-ibex-version-1.0}" 9 | 10 | readonly AUTOCOMMIT_NAME="Travis CI" 11 | readonly AUTOCOMMIT_EMAIL="travis@travis-ci.org" 12 | readonly AUTOCOMMIT_BRANCH="temp" 13 | 14 | log() { 15 | echo "$@" >&2 16 | } 17 | 18 | ensure_preconditions_met() { 19 | if [ -z "${TRAVIS_PULL_REQUEST_BRANCH}" ]; then 20 | log "Job is CI for a push, skipping creation of production build" 21 | exit 0 22 | fi 23 | if [ "${TRAVIS_BRANCH}_${TRAVIS_PULL_REQUEST_BRANCH}" != "${TARGET_BRANCH}_${SOURCE_BRANCH}" ]; then 24 | log "Skipping creation of production build" 25 | log "We only create production builds for pull requests from '${SOURCE_BRANCH}' to '${TARGET_BRANCH}'" 26 | log "but this pull request is from '${TRAVIS_PULL_REQUEST BRANCH}' to '${TRAVIS_BRANCH}'" 27 | exit 0 28 | fi 29 | if [ -z "${GITHUB_TOKEN}" ]; then 30 | log "GITHUB_TOKEN not set: won't be able to push production build" 31 | log "Please configure the token in .travis.yml or the Travis UI" 32 | exit 1 33 | fi 34 | } 35 | 36 | create_production_build() { 37 | CI="" yarn build 38 | } 39 | 40 | setup_git() { 41 | git config user.name "${AUTOCOMMIT_NAME}" 42 | git config user.email "${AUTOCOMMIT_EMAIL}" 43 | git remote add origin-travis "https://${GITHUB_TOKEN}@github.com/${GITHUB_ORG}/${GITHUB_REPO}.git" 44 | } 45 | 46 | commit_build_files() { 47 | git checkout -b "${AUTOCOMMIT_BRANCH}" 48 | git add --all build 49 | echo -e "Travis build: ${TRAVIS_BUILD_NUMBER}\n\nhttps://travis-ci.org/${GITHUB_ORG}/${GITHUB_REPO}/builds/${TRAVIS_BUILD_ID}" | git commit --file - 50 | } 51 | 52 | push_to_github() { 53 | git push origin-travis "${AUTOCOMMIT_BRANCH}:${SOURCE_BRANCH}" 54 | } 55 | 56 | ensure_preconditions_met 57 | create_production_build 58 | setup_git 59 | commit_build_files 60 | push_to_github -------------------------------------------------------------------------------- /.travis/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | yarn install 4 | yarn run css:build -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Server", 11 | "program": "${workspaceRoot}/server/index.js", 12 | "outFiles": [] 13 | }, 14 | { 15 | "type": "node", 16 | "request": "launch", 17 | "name": "Launch Program", 18 | "program": "${workspaceRoot}/client:start", 19 | "outFiles": [] 20 | }, 21 | { 22 | "type": "node", 23 | "request": "attach", 24 | "name": "Attach to Process", 25 | "address": "localhost", 26 | "port": 5858, 27 | "outFiles": [] 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "**/*.css": { "when": "$(basename).scss"} 5 | }, 6 | "editor.tabSize": 2, 7 | "editor.insertSpaces": true, 8 | "editor.detectIndentation": false, 9 | "editor.rulers": [ 10 | 120 11 | ], 12 | "search.exclude": { 13 | "**/.git": true, 14 | "**/node_modules": true, 15 | "**/build": true, 16 | "**/coverage": true 17 | } 18 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8.2.1-alpine 2 | 3 | # Create app directory 4 | RUN mkdir -p /usr/src/app 5 | RUN mkdir -p /usr/src/app/client 6 | RUN mkdir -p /usr/src/app/server 7 | WORKDIR /usr/src/app 8 | 9 | # Install app dependencies 10 | COPY yarn.lock /usr/src/app/ 11 | COPY package.json /usr/src/app/ 12 | 13 | COPY server/yarn.lock /usr/src/app/server 14 | COPY server/package.json /usr/src/app/server 15 | 16 | COPY client/yarn.lock /usr/src/app/client 17 | COPY client/package.json /usr/src/app/client 18 | 19 | RUN npm install yarn -g 20 | RUN yarn 21 | 22 | # Bundle app source 23 | COPY . /usr/src/app 24 | 25 | # Build client assets 26 | WORKDIR /usr/src/app/client 27 | RUN yarn build 28 | 29 | WORKDIR /usr/src/app/ 30 | CMD [ "npm", "start" ] 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Elad Iwanir 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "main.css": "static/css/main.526645cd.css", 3 | "main.css.map": "static/css/main.526645cd.css.map", 4 | "main.js": "static/js/main.462c17f8.js", 5 | "main.js.map": "static/js/main.462c17f8.js.map" 6 | } -------------------------------------------------------------------------------- /build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevstar/Dashborad/84d34bee2227de8742a0990deaf0e8a1941ce619/build/favicon.ico -------------------------------------------------------------------------------- /build/images/application-insights.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevstar/Dashborad/84d34bee2227de8742a0990deaf0e8a1941ce619/build/images/application-insights.png -------------------------------------------------------------------------------- /build/images/azure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevstar/Dashborad/84d34bee2227de8742a0990deaf0e8a1941ce619/build/images/azure.png -------------------------------------------------------------------------------- /build/images/bot-ai-base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevstar/Dashborad/84d34bee2227de8742a0990deaf0e8a1941ce619/build/images/bot-ai-base.png -------------------------------------------------------------------------------- /build/images/bot-ai-cs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevstar/Dashborad/84d34bee2227de8742a0990deaf0e8a1941ce619/build/images/bot-ai-cs.png -------------------------------------------------------------------------------- /build/images/bot-framework-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevstar/Dashborad/84d34bee2227de8742a0990deaf0e8a1941ce619/build/images/bot-framework-preview.png -------------------------------------------------------------------------------- /build/images/bot-instrumented.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevstar/Dashborad/84d34bee2227de8742a0990deaf0e8a1941ce619/build/images/bot-instrumented.png -------------------------------------------------------------------------------- /build/images/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevstar/Dashborad/84d34bee2227de8742a0990deaf0e8a1941ce619/build/images/default.png -------------------------------------------------------------------------------- /build/images/human-to-bot-handoff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevstar/Dashborad/84d34bee2227de8742a0990deaf0e8a1941ce619/build/images/human-to-bot-handoff.png -------------------------------------------------------------------------------- /build/images/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevstar/Dashborad/84d34bee2227de8742a0990deaf0e8a1941ce619/build/images/sample.png -------------------------------------------------------------------------------- /build/index.html: -------------------------------------------------------------------------------- 1 | React App
-------------------------------------------------------------------------------- /client/@types/ai.d.ts: -------------------------------------------------------------------------------- 1 | interface IQueryResults { 2 | Tables: IQueryResult[] 3 | } 4 | 5 | interface IQueryResult { 6 | TableName: string, 7 | Columns: { 8 | ColumnName: string, 9 | DataType: string, 10 | ColumnType: string 11 | }[], 12 | Rows: any[][] 13 | } 14 | 15 | interface IQueryStatus { 16 | Ordinal: number, 17 | Kind: string, 18 | Name: string, 19 | Id: string 20 | } 21 | 22 | /** 23 | * ==================================== 24 | * Template definitions 25 | * ==================================== 26 | */ 27 | 28 | /** 29 | * Application Insights data source definition 30 | */ 31 | interface AIDataSource extends IDataSource { 32 | type: 'ApplicationInsights/Query', 33 | dependencies: { 34 | /** 35 | * Required - to use in all queries to app insights as a basic timespan parameter 36 | */ 37 | queryTimespan: string, 38 | /** 39 | * Optional - from which 'queryTimespan' is derived 40 | */ 41 | timespan?: string, 42 | /** 43 | * Used for queries that require granularity (like timeline) 44 | */ 45 | granularity?: string 46 | }, 47 | params: AIQueryParams | AIForkedQueryParams; 48 | } 49 | 50 | /** 51 | * A simple query on application insights data source 52 | */ 53 | interface AIQueryParams { 54 | query: AIQuery, 55 | mappings?: AIMapping 56 | } 57 | 58 | /** 59 | * A forked query that aggregates several queries into a single API call 60 | */ 61 | interface AIForkedQueryParams { 62 | /** 63 | * The table on which to perform the forken query 64 | */ 65 | table: string, 66 | queries: IDict<{ 67 | query: AIQuery, 68 | mappings?: AIMapping, 69 | filters?: Array, 70 | calculated?: (state: any, dependencies?: any, prevState?: any) => any 71 | }>, 72 | } 73 | 74 | type AIQuery = string | (() => string) | ((dependencies: any) => string); 75 | type AIMapping = IDict<(value: any, row: any, idx: number) => string | number | boolean>; 76 | -------------------------------------------------------------------------------- /client/@types/botframework.d.ts: -------------------------------------------------------------------------------- 1 | interface IQueryResult { 2 | value: string 3 | } 4 | 5 | /** 6 | * ==================================== 7 | * Template definitions 8 | * ==================================== 9 | */ 10 | 11 | /** 12 | * Application Insights data source definition 13 | */ 14 | interface BotFrameworkDataSource extends IDataSource { 15 | type: 'BotFramework/DirectLine', 16 | dependencies: { 17 | /** 18 | * Required - to use in all queries to app insights as a basic timespan parameter 19 | */ 20 | queryTimespan: string, 21 | /** 22 | * Optional - from which 'queryTimespan' is derived 23 | */ 24 | timespan?: string, 25 | /** 26 | * Used for queries that require granularity (like timeline) 27 | */ 28 | granularity?: string 29 | }, 30 | params: BotFrameworkQueryParams; 31 | } 32 | 33 | /** 34 | * A simple query on application insights data source 35 | */ 36 | interface BotFrameworkQueryParams { 37 | } 38 | 39 | type BotFrameworkQuery = string | (() => string) | ((dependencies: any) => string); 40 | -------------------------------------------------------------------------------- /client/@types/constant.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A data source to hold unchanging data 3 | */ 4 | interface ConstantDataSource extends IDataSource { 5 | type: 'Constant', 6 | params: { 7 | /** 8 | * List of values to choose from 9 | */ 10 | values: any[], 11 | /** 12 | * Current selected value (usually used in constant filters) 13 | */ 14 | selectedValue: string 15 | } 16 | } -------------------------------------------------------------------------------- /client/@types/cosmosdb.d.ts: -------------------------------------------------------------------------------- 1 | interface IQueryResult { 2 | value: string 3 | } 4 | 5 | /** 6 | * ==================================== 7 | * Template definitions 8 | * ==================================== 9 | */ 10 | 11 | /** 12 | * Application Insights data source definition 13 | */ 14 | interface CosmosDBDataSource extends IDataSource { 15 | type: 'CosmosDB/Query', 16 | dependencies: { 17 | /** 18 | * Required - to use in all queries to app insights as a basic timespan parameter 19 | */ 20 | queryTimespan: string, 21 | /** 22 | * Optional - from which 'queryTimespan' is derived 23 | */ 24 | timespan?: string, 25 | /** 26 | * Used for queries that require granularity (like timeline) 27 | */ 28 | granularity?: string 29 | }, 30 | params: CosmosDBQueryParams; 31 | } 32 | 33 | /** 34 | * A simple query on application insights data source 35 | */ 36 | interface CosmosDBQueryParams { 37 | } 38 | 39 | type CosmosDBQuery = string | (() => string) | ((dependencies: any) => string); 40 | -------------------------------------------------------------------------------- /client/@types/sample.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A data source to hold unchanging data 3 | */ 4 | interface SampleDataSource extends IDataSource { 5 | type: 'Sample', 6 | params?: { 7 | /** 8 | * All values in this dictionary will be available as dependencies from this data source 9 | */ 10 | samples: { 11 | [key: string]: any, 12 | /** 13 | * List of sample values 14 | */ 15 | values?: any[], 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ibex-dashboard-client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "proxy": "http://localhost:4000/", 6 | "devDependencies": { 7 | "@types/alt": "^0.16.32", 8 | "@types/jest": "^19.2.2", 9 | "@types/lodash": "^4.14.55", 10 | "@types/nock": "^8.2.1", 11 | "@types/node": "^7.0.8", 12 | "@types/react": "^15.0.16", 13 | "@types/react-addons-test-utils": "^0.14.17", 14 | "@types/react-dom": "^0.14.23", 15 | "@types/react-router": "^3.0.8", 16 | "alt": "^0.18.6", 17 | "alt-utils": "^1.0.0", 18 | "leaflet": "^1.2.0", 19 | "leaflet-geosearch": "^2.6.0", 20 | "leaflet.markercluster": "^1.2.0", 21 | "nock": "^9.0.9", 22 | "node-sass": "^4.5.3", 23 | "npm-run-all": "^4.0.2", 24 | "react": "^15.4.2", 25 | "react-addons-css-transition-group": "^15.4.2", 26 | "react-addons-test-utils": "^15.4.2", 27 | "react-addons-transition-group": "^15.4.2", 28 | "react-dom": "^15.4.2", 29 | "react-grid-layout": "^0.14.7", 30 | "react-leaflet": "^1.7.8", 31 | "react-leaflet-div-icon": "^1.1.0", 32 | "react-leaflet-markercluster": "^1.1.8", 33 | "react-md": "^1.0.18", 34 | "react-render-html": "^0.1.6", 35 | "react-router": "3.0.0", 36 | "react-scripts-ts": "^2.6.0", 37 | "recharts": "^0.21.2", 38 | "tslint": "^5.8.0" 39 | }, 40 | "dependencies": { 41 | "body-parser": "^1.17.1", 42 | "cookie-parser": "^1.4.3", 43 | "express": "^4.15.2", 44 | "express-session": "^1.15.2", 45 | "lodash": "^4.17.4", 46 | "lodash.throttle": "^4.1.1", 47 | "material-colors": "^1.2.5", 48 | "moment": "^2.18.0", 49 | "morgan": "^1.8.1", 50 | "ms-rest-azure": "^2.1.2", 51 | "passport": "^0.3.2", 52 | "passport-azure-ad": "^3.0.5", 53 | "react-ace": "^5.0.1", 54 | "react-date-range": "^0.9.4", 55 | "react-json-tree": "^0.10.9", 56 | "xhr-request": "^1.0.1" 57 | }, 58 | "scripts": { 59 | "css:build": "node-sass src/ -o src/", 60 | "css:watch": "yarn run css:build && node-sass src/ -o src/ --watch --recursive", 61 | "client:start": "react-scripts-ts start", 62 | "start": "npm-run-all -p css:watch client:start", 63 | "coverage": "react-scripts-ts test --env=jsdom --coverage --collectCoverageFrom=src/**/*.{ts,tsx} --collectCoverageFrom=!src/**/*.d.ts --collectCoverageFrom=!src/tests/**", 64 | "lint": "tslint src", 65 | "build": "yarn run css:build && react-scripts-ts build && rm -rf ../build/static && cp -rf build ../", 66 | "test": "react-scripts-ts test --env=jsdom" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevstar/Dashborad/84d34bee2227de8742a0990deaf0e8a1941ce619/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/images/bot-framework-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevstar/Dashborad/84d34bee2227de8742a0990deaf0e8a1941ce619/client/public/images/bot-framework-preview.png -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 18 | React App 19 | 20 | 21 |
22 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /client/src/actions/AccountActions.ts: -------------------------------------------------------------------------------- 1 | import alt, { AbstractActions } from '../alt'; 2 | import * as request from 'xhr-request'; 3 | import { ToastActions } from '../components/Toast'; 4 | import utils from '../utils'; 5 | 6 | interface IAccountActions { 7 | failure(error: any): any; 8 | updateAccount(): any; 9 | } 10 | 11 | class AccountActions extends AbstractActions implements IAccountActions { 12 | 13 | updateAccount() { 14 | 15 | return (dispatcher: (account: IDictionary) => void) => { 16 | 17 | request('/auth/account', { json: true }, (error: any, result: any) => { 18 | if (error || result && result.error) { 19 | return this.failure(error || result && result.error); 20 | } 21 | return dispatcher({ account: result.account }); 22 | } 23 | ); 24 | 25 | }; 26 | } 27 | 28 | failure(error: any) { 29 | ToastActions.addToast({ text: utils.errorToMessage(error) }); 30 | return { error }; 31 | } 32 | } 33 | 34 | const accountActions = alt.createActions(AccountActions); 35 | 36 | export default accountActions; 37 | -------------------------------------------------------------------------------- /client/src/actions/ConnectionsActions.ts: -------------------------------------------------------------------------------- 1 | import alt, { AbstractActions } from '../alt'; 2 | 3 | interface IConnectionsActions { 4 | updateConnection(connectionName: string, args: IDictionary): any; 5 | } 6 | 7 | class ConnectionsActions extends AbstractActions implements IConnectionsActions { 8 | 9 | updateConnection(connectionName: string, args: IDictionary) { 10 | return { connectionName, args }; 11 | } 12 | } 13 | 14 | const connectionsActions = alt.createActions(ConnectionsActions); 15 | 16 | export default connectionsActions; 17 | -------------------------------------------------------------------------------- /client/src/actions/SettingsActions.ts: -------------------------------------------------------------------------------- 1 | import alt, { AbstractActions } from '../alt'; 2 | 3 | interface ISettingsActions { 4 | saveSettings(): any; 5 | saveSettingsCompleted(): any; 6 | } 7 | 8 | class SettingsActions extends AbstractActions implements ISettingsActions { 9 | 10 | saveSettings() { 11 | return { }; 12 | } 13 | 14 | saveSettingsCompleted() { 15 | return { }; 16 | } 17 | } 18 | 19 | const settingsActions = alt.createActions(SettingsActions); 20 | 21 | export default settingsActions; 22 | -------------------------------------------------------------------------------- /client/src/actions/SetupActions.ts: -------------------------------------------------------------------------------- 1 | import alt, { AbstractActions } from '../alt'; 2 | import * as request from 'xhr-request'; 3 | import { IToast, ToastActions } from '../components/Toast'; 4 | 5 | interface ISetupActions { 6 | load(): any; 7 | save(setupConfig: ISetupConfig, successCallback: () => void): any; 8 | failure(error: any): void; 9 | } 10 | 11 | class SetupActions extends AbstractActions implements ISetupActions { 12 | 13 | load() { 14 | 15 | return (dispatcher: (setupConfig: ISetupConfig) => void) => { 16 | 17 | request('/api/setup', { json: true }, (setupError: any, setupConfig: ISetupConfig) => { 18 | return dispatcher(setupConfig); 19 | }); 20 | }; 21 | } 22 | 23 | save(setupConfig: ISetupConfig, successCallback: () => void) { 24 | return (dispatcher: (setupConfig: ISetupConfig) => void) => { 25 | 26 | setupConfig.stage = 'configured'; 27 | let stringConfig = JSON.stringify(setupConfig); 28 | 29 | request('/api/setup', { 30 | method: 'POST', 31 | json: true, 32 | body: { json: stringConfig } 33 | }, 34 | (setupError: any, setupJson: any) => { 35 | 36 | if (setupError) { 37 | return this.failure(setupError); 38 | } 39 | 40 | return request('/auth/init', 41 | (authError: any, authJson: any) => { 42 | 43 | if (authError) { 44 | return this.failure(authError); 45 | } 46 | 47 | let toast: IToast = { text: 'Setup was saved successfully.' }; 48 | ToastActions.addToast(toast); 49 | 50 | try { 51 | if (successCallback) { 52 | successCallback(); 53 | } 54 | } catch (e) { } 55 | 56 | return dispatcher(authJson); 57 | } 58 | ); 59 | } 60 | ); 61 | }; 62 | } 63 | 64 | failure(error: any) { 65 | return { error }; 66 | } 67 | } 68 | 69 | const setupActions = alt.createActions(SetupActions); 70 | 71 | export default setupActions; 72 | -------------------------------------------------------------------------------- /client/src/actions/VisibilityActions.ts: -------------------------------------------------------------------------------- 1 | import alt, { AbstractActions } from '../alt'; 2 | 3 | interface IVisibilityActions { 4 | setFlags(flags: IDict): any; 5 | turnFlagOn(flagName: string): any; 6 | turnFlagOff(flagName: string): any; 7 | } 8 | 9 | class VisibilityActions extends AbstractActions implements IVisibilityActions { 10 | 11 | setFlags(flags: IDict): any { 12 | return flags; 13 | } 14 | 15 | initializeFlag(flagName: string): any { 16 | 17 | } 18 | 19 | turnFlagOn(flagName: string): any { 20 | let flag = {}; 21 | flag[flagName] = true; 22 | return flag; 23 | } 24 | turnFlagOff(flagName: string): any { 25 | let flag = {}; 26 | flag[flagName] = false; 27 | return flag; 28 | } 29 | } 30 | 31 | const visibilityActions = alt.createActions(VisibilityActions); 32 | 33 | export default visibilityActions; 34 | -------------------------------------------------------------------------------- /client/src/alt.ts: -------------------------------------------------------------------------------- 1 | import * as Alt from 'alt'; 2 | export default new Alt(); 3 | 4 | export class AbstractActions implements AltJS.ActionsClass { 5 | dispatch: (...payload: Array) => void; 6 | generateActions: (...actions: string[]) => void; 7 | constructor(alt: AltJS.Alt) {} 8 | } 9 | 10 | export class AbstractStoreModel implements AltJS.StoreModel { 11 | bindListeners: (obj: any) => void; 12 | exportPublicMethods: (config: { [key: string]: (...args: Array) => any }) => any; 13 | exportAsync: (source: any) => void; 14 | waitFor: any; 15 | exportConfig: any; 16 | getState: () => S; 17 | } -------------------------------------------------------------------------------- /client/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Navbar from './Navbar'; 3 | import { Toast } from './Toast'; 4 | 5 | class App extends React.Component { 6 | 7 | render() { 8 | 9 | var { children } = this.props; 10 | 11 | return ( 12 |
13 | 14 | {children} 15 | 16 | 17 |
18 | ); 19 | } 20 | } 21 | 22 | export default App; -------------------------------------------------------------------------------- /client/src/components/AutoRefreshSelector/AutoRefreshSelector.tsx: -------------------------------------------------------------------------------- 1 | import * as request from 'xhr-request'; 2 | import * as React from 'react'; 3 | import * as _ from 'lodash'; 4 | 5 | import RefreshStore, { IRefreshStoreState } from './RefreshStore'; 6 | import SelectField from 'react-md/lib/SelectFields'; 7 | 8 | import RefreshActions from './RefreshActions'; 9 | 10 | import { DataSourceConnector, IDataSourceDictionary, IDataSource } from '../../data-sources/DataSourceConnector'; 11 | 12 | interface IRefreshState extends IRefreshStoreState { 13 | refreshMenuVisible?: boolean; 14 | } 15 | 16 | export default class AutoRefreshSelector extends React.Component { 17 | 18 | oneSecInMs = 1000; 19 | oneMinInMs = 60 * this.oneSecInMs; 20 | refreshIntervals = [ 21 | {text: 'None', intervalMs: -1}, 22 | {text: '30 Sec', intervalMs: 30 * this.oneSecInMs}, 23 | {text: '60 Sec', intervalMs: 60 * this.oneSecInMs}, 24 | {text: '90 Sec', intervalMs: 90 * this.oneSecInMs}, 25 | {text: '2 Min', intervalMs: 2 * this.oneMinInMs}, 26 | {text: '5 Min', intervalMs: 5 * this.oneMinInMs}, 27 | {text: '15 Min', intervalMs: 15 * this.oneMinInMs}, 28 | {text: '30 Min', intervalMs: 30 * this.oneMinInMs}, 29 | ]; 30 | 31 | constructor(props: any) { 32 | super(props); 33 | 34 | this.state = RefreshStore.getState(); 35 | 36 | this.handleRefreshIntervalChange = this.handleRefreshIntervalChange.bind(this); 37 | } 38 | 39 | handleRefreshIntervalChange = (refreshInterval: string) => { 40 | var oneSec = 1000; 41 | var interval = this.refreshIntervals.find((x) => { return x.text === refreshInterval; }).intervalMs; 42 | 43 | RefreshActions.updateInterval(interval); 44 | RefreshActions.setRefreshTimer( 45 | interval, 46 | DataSourceConnector.refreshDs); 47 | } 48 | 49 | render () { 50 | 51 | let refreshDropDownTexts = this.refreshIntervals.map((x) => { return x.text; }); 52 | return ( 53 | 64 | ); 65 | } 66 | } -------------------------------------------------------------------------------- /client/src/components/AutoRefreshSelector/RefreshActions.ts: -------------------------------------------------------------------------------- 1 | import alt, { AbstractActions } from '../../alt'; 2 | import * as request from 'xhr-request'; 3 | 4 | interface IRefreshActions { 5 | updateInterval(newInterval: number): { refreshInterval: number }; 6 | setRefreshTimer(newInterval: any, cb: any): void; 7 | } 8 | 9 | class RefreshActions extends AbstractActions implements IRefreshActions { 10 | 11 | private runningRefreshInterval: any; 12 | 13 | constructor(altobj: AltJS.Alt) { 14 | super(altobj); 15 | 16 | this.setRefreshTimer = this.setRefreshTimer.bind(this); 17 | } 18 | 19 | updateInterval(newInterval: number) { 20 | 21 | return { refreshInterval: newInterval }; 22 | } 23 | 24 | setRefreshTimer(newInterval: any, cb: any) { 25 | return (dispatch) => { 26 | // clear any previously scheduled interval 27 | if (this.runningRefreshInterval) { 28 | clearInterval(this.runningRefreshInterval); 29 | this.runningRefreshInterval = null; 30 | } 31 | 32 | if (!newInterval || newInterval === -1) { 33 | // don't auto refresh 34 | return; 35 | } 36 | 37 | // setup a new interval 38 | var interval = setInterval( 39 | cb, 40 | newInterval); 41 | 42 | this.runningRefreshInterval = interval; 43 | }; 44 | } 45 | } 46 | const refreshActions = alt.createActions(RefreshActions); 47 | 48 | export default refreshActions; 49 | -------------------------------------------------------------------------------- /client/src/components/AutoRefreshSelector/RefreshStore.ts: -------------------------------------------------------------------------------- 1 | import alt, { AbstractStoreModel } from '../../alt'; 2 | 3 | import refreshActions from './RefreshActions'; 4 | 5 | export interface IRefreshStoreState { 6 | refreshInterval: number; 7 | } 8 | 9 | class RefreshStore extends AbstractStoreModel implements IRefreshStoreState { 10 | 11 | refreshInterval: number; 12 | 13 | constructor() { 14 | super(); 15 | 16 | this.bindListeners({ 17 | updateInterval: refreshActions.updateInterval 18 | }); 19 | } 20 | 21 | updateInterval(state: any) { 22 | this.refreshInterval = state.refreshInterval; 23 | 24 | } 25 | } 26 | 27 | const refreshStore = alt.createStore(RefreshStore as AltJS.StoreModel, 'RefreshStore'); 28 | 29 | export default refreshStore; 30 | -------------------------------------------------------------------------------- /client/src/components/AutoRefreshSelector/index.tsx: -------------------------------------------------------------------------------- 1 | import AutoRefreshSelector from './AutoRefreshSelector'; 2 | import RefreshActions from './RefreshActions'; 3 | import RefreshStore from './RefreshStore'; 4 | 5 | export { 6 | AutoRefreshSelector, 7 | RefreshActions, 8 | RefreshStore 9 | }; -------------------------------------------------------------------------------- /client/src/components/Card/Settings/CardSettingsActions.tsx: -------------------------------------------------------------------------------- 1 | import alt, { AbstractActions } from '../../../alt'; 2 | 3 | interface ICardSettingsActions { 4 | openDialog(title: string, elementId: string): IDict; 5 | closeDialog(): any; 6 | selectIndex(index: number): number; 7 | getExportData(dashboard: IDashboardConfig): IDashboardConfig; 8 | downloadData(): void; 9 | } 10 | 11 | class CardSettingsActions extends AbstractActions implements ICardSettingsActions { 12 | 13 | openDialog(title: string, elementId: string) { 14 | return {title, elementId}; 15 | } 16 | 17 | closeDialog() { 18 | return {}; 19 | } 20 | 21 | selectIndex(index: number) { 22 | return index; 23 | } 24 | 25 | getExportData(dashboard: IDashboardConfig) { 26 | return dashboard; 27 | } 28 | 29 | downloadData() { 30 | return {}; 31 | } 32 | 33 | } 34 | 35 | const cardSettingsActions = alt.createActions(CardSettingsActions); 36 | 37 | export default cardSettingsActions; 38 | -------------------------------------------------------------------------------- /client/src/components/Card/Settings/index.tsx: -------------------------------------------------------------------------------- 1 | import Settings from './Settings'; 2 | import SettingsActions from './CardSettingsActions'; 3 | import SettingsStore from './CardSettingsStore'; 4 | 5 | export { 6 | Settings, 7 | SettingsActions, 8 | SettingsStore 9 | }; -------------------------------------------------------------------------------- /client/src/components/Card/index.tsx: -------------------------------------------------------------------------------- 1 | import Card from './Card'; 2 | 3 | export default Card; -------------------------------------------------------------------------------- /client/src/components/Dashboard/DownloadFile.tsx: -------------------------------------------------------------------------------- 1 | 2 | function downloadBlob(data: string, mimeType: string, filename: string) { 3 | const blob = new Blob([data], { 4 | type: mimeType 5 | }); 6 | var el = document.createElement('a'); 7 | el.setAttribute('href', window.URL.createObjectURL(blob)); 8 | el.setAttribute('download', filename); 9 | el.style.display = 'none'; 10 | document.body.appendChild(el); 11 | el.click(); 12 | document.body.removeChild(el); 13 | } 14 | 15 | export { downloadBlob }; -------------------------------------------------------------------------------- /client/src/components/Dashboard/Editor/EditorActions.ts: -------------------------------------------------------------------------------- 1 | import alt, { AbstractActions } from '../../../alt'; 2 | import * as request from 'xhr-request'; 3 | 4 | interface IEditorActions { 5 | openDialog(): any; 6 | closeDialog(): any; 7 | loadDashboard(dashboardId: string): any; 8 | selectTheme(index: number): number; 9 | updateValue(newValue: string): string; 10 | } 11 | 12 | class EditorActions extends AbstractActions implements IEditorActions { 13 | 14 | openDialog() { 15 | return {}; 16 | } 17 | 18 | closeDialog() { 19 | return {}; 20 | } 21 | 22 | loadDashboard(dashboardId: string) { 23 | this.openDialog(); 24 | return (dispatch) => { 25 | request('/api/dashboards/' + dashboardId + '?format=raw', {}, function (err: any, data: any) { 26 | if (err) { 27 | throw err; 28 | } 29 | return dispatch( data ); 30 | }); 31 | }; 32 | } 33 | 34 | selectTheme(index: number) { 35 | return index; 36 | } 37 | 38 | updateValue(newValue: string): string { 39 | return newValue; 40 | } 41 | 42 | } 43 | 44 | const editorActions = alt.createActions(EditorActions); 45 | 46 | export default editorActions; 47 | -------------------------------------------------------------------------------- /client/src/components/Dashboard/Editor/EditorStore.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import alt, { AbstractStoreModel } from '../../../alt'; 3 | 4 | import editorActions from './EditorActions'; 5 | 6 | interface IEditorStoreState { 7 | visible: boolean; 8 | value: string; 9 | selectedTheme: number; 10 | // internal 11 | saveDisabled: boolean; 12 | } 13 | 14 | class EditorStore extends AbstractStoreModel implements IEditorStoreState { 15 | 16 | visible: boolean; 17 | value: string; 18 | selectedTheme: number; 19 | // internal 20 | saveDisabled: boolean; 21 | 22 | constructor() { 23 | super(); 24 | 25 | this.visible = false; 26 | this.value = null; 27 | this.selectedTheme = 0; 28 | 29 | this.saveDisabled = false; 30 | 31 | this.bindListeners({ 32 | openDialog: editorActions.openDialog, 33 | closeDialog: editorActions.closeDialog, 34 | loadDashboard: editorActions.loadDashboard, 35 | selectTheme: editorActions.selectTheme, 36 | updateValue: editorActions.updateValue, 37 | }); 38 | } 39 | 40 | openDialog() { 41 | this.visible = true; 42 | } 43 | 44 | closeDialog() { 45 | this.visible = false; 46 | } 47 | 48 | loadDashboard(value: string) { 49 | this.value = value; 50 | } 51 | 52 | selectTheme(index: number) { 53 | this.selectedTheme = index; 54 | } 55 | 56 | updateValue(newValue: string) { 57 | this.value = newValue; 58 | } 59 | } 60 | 61 | const editorStore = alt.createStore(EditorStore as AltJS.StoreModel, 'EditorStore'); 62 | 63 | export default editorStore; 64 | -------------------------------------------------------------------------------- /client/src/components/Dashboard/Editor/index.tsx: -------------------------------------------------------------------------------- 1 | import Editor from './Editor'; 2 | import EditorActions from './EditorActions'; 3 | import EditorStore from './EditorStore'; 4 | 5 | export { 6 | Editor, 7 | EditorActions, 8 | EditorStore 9 | }; -------------------------------------------------------------------------------- /client/src/components/Home/IconPicker.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Autocomplete from 'react-md/lib/Autocompletes'; 3 | import FontIcon from 'react-md/lib/FontIcons'; 4 | 5 | import icons from '../../constants/icons'; 6 | 7 | interface IIconPickerProps { 8 | defaultLabel?: string; 9 | defaultIcon?: string; 10 | listStyle?: React.CSSProperties; 11 | } 12 | 13 | interface IIconPickerState { 14 | label: string; 15 | icon: string; 16 | } 17 | 18 | export default class IconPicker extends React.Component { 19 | 20 | static defaultProps = { 21 | defaultLabel: 'Search icons', 22 | defaultIcon: 'dashboard', 23 | listStyle: {}, 24 | }; 25 | 26 | static listItems: any = []; 27 | 28 | constructor(props: any) { 29 | super(props); 30 | 31 | const { defaultLabel, defaultIcon } = props; 32 | 33 | this.state = { 34 | label: defaultLabel, 35 | icon: defaultIcon, 36 | }; 37 | 38 | this.onChange = this.onChange.bind(this); 39 | } 40 | 41 | getIcon() { 42 | const { icon } = this.state; 43 | // check icon value is valid 44 | if (icons.findIndex(i => i === icon) > 0) { 45 | return icon; 46 | } 47 | return 'dashboard'; 48 | } 49 | 50 | componentWillMount() { 51 | if (IconPicker.listItems.length === 0) { 52 | IconPicker.listItems = icons.map((icon) => ({ icon, leftIcon: {icon} })); 53 | } 54 | } 55 | 56 | render() { 57 | const { label, icon } = this.state; 58 | const { listStyle } = this.props; 59 | return ( 60 | 71 | ); 72 | } 73 | 74 | private onChange(icon: string) { 75 | this.setState({ icon }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /client/src/components/Navbar/style.scss: -------------------------------------------------------------------------------- 1 | 2 | .md-toolbar .md-select-field--toolbar.md-select-field--toolbar { 3 | 4 | margin-bottom: 6px; 5 | margin-top: 6px; 6 | 7 | .md-floating-label { 8 | padding-left: 6px; 9 | } 10 | 11 | .md-text-field { 12 | padding-bottom: 0; 13 | padding-left: 6px; 14 | padding-right: 16px; 15 | padding-top: 0; 16 | margin: 20px 0 0 0; 17 | } 18 | } -------------------------------------------------------------------------------- /client/src/components/ResponsiveContainer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Media } from 'react-md/lib/Media'; 3 | import { ResponsiveContainer as RechartResponsiveContainer } from 'recharts'; 4 | 5 | interface IeContainerProps { 6 | layout: { 7 | x: number; 8 | y: number; 9 | w: number; 10 | h: number; 11 | }; 12 | } 13 | 14 | interface IContainerState { 15 | 16 | } 17 | 18 | /** 19 | * This class is used to remove warning from test cases 20 | */ 21 | export default class ResponsiveContainer extends React.PureComponent { 22 | 23 | render() { 24 | 25 | const { children, layout } = this.props; 26 | 27 | let containerProps = {}; 28 | if (!layout || !layout.w) { 29 | containerProps['width'] = 100; 30 | containerProps['aspect'] = 1; 31 | } 32 | 33 | return ( 34 | 35 | {children} 36 | 37 | ); 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /client/src/components/Settings/index.tsx: -------------------------------------------------------------------------------- 1 | import SettingsButton from './SettingsButton'; 2 | import SetupDashboard from './SetupDashboard'; 3 | 4 | export { SettingsButton, SetupDashboard }; -------------------------------------------------------------------------------- /client/src/components/Spinner/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import * as request from 'xhr-request'; 2 | import * as React from 'react'; 3 | import * as _ from 'lodash'; 4 | 5 | import CSSTransitionGroup from 'react-addons-css-transition-group'; 6 | import CircularProgress from 'react-md/lib/Progress/CircularProgress'; 7 | import Snackbar from 'react-md/lib/Snackbars'; 8 | 9 | import SpinnerStore, { ISpinnerStoreState } from './SpinnerStore'; 10 | import SpinnerActions from './SpinnerActions'; 11 | 12 | interface ISpinnerState extends ISpinnerStoreState { 13 | } 14 | 15 | export default class Spinner extends React.Component { 16 | 17 | constructor(props: any) { 18 | super(props); 19 | 20 | this.state = SpinnerStore.getState(); 21 | 22 | this.onChange = this.onChange.bind(this); 23 | } 24 | 25 | componentDidMount() { 26 | this.onChange(SpinnerStore.getState()); 27 | SpinnerStore.listen(this.onChange); 28 | } 29 | 30 | componentWillUnmount() { 31 | SpinnerStore.unlisten(this.onChange); 32 | } 33 | 34 | onChange(state: ISpinnerState) { 35 | this.setState(state); 36 | } 37 | 38 | render () { 39 | 40 | let refreshing = this.state.pageLoading || this.state.requestLoading || false; 41 | 42 | return ( 43 |
44 | {refreshing && } 45 |
46 | ); 47 | } 48 | } -------------------------------------------------------------------------------- /client/src/components/Spinner/SpinnerActions.ts: -------------------------------------------------------------------------------- 1 | import alt, { AbstractActions } from '../../alt'; 2 | 3 | interface ISpinnerActions { 4 | startPageLoading: AltJS.Action; 5 | endPageLoading: AltJS.Action; 6 | startRequestLoading: AltJS.Action; 7 | endRequestLoading: AltJS.Action; 8 | } 9 | 10 | class SpinnerActions extends AbstractActions /*implements ISpinnerActions*/ { 11 | constructor(altobj: AltJS.Alt) { 12 | super(altobj); 13 | 14 | this.generateActions( 15 | 'startPageLoading', 16 | 'endPageLoading', 17 | 'startRequestLoading', 18 | 'endRequestLoading' 19 | ); 20 | } 21 | } 22 | 23 | const spinnerActions = alt.createActions(SpinnerActions); 24 | 25 | export default spinnerActions; 26 | -------------------------------------------------------------------------------- /client/src/components/Spinner/SpinnerStore.ts: -------------------------------------------------------------------------------- 1 | import alt, { AbstractStoreModel } from '../../alt'; 2 | 3 | import { Toast, ToastActions, IToast } from '../Toast'; 4 | import spinnerActions from './SpinnerActions'; 5 | 6 | export interface ISpinnerStoreState { 7 | pageLoading?: number; 8 | requestLoading?: number; 9 | } 10 | 11 | const openOriginal = XMLHttpRequest.prototype.open; 12 | const sendOriginal = XMLHttpRequest.prototype.send; 13 | 14 | XMLHttpRequest.prototype.open = function(method: string, url: string, async?: boolean, _?: string, __?: string) { 15 | spinnerActions.startRequestLoading.defer(null); 16 | openOriginal.apply(this, arguments); 17 | }; 18 | 19 | XMLHttpRequest.prototype.send = function(data: any) { 20 | let _xhr: XMLHttpRequest = this; 21 | _xhr.onreadystatechange = (response) => { 22 | 23 | // readyState === 4: means the response is complete 24 | if (_xhr.readyState === 4) { 25 | spinnerActions.endRequestLoading.defer(null); 26 | 27 | if (_xhr.status === 429) { 28 | _429ApplicationInsights(); 29 | } 30 | } 31 | }; 32 | sendOriginal.apply(_xhr, arguments); 33 | }; 34 | 35 | function _429ApplicationInsights() { 36 | let toast: IToast = { text: 'You have reached the maximum number of Application Insights requests.' }; 37 | ToastActions.addToast(toast); 38 | } 39 | 40 | class SpinnerStore extends AbstractStoreModel implements ISpinnerStoreState { 41 | 42 | pageLoading: number; 43 | requestLoading: number; 44 | 45 | constructor() { 46 | super(); 47 | 48 | this.pageLoading = 0; 49 | this.requestLoading = 0; 50 | 51 | this.bindListeners({ 52 | startPageLoading: spinnerActions.startPageLoading, 53 | endPageLoading: spinnerActions.endPageLoading, 54 | startRequestLoading: spinnerActions.startRequestLoading, 55 | endRequestLoading: spinnerActions.endRequestLoading, 56 | }); 57 | } 58 | 59 | startPageLoading(): void { 60 | this.pageLoading++; 61 | } 62 | 63 | endPageLoading(): void { 64 | this.pageLoading--; 65 | } 66 | 67 | startRequestLoading(): void { 68 | this.requestLoading++; 69 | } 70 | 71 | endRequestLoading(): void { 72 | this.requestLoading--; 73 | } 74 | } 75 | 76 | const spinnerStore = alt.createStore(SpinnerStore as any, 'SpinnerStore'); 77 | 78 | export default spinnerStore; 79 | -------------------------------------------------------------------------------- /client/src/components/Spinner/index.tsx: -------------------------------------------------------------------------------- 1 | import Spinner from './Spinner'; 2 | import SpinnerActions from './SpinnerActions'; 3 | import SpinnerStore from './SpinnerStore'; 4 | 5 | export { 6 | Spinner, 7 | SpinnerActions, 8 | SpinnerStore 9 | }; -------------------------------------------------------------------------------- /client/src/components/Toast/Toast.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import ToastStore, { IToastStoreState, IToast } from './ToastStore'; 4 | import ToastActions from './ToastActions'; 5 | 6 | import Snackbar from 'react-md/lib/Snackbars'; 7 | 8 | export default class Toast extends React.Component { 9 | 10 | constructor(props: any) { 11 | super(props); 12 | 13 | this.state = ToastStore.getState(); 14 | 15 | this.onChange = this.onChange.bind(this); 16 | this.removeToast = this.removeToast.bind(this); 17 | } 18 | 19 | onChange(state: IToastStoreState) { 20 | let { toasts, autohide, autohideTimeout } = state; 21 | this.setState({ 22 | toasts: toasts.map(o => ({ text: o.text })), 23 | autohide, 24 | autohideTimeout 25 | }); 26 | } 27 | 28 | componentDidMount() { 29 | ToastStore.listen(this.onChange); 30 | } 31 | 32 | render() { 33 | const {toasts, autohide, autohideTimeout} = this.state; 34 | return ( 35 | 42 | ); 43 | } 44 | 45 | private removeToast() { 46 | ToastActions.removeToast(); 47 | } 48 | } -------------------------------------------------------------------------------- /client/src/components/Toast/ToastActions.ts: -------------------------------------------------------------------------------- 1 | import alt, { AbstractActions } from '../../alt'; 2 | import { IToast } from './ToastStore'; 3 | 4 | interface IToastActions { 5 | showText(test: string): IToast; 6 | addToast(toast: IToast): IToast; 7 | removeToast(): void; 8 | } 9 | 10 | class ToastActions extends AbstractActions { 11 | constructor(altobj: AltJS.Alt) { 12 | super(altobj); 13 | 14 | this.generateActions( 15 | 'addToast', 16 | 'removeToast' 17 | ); 18 | } 19 | 20 | addToast(toast: IToast): IToast { 21 | return toast; 22 | } 23 | 24 | showText(text: string): IToast { 25 | return this.addToast({ text }); 26 | } 27 | } 28 | 29 | const toastActions = alt.createActions(ToastActions); 30 | 31 | export default toastActions; 32 | -------------------------------------------------------------------------------- /client/src/components/Toast/ToastStore.ts: -------------------------------------------------------------------------------- 1 | import alt, { AbstractStoreModel } from '../../alt'; 2 | 3 | import toastActions from './ToastActions'; 4 | 5 | export interface IToast { 6 | text: string; 7 | action?: any; 8 | } 9 | 10 | export interface IToastStoreState { 11 | toasts: IToast[]; 12 | queued: Array; 13 | autohideTimeout: number; 14 | autohide: boolean; 15 | } 16 | 17 | const MIN_TIMEOUT_MS = 3000; 18 | const AVG_WORDS_PER_SEC: number = 2; 19 | 20 | class ToastStore extends AbstractStoreModel implements IToastStoreState { 21 | toasts: IToast[]; 22 | queued: Array; 23 | autohideTimeout: number; 24 | autohide: boolean; 25 | 26 | constructor() { 27 | super(); 28 | 29 | this.toasts = []; 30 | this.queued = Array(); 31 | this.autohideTimeout = MIN_TIMEOUT_MS; 32 | this.autohide = true; 33 | 34 | this.bindListeners({ 35 | addToast: toastActions.addToast, 36 | removeToast: toastActions.removeToast, 37 | }); 38 | } 39 | 40 | addToast(toast: IToast): void { 41 | if (this.toastExists(toast)) { 42 | return; // ignore dups 43 | } 44 | if (this.toasts.length === 0) { 45 | this.toasts.push(toast); 46 | } else { 47 | this.queued.push(toast); 48 | } 49 | this.updateSnackbarAttributes(toast); 50 | } 51 | 52 | removeToast(): void { 53 | if (this.queued.length > 0) { 54 | this.toasts = this.queued.splice(0, 1); 55 | } else if (this.toasts.length > 0) { 56 | const [, ...toasts] = this.toasts; 57 | this.toasts = toasts; 58 | } 59 | } 60 | 61 | private toastExists(toast: IToast): boolean { 62 | return this.toasts.findIndex(x => x.text === toast.text) > -1 63 | || this.queued.findIndex(x => x.text === toast.text) > -1; 64 | } 65 | 66 | private updateSnackbarAttributes(toast: IToast): void { 67 | const words = toast.text.split(' ').length; 68 | this.autohideTimeout = Math.max(MIN_TIMEOUT_MS, (words / AVG_WORDS_PER_SEC) * 1000); 69 | this.autohide = !toast.action; 70 | } 71 | } 72 | 73 | const toastStore = alt.createStore(ToastStore as AltJS.StoreModel, 'ToastStore'); 74 | 75 | export default toastStore; 76 | -------------------------------------------------------------------------------- /client/src/components/Toast/index.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import Toast from './Toast'; 4 | import ToastActions from './ToastActions'; 5 | import ToastStore, { IToast, IToastStoreState } from './ToastStore'; 6 | 7 | export { 8 | Toast, 9 | ToastActions, 10 | ToastStore, 11 | IToast, 12 | IToastStoreState 13 | }; -------------------------------------------------------------------------------- /client/src/components/Tooltip/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import injectTooltip from 'react-md/lib/Tooltips'; 3 | 4 | const Tooltip = injectTooltip( 5 | ({children, className, tooltip, ...props }) => ( 6 |
7 | {tooltip} 8 | {children} 9 |
10 | )); 11 | 12 | Tooltip.propTypes = { 13 | children: React.PropTypes.node, 14 | className: React.PropTypes.string, 15 | tooltip: React.PropTypes.node, 16 | }; 17 | 18 | export default Tooltip; -------------------------------------------------------------------------------- /client/src/components/Tooltip/TooltipFontIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import FontIcon from 'react-md/lib/FontIcons'; 3 | import injectTooltip from 'react-md/lib/Tooltips'; 4 | 5 | // Material icons shouldn't have any other children other than the child string and 6 | // it gets converted into a span if the tooltip is added, so we add a container 7 | // around the two. 8 | const TooltipFontIcon = injectTooltip(({ 9 | children, iconClassName, className, tooltip, forceIconFontSize, forceIconSize, style, iconStyle, ...props }) => ( 10 | 11 |
12 | {tooltip} 13 | 19 | {children} 20 | 21 |
22 | )); 23 | 24 | TooltipFontIcon.propTypes = { 25 | children: React.PropTypes.string.isRequired, 26 | className: React.PropTypes.string, 27 | iconClassName: React.PropTypes.string, 28 | tooltip: React.PropTypes.node, 29 | forceIconFontSize: React.PropTypes.bool, 30 | forceIconSize: React.PropTypes.number, 31 | style: React.PropTypes.object, 32 | iconStyle: React.PropTypes.object 33 | }; 34 | 35 | export default TooltipFontIcon; -------------------------------------------------------------------------------- /client/src/components/Tooltip/index.tsx: -------------------------------------------------------------------------------- 1 | import Tooltip from './Tooltip'; 2 | 3 | export default Tooltip; -------------------------------------------------------------------------------- /client/src/components/colors/index.ts: -------------------------------------------------------------------------------- 1 | import * as colors from 'material-colors'; 2 | 3 | var ThemeColors = [ 4 | colors.pink[800], 5 | colors.purple[800], 6 | colors.cyan[800], 7 | colors.red[800], 8 | colors.blue[800], 9 | colors.lightBlue[800], 10 | colors.deepPurple[800], 11 | colors.lime[800], 12 | colors.teal[800] 13 | ]; 14 | 15 | var ThemeColors2 = ThemeColors.slice().reverse(); 16 | 17 | const DangerColor = colors.red[500]; 18 | const PersonColor = colors.teal[700]; 19 | const IntentsColor = colors.teal.a700; 20 | const GoodColor = colors.lightBlue[700]; 21 | const BadColor = colors.red[700]; 22 | const PositiveColor = colors.lightBlue[700]; 23 | const NeutralColor = colors.grey[500]; 24 | 25 | export default { 26 | ThemeColors, 27 | ThemeColors2, 28 | 29 | DangerColor, 30 | PersonColor, 31 | IntentsColor, 32 | 33 | GoodColor, 34 | BadColor, 35 | 36 | PositiveColor, 37 | NeutralColor, 38 | 39 | getColor: (idx) => { 40 | return ThemeColors[idx]; 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /client/src/components/generic/CheckboxFilter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { GenericComponent } from './GenericComponent'; 3 | import Checkbox from 'react-md/lib/SelectionControls/Checkbox'; 4 | 5 | const style = { 6 | checkbox: { 7 | float: 'left', 8 | paddingTop: '24px' 9 | } 10 | }; 11 | 12 | export default class CheckboxFilter extends GenericComponent { 13 | 14 | state = { 15 | values: [], 16 | selectedValues: [] 17 | }; 18 | 19 | constructor(props: any) { 20 | super(props); 21 | 22 | this.onChange = this.onChange.bind(this); 23 | } 24 | 25 | onChange(newValue: string, checked: boolean, event: React.ChangeEvent) { 26 | var { selectedValues } = this.state; 27 | let newSelectedValues = selectedValues.slice(0); 28 | 29 | const idx = selectedValues.findIndex((x) => x === newValue); 30 | if (idx === -1 && checked) { 31 | newSelectedValues.push(newValue); 32 | } else if (idx > -1 && !checked) { 33 | newSelectedValues.splice(idx, 1); 34 | } else { 35 | console.warn('Unexpected checked filter state:', newValue, checked); 36 | } 37 | 38 | this.trigger('onChange', newSelectedValues); 39 | } 40 | 41 | render() { 42 | var { title } = this.props; 43 | var { selectedValues, values } = this.state; 44 | values = values || []; 45 | 46 | let checkboxes = values.map((value, idx) => { 47 | return ( 48 | x === value) !== undefined} 56 | /> 57 | ); 58 | }); 59 | 60 | return ( 61 |
62 |
63 | {checkboxes} 64 |
65 | ); 66 | } 67 | } -------------------------------------------------------------------------------- /client/src/components/generic/Detail/index.ts: -------------------------------------------------------------------------------- 1 | import Detail from './Detail'; 2 | 3 | export default Detail; -------------------------------------------------------------------------------- /client/src/components/generic/Dialogs/DialogsActions.ts: -------------------------------------------------------------------------------- 1 | import alt, { AbstractActions } from '../../../alt'; 2 | 3 | interface IDialogsActions { 4 | openDialog(dialogName: string, args: { [id: string]: Object }): any; 5 | closeDialog(): any; 6 | } 7 | 8 | class DialogsActions extends AbstractActions implements IDialogsActions { 9 | 10 | openDialog(dialogName: string, args: { [id: string]: Object }) { 11 | return { dialogName, args }; 12 | } 13 | 14 | closeDialog() { 15 | return {}; 16 | } 17 | } 18 | 19 | const dialogsActions = alt.createActions(DialogsActions); 20 | 21 | export default dialogsActions; 22 | -------------------------------------------------------------------------------- /client/src/components/generic/Dialogs/DialogsStore.ts: -------------------------------------------------------------------------------- 1 | import alt, { AbstractStoreModel } from '../../../alt'; 2 | 3 | import dialogsActions from './DialogsActions'; 4 | 5 | interface IDialogsStoreState { 6 | dialogsStack: { dialogName: string, args: any }[]; 7 | dialogId: string; 8 | dialogArgs: any; 9 | } 10 | 11 | class DialogsStore extends AbstractStoreModel implements IDialogsStoreState { 12 | 13 | dialogsStack: { dialogName: string, args: any }[]; 14 | dialogId: string; 15 | dialogArgs: any; 16 | 17 | constructor() { 18 | super(); 19 | 20 | this.dialogsStack = []; 21 | this.dialogId = null; 22 | this.dialogArgs = null; 23 | 24 | this.bindListeners({ 25 | openDialog: dialogsActions.openDialog, 26 | closeDialog: dialogsActions.closeDialog 27 | }); 28 | } 29 | 30 | openDialog(params: { dialogName: string, args: any }) { 31 | this.dialogsStack.push(params); 32 | this.dialogId = params.dialogName; 33 | this.dialogArgs = params.args; 34 | } 35 | 36 | closeDialog() { 37 | this.dialogsStack.pop(); 38 | var dialog = this.dialogsStack.length > 0 ? 39 | this.dialogsStack[this.dialogsStack.length - 1] : 40 | { dialogName: null, args: null }; 41 | this.dialogId = dialog.dialogName; 42 | this.dialogArgs = dialog.args; 43 | } 44 | } 45 | 46 | const dialogsStore = alt.createStore(DialogsStore as AltJS.StoreModel, 'DialogsStore'); 47 | 48 | export default dialogsStore; 49 | -------------------------------------------------------------------------------- /client/src/components/generic/Dialogs/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import Dialog from './Dialog'; 4 | import DialogsActions from './DialogsActions'; 5 | import DialogsStore from './DialogsStore'; 6 | 7 | function loadDialogsFromDashboard(dashboard: IDashboardConfig): JSX.Element[] { 8 | 9 | if (!dashboard.dialogs) { 10 | return null; 11 | } 12 | 13 | var dialogs = dashboard.dialogs.map((dialog, idx) => 14 | 15 | ); 16 | 17 | return dialogs; 18 | } 19 | 20 | export { 21 | loadDialogsFromDashboard, 22 | Dialog, 23 | DialogsActions, 24 | DialogsStore 25 | }; -------------------------------------------------------------------------------- /client/src/components/generic/RadialBarChartCard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { GenericComponent, IGenericProps, IGenericState } from './GenericComponent'; 3 | import Card from '../Card'; 4 | import ResponsiveContainer from '../ResponsiveContainer'; 5 | import { RadialBarChart, RadialBar, PolarGrid, Tooltip, Legend } from 'recharts'; 6 | 7 | import * as _ from 'lodash'; 8 | import * as moment from 'moment'; 9 | 10 | import colors from '../colors'; 11 | var { ThemeColors } = colors; 12 | 13 | interface IRadarProps extends IGenericProps { 14 | props: { 15 | 16 | nameKey: string; 17 | }; 18 | } 19 | 20 | interface IRadarState extends IGenericState { 21 | values: Object[]; 22 | } 23 | 24 | export default class RadialBarChartCard extends GenericComponent { 25 | 26 | state = { 27 | values: [], 28 | bars: [] 29 | }; 30 | 31 | constructor(props: any) { 32 | super(props); 33 | 34 | this.handleClick = this.handleClick.bind(this); 35 | } 36 | 37 | handleClick(data: any, index: number) { 38 | this.trigger('onBarClick', data.payload); 39 | } 40 | 41 | render() { 42 | 43 | var { values } = this.state; 44 | var { title, subtitle, props, layout } = this.props; 45 | 46 | if (!values) { 47 | return null; 48 | } 49 | 50 | const domain = 100; 51 | 52 | const data = [ 53 | { name: 'alarm.set', uv: 31.47, pv: 2400, fill: '#8884d8' }, 54 | { name: '*:/', uv: 26.69, pv: 4567, fill: '#83a6ed' }, 55 | { name: 'none', uv: 15.69, pv: 1398, fill: '#8dd1e1' }, 56 | { name: 'invalid property type object', uv: 8.22, pv: 9800, fill: '#82ca9d' } 57 | ]; 58 | 59 | return ( 60 | 61 | 62 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /client/src/components/generic/SplitPanel/index.ts: -------------------------------------------------------------------------------- 1 | import SplitPanel from './SplitPanel'; 2 | 3 | export default SplitPanel; -------------------------------------------------------------------------------- /client/src/components/generic/Table/index.ts: -------------------------------------------------------------------------------- 1 | import Table from './Table'; 2 | 3 | export default Table; -------------------------------------------------------------------------------- /client/src/components/generic/TextFilter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { GenericComponent } from './GenericComponent'; 3 | import SelectField from 'react-md/lib/SelectFields'; 4 | 5 | export default class TextFilter extends GenericComponent { 6 | 7 | static defaultProps = { 8 | title: 'Select' 9 | }; 10 | 11 | static fromSource(source: string) { 12 | return { 13 | selectedValue: GenericComponent.sourceFormat(source, 'values-selected'), 14 | values: GenericComponent.sourceFormat(source, 'values-all') 15 | }; 16 | } 17 | 18 | constructor(props: any) { 19 | super(props); 20 | 21 | this.onChange = this.onChange.bind(this); 22 | } 23 | 24 | onChange(newValue: any) { 25 | this.trigger('onChange', newValue); 26 | } 27 | 28 | render() { 29 | var { selectedValue, values } = this.state; 30 | var { title } = this.props; 31 | values = values || []; 32 | 33 | return ( 34 | 44 | ); 45 | } 46 | } -------------------------------------------------------------------------------- /client/src/components/generic/plugins.tsx: -------------------------------------------------------------------------------- 1 | import PieData from './PieData'; 2 | import Timeline from './Timeline'; 3 | import Scatter from './Scatter'; 4 | import BarData from './BarData'; 5 | import Area from './Area'; 6 | import Scorecard from './Scorecard'; 7 | import RadarChartCard from './RadarChartCard'; 8 | import RadialBarChartCard from './RadialBarChartCard'; 9 | import SimpleRadialBarChartCard from './SimpleRadialBarChartCard'; 10 | import MapData from './MapData'; 11 | // filters 12 | import TextFilter from './TextFilter'; 13 | import CheckboxFilter from './CheckboxFilter'; 14 | import MenuFilter from './MenuFilter'; 15 | import DatePickerFilter from './DatePickerFilter'; 16 | // dialog views 17 | import Table from './Table'; 18 | import Detail from './Detail'; 19 | import SplitPanel from './SplitPanel'; 20 | import RequestButton from './RequestButton'; 21 | 22 | export default { 23 | PieData, 24 | Timeline, 25 | Scatter, 26 | BarData, 27 | Area, 28 | Scorecard, 29 | TextFilter, 30 | CheckboxFilter, 31 | MenuFilter, 32 | DatePickerFilter, 33 | Table, 34 | Detail, 35 | SplitPanel, 36 | MapData, 37 | RadarChartCard, 38 | RadialBarChartCard, 39 | SimpleRadialBarChartCard, 40 | RequestButton, 41 | }; -------------------------------------------------------------------------------- /client/src/data-sources/connections/Connection.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface IConnection { 4 | type: string; 5 | params: string[]; 6 | editor?: new (props: any) => ConnectionEditor; 7 | } 8 | 9 | interface IConnectionProps { 10 | connection: any; 11 | onParamChange: (paramId: string, paramValue: string) => void; 12 | } 13 | 14 | abstract class ConnectionEditor extends React.Component { 15 | 16 | constructor(props: T1) { 17 | super(props); 18 | 19 | this.onParamChange = this.onParamChange.bind(this); 20 | } 21 | 22 | onParamChange(value: string, event: any) { 23 | if (typeof this.props.onParamChange === 'function') { 24 | const trimmedValue = ('' + value).trim(); 25 | this.props.onParamChange(event.target.id, trimmedValue); 26 | } 27 | } 28 | } 29 | 30 | export { 31 | IConnection, 32 | IConnectionProps, 33 | ConnectionEditor 34 | }; -------------------------------------------------------------------------------- /client/src/data-sources/connections/application-insights/index.tsx: -------------------------------------------------------------------------------- 1 | import ApplicationInsightsConnection from './application-insights'; 2 | 3 | export default ApplicationInsightsConnection; -------------------------------------------------------------------------------- /client/src/data-sources/connections/cosmos-db.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import TextField from 'react-md/lib/TextFields'; 3 | import Checkbox from 'react-md/lib/SelectionControls/Checkbox'; 4 | import Card from 'react-md/lib/Cards/Card'; 5 | import CardTitle from 'react-md/lib/Cards/CardTitle'; 6 | import Avatar from 'react-md/lib/Avatars'; 7 | import FontIcon from 'react-md/lib/FontIcons'; 8 | import InfoDrawer from '../../components/common/InfoDrawer'; 9 | import { IConnection, ConnectionEditor, IConnectionProps } from './Connection'; 10 | 11 | export default class CosmosDBConnection implements IConnection { 12 | type = 'cosmos-db'; 13 | params = ['host', 'key']; 14 | editor = CosmosDBConnectionEditor; 15 | } 16 | 17 | class CosmosDBConnectionEditor extends ConnectionEditor { 18 | 19 | render() { 20 | let { connection } = this.props; 21 | // connection = connection || {'ssl':true }; 22 | return ( 23 | 24 | receipt} />} 27 | style={{ float: 'left'}} 28 | /> 29 | 35 | 40 | 41 | 50 | 59 | 60 | ); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /client/src/data-sources/connections/graphql.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Card from 'react-md/lib/Cards/Card'; 3 | import CardTitle from 'react-md/lib/Cards/CardTitle'; 4 | import Avatar from 'react-md/lib/Avatars'; 5 | import FontIcon from 'react-md/lib/FontIcons'; 6 | import TextField from 'react-md/lib/TextFields'; 7 | import InfoDrawer from '../../components/common/InfoDrawer'; 8 | import { IConnection, ConnectionEditor, IConnectionProps } from './Connection'; 9 | 10 | export default class GraphQLConnection implements IConnection { 11 | type = 'graphql'; 12 | params = [ 'serviceUrl' ]; 13 | editor = GraphQLConnectionEditor; 14 | } 15 | 16 | class GraphQLConnectionEditor extends ConnectionEditor { 17 | 18 | constructor(props: IConnectionProps) { 19 | super(props); 20 | 21 | this.onParamChange = this.onParamChange.bind(this); 22 | } 23 | 24 | onParamChange(value: string, event: any) { 25 | if (typeof this.props.onParamChange === 'function') { 26 | this.props.onParamChange(event.target.id, value); 27 | } 28 | } 29 | 30 | render() { 31 | let { connection } = this.props; 32 | connection = connection || {}; 33 | 34 | return ( 35 | 36 | receipt} />} 39 | style={{ float: 'left'}} 40 | /> 41 | 47 |
48 | Just enter the URL of the GraphQL service you wish to query below. 49 | Currently only publicly accessible GraphQL endpoints are supported. 50 |
51 |
52 | 61 |
62 | ); 63 | } 64 | } -------------------------------------------------------------------------------- /client/src/data-sources/connections/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import ApplicationInsightsConnection from './application-insights'; 4 | import GraphQLConnection from './graphql'; 5 | import BotFrameworkConnection from './bot-framework'; 6 | import CosmosDBConnection from './cosmos-db'; 7 | import AzureConnection from './azure'; 8 | import { IConnection } from './Connection'; 9 | 10 | var connectionTypes = [ 11 | ApplicationInsightsConnection, 12 | GraphQLConnection, 13 | AzureConnection, 14 | CosmosDBConnection, 15 | BotFrameworkConnection ]; 16 | 17 | var connections: IDict = {}; 18 | connectionTypes.forEach(connectionType => { 19 | var newConnection: IConnection = new connectionType(); 20 | connections[newConnection.type] = newConnection; 21 | }); 22 | 23 | export default connections; -------------------------------------------------------------------------------- /client/src/data-sources/index.ts: -------------------------------------------------------------------------------- 1 | import { DataSourceConnector, IDataSource, IDataSourceDictionary, IExtrapolationResult } from './DataSourceConnector'; 2 | 3 | export { 4 | DataSourceConnector, 5 | IDataSource, 6 | IDataSourceDictionary, 7 | IExtrapolationResult 8 | }; -------------------------------------------------------------------------------- /client/src/data-sources/plugins/ApplicationInsights/ApplicationInsightsApi.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'xhr-request'; 2 | 3 | import { appInsightsUri } from './common'; 4 | 5 | export interface IApplicationInsightsApi { 6 | callQuery (query: string, callback: () => void): void; 7 | } 8 | 9 | export default class ApplicationInsightsApi implements IApplicationInsightsApi { 10 | 11 | constructor(private appId: string, private apiKey: string) { } 12 | 13 | callQuery(query: string, callback: (error?: Error, json?: any) => void) { 14 | var url = `${appInsightsUri}/${this.appId}/query`; 15 | try { 16 | request( 17 | url, 18 | { 19 | method: 'POST', 20 | json: true, 21 | headers: { 22 | 'x-api-key': this.apiKey 23 | }, 24 | body: { query } 25 | }, 26 | (error: Error, json: any) => { 27 | if (error) { 28 | callback(error); 29 | } 30 | 31 | callback(null, json); 32 | } 33 | ); 34 | } catch (ex) { 35 | callback(ex); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /client/src/data-sources/plugins/ApplicationInsights/common.ts: -------------------------------------------------------------------------------- 1 | 2 | var appInsightsUri = 'https://api.applicationinsights.io/beta/apps'; 3 | var appId = process.env.REACT_APP_APP_INSIGHTS_APPID; 4 | var apiKey = process.env.REACT_APP_APP_INSIGHTS_APIKEY; 5 | 6 | export { 7 | appInsightsUri, 8 | appId, 9 | apiKey 10 | }; -------------------------------------------------------------------------------- /client/src/data-sources/plugins/Constant/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | import { DataSourcePlugin, IOptions } from '../DataSourcePlugin'; 4 | import ConstantDatasourceSettings from './Settings'; 5 | 6 | interface IConstantParams { 7 | values: Array; 8 | selectedValue: string; 9 | } 10 | 11 | export default class Constant extends DataSourcePlugin { 12 | 13 | static editor = ConstantDatasourceSettings; 14 | type = 'Constant'; 15 | defaultProperty = 'selectedValue'; 16 | 17 | constructor(options: IOptions, connections: IDict) { 18 | super(options, connections); 19 | 20 | var props = this._props; 21 | var params = options.params; 22 | 23 | props.actions.push.apply(props.actions, [ 'initialize', 'updateSelectedValue', 'updateSelectedValues' ]); 24 | } 25 | 26 | initialize() { 27 | var { selectedValue, values } = this._props.params; 28 | return { selectedValue, values }; 29 | } 30 | 31 | /** 32 | * updateDependencies - called when dependencies are created 33 | */ 34 | dependenciesUpdated(dependencies: IDictionary, args: IDictionary, callback: (result: any) => void) { 35 | var result = _.extend(dependencies, args); 36 | 37 | if (typeof callback === 'function') { 38 | return callback(result); 39 | } 40 | 41 | return result; 42 | } 43 | 44 | updateSelectedValue(dependencies: IDictionary, selectedValue: any) { 45 | return { selectedValue }; 46 | } 47 | 48 | updateSelectedValues(dependencies: IDictionary, selectedValues: any) { 49 | return { selectedValues }; 50 | } 51 | } -------------------------------------------------------------------------------- /client/src/data-sources/plugins/GraphQL.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'xhr-request'; 2 | 3 | import { DataSourcePlugin, IOptions } from './DataSourcePlugin'; 4 | import GraphQLConnection from '../connections/graphql'; 5 | import { DataSourceConnector } from '../DataSourceConnector'; 6 | 7 | let connectionType = new GraphQLConnection(); 8 | 9 | interface IGraphQLParams { 10 | query: string; 11 | variables: Object; 12 | } 13 | 14 | export default class GraphQL extends DataSourcePlugin { 15 | type = 'GraphQL'; 16 | defaultProperty = 'data'; 17 | connectionType = connectionType.type; 18 | 19 | constructor(options: IOptions, connections: IDict) { 20 | super(options, connections); 21 | this.validateParams(this._props.params); 22 | } 23 | 24 | dependenciesUpdated(dependencies: any) { 25 | // Ensure dependencies exist 26 | const isAnyDependencyMissing = Object.keys(this.getDependencies()).some(key => dependencies[key] == null); 27 | if (isAnyDependencyMissing) { 28 | return dispatch => dispatch(); 29 | } 30 | 31 | // Validate connection 32 | const connection = this.getConnection(); 33 | const { serviceUrl } = connection; 34 | if (!connection || !serviceUrl) { 35 | return dispatch => dispatch(); 36 | } 37 | 38 | const params = this.getParams() || ({} as IGraphQLParams); 39 | const query = params.query || ''; 40 | const variables = dependencies['variables'] || params.variables; 41 | 42 | return dispatch => { 43 | request('/graphql/query', { 44 | method: 'POST', 45 | json: true, 46 | body: { 47 | serviceUrl: serviceUrl, 48 | query: query, 49 | variables: variables 50 | } 51 | }, (err, json) => { 52 | const error = err || (json['errors'] && json['errors'][0]); 53 | if (error || json['errors']) { 54 | return this.failure(error); 55 | } 56 | 57 | return dispatch(json); 58 | }); 59 | }; 60 | } 61 | 62 | updateSelectedValues(dependencies: IDictionary, selectedValues: any) { 63 | if (Array.isArray(selectedValues)) { 64 | return Object.assign(dependencies, { 'selectedValues': selectedValues }); 65 | } else { 66 | return Object.assign(dependencies, { ... selectedValues }); 67 | } 68 | } 69 | 70 | private validateParams(params: IGraphQLParams): void { 71 | return; 72 | } 73 | } -------------------------------------------------------------------------------- /client/src/data-sources/plugins/PluginsMapping.ts: -------------------------------------------------------------------------------- 1 | import Constant from './Constant/index'; 2 | 3 | export default { 4 | 'ApplicationInsights/Query': 'ApplicationInsights/Query', 5 | 'CosmosDB/Query': 'CosmosDB/Query', 6 | 'Azure': 'Azure', 7 | 'Constant': 'Constant/index', 8 | 'Sample': 'Sample', 9 | 'BotFramework/DirectLine': 'BotFramework/DirectLine', 10 | 'GraphQL': 'GraphQL', 11 | }; -------------------------------------------------------------------------------- /client/src/data-sources/plugins/Sample.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as _ from 'lodash'; 3 | import { DataSourcePlugin, IOptions } from './DataSourcePlugin'; 4 | 5 | interface ISampleParams { 6 | samples: IDictionary; 7 | } 8 | 9 | export default class Sample extends DataSourcePlugin { 10 | 11 | type = 'Sample'; 12 | defaultProperty = 'values'; 13 | 14 | constructor(options: IOptions, connections: IDict) { 15 | super(options, connections); 16 | 17 | var props = this._props; 18 | var params = options.params; 19 | 20 | props.actions.push.apply(props.actions, [ 'initialize' ]); 21 | } 22 | 23 | initialize() { 24 | let { samples } = _.cloneDeep(this._props.params); 25 | return samples || {}; 26 | } 27 | 28 | /** 29 | * updateDependencies - called when dependencies are created 30 | */ 31 | dependenciesUpdated(dependencies: IDictionary, args: IDictionary, callback: (result: any) => void) { 32 | let result = _.extend(dependencies, args); 33 | let { samples } = this.getParams(); 34 | 35 | _.extend(result, samples); 36 | 37 | if (typeof callback === 'function') { 38 | return callback(result); 39 | } 40 | 41 | return result; 42 | } 43 | 44 | updateSelectedValues(dependencies: IDictionary, selectedValues: any) { 45 | if (Array.isArray(selectedValues)) { 46 | return _.extend(dependencies, { 'selectedValues': selectedValues }); 47 | } else { 48 | return _.extend(dependencies, { ... selectedValues }); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /client/src/data-sources/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import ApplicationInsightsQuery from './ApplicationInsights/Query'; 2 | import CosmosDBQuery from './CosmosDB/Query'; 3 | import BotFrameworkDirectLine from './BotFramework/DirectLine'; 4 | import Azure from './Azure'; 5 | import Constant from './Constant'; 6 | import Sample from './Sample'; 7 | import GraphQL from './GraphQL'; 8 | 9 | export default { 10 | 'ApplicationInsights/Query': ApplicationInsightsQuery, 11 | 'CosmosDB/Query': CosmosDBQuery, 12 | 'Azure': Azure, 13 | 'Constant': Constant, 14 | 'BotFramework/DirectLine': BotFrameworkDirectLine, 15 | 'Sample': Sample, 16 | 'GraphQL': GraphQL 17 | }; 18 | -------------------------------------------------------------------------------- /client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { Router, browserHistory } from 'react-router'; 4 | import routes from './routes'; 5 | 6 | import './index.css'; 7 | import 'react-grid-layout/css/styles.css'; 8 | import 'react-resizable/css/styles.css'; 9 | 10 | ReactDOM.render( 11 | 12 | {routes} 13 | , 14 | document.getElementById('root')); -------------------------------------------------------------------------------- /client/src/pages/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import DashboardComponent from '../components/Dashboard'; 4 | import { SetupDashboard } from '../components/Settings'; 5 | 6 | import ConfigurationsActions from '../actions/ConfigurationsActions'; 7 | import ConfigurationsStore from '../stores/ConfigurationsStore'; 8 | 9 | interface IDashboardState { 10 | dashboard?: IDashboardConfig; 11 | connections?: IConnections; 12 | connectionsMissing?: boolean; 13 | } 14 | 15 | export default class Dashboard extends React.Component { 16 | 17 | state: IDashboardState = { 18 | dashboard: null, 19 | connections: {}, 20 | connectionsMissing: false 21 | }; 22 | 23 | constructor(props: any) { 24 | super(props); 25 | 26 | this.updateConfiguration = this.updateConfiguration.bind(this); 27 | } 28 | 29 | updateConfiguration(newState: IDashboardState) { 30 | this.setState(newState); 31 | } 32 | 33 | componentDidMount() { 34 | 35 | this.setState(ConfigurationsStore.getState()); 36 | ConfigurationsStore.listen(this.updateConfiguration); 37 | } 38 | 39 | componentWillUnmount() { 40 | ConfigurationsStore.unlisten(this.updateConfiguration); 41 | } 42 | 43 | render() { 44 | var { dashboard, connections, connectionsMissing } = this.state; 45 | 46 | if (!dashboard) { 47 | return null; 48 | } 49 | 50 | if (connectionsMissing) { 51 | return ( 52 | 53 | ); 54 | } 55 | 56 | return ( 57 | 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /client/src/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import HomeComponent from '../components/Home'; 4 | 5 | export default class Home extends React.Component { 6 | 7 | render() { 8 | 9 | return ( 10 | 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/src/pages/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default () => ( 4 |
5 |

6 | 404 Not Found :( 7 |

8 |
9 | ); -------------------------------------------------------------------------------- /client/src/pages/Setup.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import SetupComponent from '../components/Setup'; 4 | 5 | export default class Setup extends React.Component { 6 | 7 | render() { 8 | 9 | return ( 10 | 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/src/routes.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Route } from 'react-router'; 3 | 4 | import App from './components/App'; 5 | import NotFound from './pages/NotFound'; 6 | import Home from './pages/Home'; 7 | import Dashboard from './pages/Dashboard'; 8 | import Setup from './pages/Setup'; 9 | 10 | export default ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); -------------------------------------------------------------------------------- /client/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | process.env.REACT_APP_APP_INSIGHTS_APPID = "APPID"; 2 | process.env.REACT_APP_APP_INSIGHTS_APIKEY = "APIKEY"; -------------------------------------------------------------------------------- /client/src/stores/AccountStore.ts: -------------------------------------------------------------------------------- 1 | import alt, { AbstractStoreModel } from '../alt'; 2 | 3 | import accountActions from '../actions/AccountActions'; 4 | 5 | interface IAccountStoreState { 6 | account: IDictionary; 7 | } 8 | 9 | class AccountStore extends AbstractStoreModel implements IAccountStoreState { 10 | 11 | account: IDictionary; 12 | 13 | constructor() { 14 | super(); 15 | 16 | this.account = null; 17 | 18 | this.bindListeners({ 19 | updateAccount: accountActions.updateAccount 20 | }); 21 | } 22 | 23 | updateAccount(state: any) { 24 | this.account = state.account; 25 | } 26 | } 27 | 28 | const accountStore = alt.createStore((AccountStore as AltJS.StoreModel), 'AccountStore'); 29 | 30 | export default accountStore; 31 | -------------------------------------------------------------------------------- /client/src/stores/ConnectionsStore.ts: -------------------------------------------------------------------------------- 1 | import alt, { AbstractStoreModel } from '../alt'; 2 | 3 | import connectionsActions from '../actions/ConnectionsActions'; 4 | 5 | interface IConnectionsStoreState { 6 | connections: IDictionary; 7 | } 8 | 9 | class ConnectionsStore extends AbstractStoreModel implements IConnectionsStoreState { 10 | 11 | connections: IDictionary; 12 | 13 | constructor() { 14 | super(); 15 | 16 | this.connections = {}; 17 | 18 | this.bindListeners({ 19 | updateConnection: connectionsActions.updateConnection 20 | }); 21 | } 22 | 23 | updateConnection(connectionName: string, args: IDictionary) { 24 | this.connections[connectionName] = args; 25 | } 26 | } 27 | 28 | const connectionsStore = 29 | alt.createStore((ConnectionsStore as AltJS.StoreModel), 'ConnectionsStore'); 30 | 31 | export default connectionsStore; 32 | -------------------------------------------------------------------------------- /client/src/stores/SettingsStore.ts: -------------------------------------------------------------------------------- 1 | import alt, { AbstractStoreModel } from '../alt'; 2 | 3 | import settingsActions from '../actions/SettingsActions'; 4 | 5 | export interface ISettingsStoreState { 6 | isSavingSettings: boolean; 7 | } 8 | 9 | class SettingsStore extends AbstractStoreModel implements ISettingsStoreState { 10 | 11 | isSavingSettings: boolean; 12 | 13 | constructor() { 14 | super(); 15 | 16 | this.bindListeners({ 17 | saveSettings: settingsActions.saveSettings, 18 | saveSettingsCompleted: settingsActions.saveSettingsCompleted 19 | }); 20 | } 21 | 22 | saveSettings() { 23 | this.isSavingSettings = true; 24 | } 25 | saveSettingsCompleted() { 26 | this.isSavingSettings = false; 27 | } 28 | } 29 | 30 | const settingsStore = alt.createStore((SettingsStore as AltJS.StoreModel), 'SettingsStore'); 31 | 32 | export default settingsStore; 33 | -------------------------------------------------------------------------------- /client/src/stores/SetupStore.ts: -------------------------------------------------------------------------------- 1 | import alt, { AbstractStoreModel } from '../alt'; 2 | import * as _ from 'lodash'; 3 | 4 | import connections from '../data-sources/connections'; 5 | import { DataSourceConnector, IDataSourceDictionary } from '../data-sources'; 6 | import SetupActions from '../actions/SetupActions'; 7 | 8 | export interface ISetupStoreState extends ISetupConfig { 9 | loaded: boolean; 10 | saveSuccess: boolean; 11 | } 12 | 13 | class SetupStore extends AbstractStoreModel implements ISetupStoreState { 14 | 15 | admins: string[]; 16 | stage: string; 17 | enableAuthentication: boolean; 18 | allowHttp: boolean; 19 | redirectUrl: string; 20 | clientID: string; 21 | clientSecret: string; 22 | issuer: string; 23 | loaded: boolean; 24 | saveSuccess: boolean; 25 | 26 | constructor() { 27 | super(); 28 | 29 | this.admins = []; 30 | this.stage = ''; 31 | this.enableAuthentication = false; 32 | this.allowHttp = false; 33 | this.redirectUrl = ''; 34 | this.clientID = ''; 35 | this.clientSecret = ''; 36 | this.loaded = false; 37 | this.saveSuccess = false; 38 | this.issuer = ''; 39 | 40 | this.bindListeners({ 41 | load: SetupActions.load 42 | }); 43 | } 44 | 45 | load(setupConfig: ISetupConfig) { 46 | 47 | if (setupConfig) { 48 | this.admins = setupConfig.admins; 49 | this.stage = setupConfig.stage; 50 | this.enableAuthentication = setupConfig.enableAuthentication; 51 | this.allowHttp = setupConfig.allowHttp; 52 | this.redirectUrl = setupConfig.redirectUrl; 53 | this.clientID = setupConfig.clientID; 54 | this.clientSecret = setupConfig.clientSecret; 55 | this.issuer = setupConfig.issuer; 56 | } 57 | 58 | this.loaded = true; 59 | this.saveSuccess = true; 60 | 61 | setTimeout(() => { this.saveSuccess = false; }, 500); 62 | } 63 | } 64 | 65 | const setupStore = alt.createStore((SetupStore as AltJS.StoreModel), 'SetupStore'); 66 | 67 | export default setupStore; 68 | -------------------------------------------------------------------------------- /client/src/stores/VisibilityStore.ts: -------------------------------------------------------------------------------- 1 | import alt, { AbstractStoreModel } from '../alt'; 2 | 3 | import visibilityActions from '../actions/VisibilityActions'; 4 | 5 | class VisibilityStore extends AbstractStoreModel { 6 | 7 | flags: IDict; 8 | 9 | constructor() { 10 | super(); 11 | 12 | this.flags = {}; 13 | 14 | this.bindListeners({ 15 | updateFlags: [visibilityActions.turnFlagOn, visibilityActions.turnFlagOff, visibilityActions.setFlags] 16 | }); 17 | } 18 | 19 | updateFlags(flags: any) { 20 | if (flags) { 21 | Object.keys(flags).forEach(flag => { 22 | this.flags[flag] = flags[flag]; 23 | }); 24 | } 25 | } 26 | } 27 | 28 | const visibilityStore = alt.createStore((VisibilityStore as AltJS.StoreModel) , 'VisibilityStore'); 29 | 30 | export default visibilityStore; 31 | -------------------------------------------------------------------------------- /client/src/tests/common/TokenInput.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import TokenInput from '../../components/common/TokenInput'; 4 | import * as TestUtils from 'react-dom/test-utils'; 5 | 6 | describe('Info drawer', () => { 7 | let tokenInput; 8 | 9 | it('check info drawer is loading', () => { 10 | Object.defineProperty(window, "matchMedia", { 11 | value: jest.fn(() => { return { matches: true } }) 12 | }); 13 | 14 | let tokensObject = []; 15 | tokenInput = TestUtils.renderIntoDocument(); 17 | 18 | TestUtils.isElementOfType(tokenInput, 'div'); 19 | }); 20 | }); -------------------------------------------------------------------------------- /client/src/tests/common/infoDrawer.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import InfoDrawer from '../../components/common/InfoDrawer'; 4 | import * as TestUtils from 'react-dom/test-utils'; 5 | 6 | describe('Info drawer', () => { 7 | let infoDrawer; 8 | 9 | it('check info drawer is loading', () => { 10 | Object.defineProperty(window, "matchMedia", { 11 | value: jest.fn(() => { return { matches: true } }) 12 | }); 13 | 14 | TestUtils.renderIntoDocument(); 20 | 21 | TestUtils.isElementOfType(infoDrawer, 'div'); 22 | }); 23 | }); -------------------------------------------------------------------------------- /client/src/tests/data-formats/data-formats.test.ts: -------------------------------------------------------------------------------- 1 | import { IDataSourceDictionary } from '../../data-sources'; 2 | import Sample from '../../data-sources/plugins/Sample'; 3 | 4 | import { formatTests } from './formats'; 5 | import * as formats from '../../utils/data-formats'; 6 | 7 | describe('Data Formats', () => { 8 | 9 | let sampleMockPlugin = new Sample( { 10 | params: { 11 | values: [ 'value 1', 'value 2', 'value 3' ], 12 | samples: { 13 | values: [ 'value1', 'value2', 'value3' ] 14 | } 15 | } 16 | }, {}); 17 | 18 | Object.keys(formatTests).forEach(testFormat => { 19 | 20 | it (testFormat, () => { 21 | 22 | let testCase = formatTests[testFormat]; 23 | let tests = []; 24 | if (!Array.isArray(testCase)) { 25 | tests = [ testCase ]; 26 | } else { 27 | tests = testCase; 28 | } 29 | 30 | tests.forEach(test => { 31 | let values = test.state; 32 | 33 | let mockPlugin = sampleMockPlugin; 34 | if (test.params) { 35 | mockPlugin = new Sample({ 36 | params: test.params 37 | }, {}); 38 | } 39 | 40 | let dependencies = test.dependencies || {}; 41 | 42 | let result = formats[testFormat](test.format, test.state, dependencies, mockPlugin, {}); 43 | expect(result).toMatchObject(test.expected); 44 | }); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /client/src/tests/data-formats/formats/bars.ts: -------------------------------------------------------------------------------- 1 | import { IFormatTest } from './formats'; 2 | 3 | export default { 4 | format: { 5 | type: 'bars', 6 | args: { 7 | barsField: 'barField', 8 | seriesField: 'seriesField' 9 | } 10 | }, 11 | state: { 12 | values: [ 13 | { count: 10, barField: 'bar 1', seriesField: 'series1Value' }, 14 | { count: 15, barField: 'bar 2', seriesField: 'series1Value' }, 15 | { count: 20, barField: 'bar 1', seriesField: 'series2Value' }, 16 | { count: 44, barField: 'bar 3', seriesField: 'series2Value' }, 17 | ] 18 | }, 19 | expected: { 20 | 'bars': ['series1Value', 'series2Value'], 21 | 'bar-values': [ 22 | {'series1Value': 10, 'series2Value': 20, 'value': 'bar 1'}, 23 | {'series1Value': 15, 'value': 'bar 2'}, 24 | {'series2Value': 44, 'value': 'bar 3'} 25 | ] 26 | } 27 | }; -------------------------------------------------------------------------------- /client/src/tests/data-formats/formats/filter.ts: -------------------------------------------------------------------------------- 1 | import { IFormatTest } from './formats'; 2 | 3 | export default { 4 | format: 'filter', 5 | state: { 6 | values: [ 7 | { value: 'value 1' }, 8 | { value: 'value 2' }, 9 | { value: 'value 3' } 10 | ] 11 | }, 12 | expected: { 13 | 'values-all': [ 'value 1', 'value 2', 'value 3' ], 14 | 'values-selected': [ ], 15 | } 16 | }; -------------------------------------------------------------------------------- /client/src/tests/data-formats/formats/filtered_samples.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import { IFormatTest } from './formats'; 3 | 4 | const values = [ 5 | { name: 'skype-en', value: 9, locale: 'en', channel: 'skype' }, 6 | { name: 'skype-fr', value: 8, locale: 'fr', channel: 'skype' }, 7 | { name: 'skype-de', value: 7, locale: 'de', channel: 'skype' }, 8 | { name: 'messenger-en', value: 6, locale: 'en', channel: 'messenger' }, 9 | { name: 'messenger-fr', value: 5, locale: 'fr', channel: 'messenger' }, 10 | { name: 'messenger-de', value: 4, locale: 'de', channel: 'messenger' }, 11 | { name: 'slack-en', value: 3, locale: 'en', channel: 'slack' }, 12 | { name: 'slack-fr', value: 2, locale: 'fr', channel: 'slack' }, 13 | { name: 'slack-de', value: 1, locale: 'de', channel: 'slack' } 14 | ]; 15 | 16 | export default [ 17 | { 18 | format: 'filtered_samples', 19 | params: { 20 | samples: { values }, 21 | filters: [ 22 | { dependency: 'selectedFilter1', queryProperty: 'locale' }, 23 | { dependency: 'selectedFilter2', queryProperty: 'channel' } 24 | ] 25 | }, 26 | dependencies: { 27 | values, 28 | selectedFilter1: [], 29 | selectedFilter2: [] 30 | }, 31 | state: { values: [ 'value 1', 'value 2', 'value 3' ] }, 32 | expected: { 33 | 'filtered_values': values 34 | } 35 | }, 36 | { 37 | format: 'filtered_samples', 38 | params: { 39 | samples: { values }, 40 | filters: [ 41 | { dependency: 'selectedFilter1', queryProperty: 'locale' }, 42 | { dependency: 'selectedFilter2', queryProperty: 'channel' } 43 | ] 44 | }, 45 | dependencies: { 46 | values, 47 | selectedFilter1: ['en'], 48 | selectedFilter2: [] 49 | }, 50 | state: { values: [ 'value 1', 'value 2', 'value 3' ] }, 51 | expected: { 52 | 'filtered_values': _.filter(values, { locale: 'en' }) 53 | } 54 | } 55 | ]; -------------------------------------------------------------------------------- /client/src/tests/data-formats/formats/flags.ts: -------------------------------------------------------------------------------- 1 | import { IFormatTest } from './formats'; 2 | 3 | export default { 4 | format: 'flags', 5 | state: { values: [ 'value 1', 'value 2', 'value 3' ] }, 6 | expected: { 7 | 'value 1': false, 8 | 'value 2': false, 9 | 'value 3': false, 10 | 'values-all': [ 'value 1', 'value 2', 'value 3' ], 11 | 'values-selected': [], 12 | } 13 | }; -------------------------------------------------------------------------------- /client/src/tests/data-formats/formats/formats.d.ts: -------------------------------------------------------------------------------- 1 | export interface IFormatTest { 2 | format: any, 3 | params?: any, 4 | state: any, 5 | dependencies?: any, 6 | expected: any 7 | } -------------------------------------------------------------------------------- /client/src/tests/data-formats/formats/index.ts: -------------------------------------------------------------------------------- 1 | import { IFormatTest } from './formats'; 2 | import bars from './bars'; 3 | import filter from './filter'; 4 | import flags from './flags'; 5 | import retention from './retention'; 6 | import scorecard from './scorecard'; 7 | import timeline from './timeline'; 8 | import timespan from './timespan'; 9 | import filtered_samples from './filtered_samples'; 10 | 11 | export const formatTests = > { 12 | bars, 13 | filter, 14 | flags, 15 | retention, 16 | scorecard, 17 | timeline, 18 | timespan, 19 | filtered_samples 20 | }; -------------------------------------------------------------------------------- /client/src/tests/data-formats/formats/retention.ts: -------------------------------------------------------------------------------- 1 | import { IFormatTest } from './formats'; 2 | 3 | export default [ 4 | { 5 | format: 'retention', 6 | state: { 7 | values: [ 8 | { 9 | totalUnique: 10, 10 | totalUniqueUsersIn24hr: 5, 11 | totalUniqueUsersIn7d: 7, 12 | totalUniqueUsersIn30d: 10, 13 | returning24hr: 3, 14 | returning7d: 3, 15 | returning30d: 6 16 | } 17 | ] 18 | }, 19 | expected: { 20 | 'returning': 0, 21 | 'returning24hr': 3, 22 | 'returning30d': 6, 23 | 'returning7d': 3, 24 | 'total': 0, 25 | 'totalUnique': 10, 26 | 'totalUniqueUsersIn24hr': 5, 27 | 'totalUniqueUsersIn30d': 10, 28 | 'totalUniqueUsersIn7d': 7, 29 | 'values': [ 30 | { 'retention': '60%', 'returning': 3, 'timespan': '24 hours', 'unique': 5 }, 31 | { 'retention': '43%', 'returning': 3, 'timespan': '7 days', 'unique': 7 }, 32 | { 'retention': '60%', 'returning': 6, 'timespan': '30 days', 'unique': 10 } 33 | ] 34 | } 35 | }, 36 | { 37 | format: 'retention', 38 | dependencies: { 39 | selectedTimespan: 'PT24H' 40 | }, 41 | state: { 42 | values: [ 43 | { 44 | totalUnique: 10, 45 | totalUniqueUsersIn24hr: 5, 46 | totalUniqueUsersIn7d: 7, 47 | totalUniqueUsersIn30d: 10, 48 | returning24hr: 3, 49 | returning7d: 3, 50 | returning30d: 6 51 | } 52 | ] 53 | }, 54 | expected: { 55 | 'returning': 3, 56 | 'returning24hr': 3, 57 | 'returning30d': 6, 58 | 'returning7d': 3, 59 | 'total': 5, 60 | 'totalUnique': 10, 61 | 'totalUniqueUsersIn24hr': 5, 62 | 'totalUniqueUsersIn30d': 10, 63 | 'totalUniqueUsersIn7d': 7, 64 | 'values': [ 65 | { 'retention': '60%', 'returning': 3, 'timespan': '24 hours', 'unique': 5 }, 66 | { 'retention': '43%', 'returning': 3, 'timespan': '7 days', 'unique': 7 }, 67 | { 'retention': '60%', 'returning': 6, 'timespan': '30 days', 'unique': 10 } 68 | ] 69 | } 70 | } 71 | ]; -------------------------------------------------------------------------------- /client/src/tests/data-formats/formats/scorecard.ts: -------------------------------------------------------------------------------- 1 | import { IFormatTest } from './formats'; 2 | 3 | export default { 4 | format: { 5 | type: 'scorecard', 6 | args: { 7 | thresholds: [{ value: 0, heading: 'Heading', color: '#fff', icon: 'chat' }], 8 | subvalueField: 'other_count', 9 | subvalueThresholds: [{ subvalue: 0, subheading: 'Subheading' }] 10 | } 11 | }, 12 | state: { 13 | values: [{ count: 99, other_count: 44 }] 14 | }, 15 | expected: { 16 | 'value': '99', 17 | 'heading': 'Heading', 18 | 'color': '#fff', 19 | 'icon': 'chat', 20 | 'subvalue': 44, 21 | 'subheading': 'Subheading' 22 | } 23 | }; -------------------------------------------------------------------------------- /client/src/tests/data-formats/formats/timeline.ts: -------------------------------------------------------------------------------- 1 | import { IFormatTest } from './formats'; 2 | 3 | export default { 4 | format: { 5 | type: 'timeline', 6 | args: { 7 | timeField: 'timestamp', 8 | lineField: 'line', 9 | valueField: 'value' 10 | } 11 | }, 12 | state: { 13 | values: [ 14 | { timestamp: '2015-01-01T00:00:01Z', line: 'red', value: 40 }, 15 | { timestamp: '2015-01-02T00:00:01Z', line: 'red', value: 50 }, 16 | { timestamp: '2015-01-03T00:00:01Z', line: 'red', value: 60 }, 17 | { timestamp: '2015-01-01T00:00:01Z', line: 'blue', value: 10 }, 18 | { timestamp: '2015-01-02T00:00:01Z', line: 'blue', value: 77 }, 19 | { timestamp: '2015-01-03T00:00:01Z', line: 'blue', value: 30 } 20 | ] 21 | }, 22 | expected: { 23 | "graphData": [ 24 | {"blue": 10, "red": 40, "time": "Thu, 01 Jan 2015 00:00:01 GMT"}, 25 | {"blue": 77, "red": 50, "time": "Fri, 02 Jan 2015 00:00:01 GMT"}, 26 | {"blue": 30, "red": 60, "time": "Sat, 03 Jan 2015 00:00:01 GMT"} 27 | ], 28 | "lines": ["red", "blue"], 29 | "pieData": [ 30 | {"name": "red", "value": 150}, 31 | {"name": "blue", "value": 117} 32 | ], 33 | "timeFormat": "date" 34 | } 35 | }; -------------------------------------------------------------------------------- /client/src/tests/data-formats/formats/timespan.ts: -------------------------------------------------------------------------------- 1 | import { IFormatTest } from './formats'; 2 | 3 | export default [ 4 | { 5 | format: 'timespan', 6 | params: { 7 | values: ["24 hours","1 week","1 month","3 months"] 8 | }, 9 | state: { 10 | selectedValue: "1 month" 11 | }, 12 | expected: { 13 | "granularity": "1d", 14 | "queryTimespan": "P30D", 15 | "values-all": ["24 hours", "1 week", "1 month", "3 months"], 16 | "values-selected": "1 month" 17 | } 18 | }, 19 | { 20 | format: 'timespan', 21 | params: { 22 | values: ["24 hours","1 week","1 month","3 months"] 23 | }, 24 | state: { 25 | selectedValue: "24 hours" 26 | }, 27 | expected: { 28 | "granularity": "5m", 29 | "queryTimespan": "PT24H", 30 | "values-all": ["24 hours", "1 week", "1 month", "3 months"], 31 | "values-selected": "24 hours" 32 | } 33 | } 34 | ]; -------------------------------------------------------------------------------- /client/src/tests/data-sources/ApplicationInsights.Fail.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import * as TestUtils from 'react-dom/test-utils'; 4 | 5 | import { IDataSourceDictionary } from '../../data-sources'; 6 | import { setupTests } from '../utils/setup'; 7 | import { appInsightsUri } from '../../data-sources/plugins/ApplicationInsights/common'; 8 | 9 | import { Toast } from '../../components/Toast'; 10 | import { mockRequests } from '../mocks/requests/application-insights'; 11 | import dashboardMock from '../mocks/dashboards/application-insights-fail'; 12 | 13 | describe('Data Source: Application Insights: Query', () => { 14 | 15 | let dataSources: IDataSourceDictionary = {}; 16 | let element; 17 | 18 | beforeAll(done => { 19 | 20 | mockRequests(); 21 | element = TestUtils.renderIntoDocument(); 22 | TestUtils.isElementOfType(element, 'div'); 23 | setupTests(dashboardMock, ds => dataSources = ds, () => setTimeout(done, 100)); 24 | }); 25 | 26 | it ('Query for 30 months with data rows', () => { 27 | 28 | let component = TestUtils.scryRenderedComponentsWithType(element, Toast); 29 | expect(component[0].state.toasts.length).toBe(1); 30 | 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /client/src/tests/data-sources/ApplicationInsights.Query.test.ts: -------------------------------------------------------------------------------- 1 | import { IDataSourceDictionary } from '../../data-sources'; 2 | import { setupTests } from '../utils/setup'; 3 | import { appInsightsUri } from '../../data-sources/plugins/ApplicationInsights/common'; 4 | 5 | import { mockRequests } from '../mocks/requests/application-insights'; 6 | import dashboardMock from '../mocks/dashboards/application-insights'; 7 | 8 | describe('Data Source: Application Insights: Query', () => { 9 | 10 | let dataSources: IDataSourceDictionary = {}; 11 | 12 | beforeAll((done) => { 13 | 14 | mockRequests(); 15 | setupTests(dashboardMock, ds => dataSources = ds, done); 16 | }); 17 | 18 | it ('Query for 30 months with data rows', () => { 19 | 20 | expect(dataSources).toHaveProperty('events'); 21 | expect(dataSources.timespan).toHaveProperty('store'); 22 | expect(dataSources.events).toHaveProperty('store'); 23 | expect(dataSources.events).toHaveProperty('action'); 24 | expect(dataSources.events.store).toHaveProperty('state'); 25 | 26 | // Listening to store to catch values arrival from app insights 27 | return new Promise((resolve, reject) => { 28 | var stateUpdate = (state => { 29 | try { 30 | expect(state).toHaveProperty('values'); 31 | expect(state.values).toHaveLength(15); 32 | expect(state.values[0]).toHaveProperty('name', 'name: message.received'); 33 | expect(state.values[0]).toHaveProperty('rowname', 'RowName: name: message.received'); 34 | expect(state.values[0]).toHaveProperty('rowindex', 'RowIndex: 0'); 35 | return resolve(); 36 | } catch (e) { 37 | return reject(e); 38 | } finally { 39 | dataSources.events.store.unlisten(stateUpdate); 40 | } 41 | }); 42 | dataSources.events.store.listen(stateUpdate); 43 | }); 44 | }); 45 | 46 | it ('Query for 24 hours with 0 rows', () => { 47 | dataSources.timespan.action.updateSelectedValue.defer('24 hours'); 48 | 49 | return new Promise((resolve, reject) => { 50 | var stateUpdate = (state => { 51 | try { 52 | expect(state).toHaveProperty('values'); 53 | expect(state.values).toHaveLength(0); 54 | return resolve(); 55 | } catch (e) { 56 | return reject(e); 57 | } finally { 58 | dataSources.events.store.unlisten(stateUpdate); 59 | } 60 | }); 61 | dataSources.events.store.listen(stateUpdate); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /client/src/tests/data-sources/Constants.test.ts: -------------------------------------------------------------------------------- 1 | import { IDataSourceDictionary } from '../../data-sources'; 2 | import { setupTests } from '../utils/setup'; 3 | import dashboardMock from '../mocks/dashboards/constants'; 4 | 5 | describe('Data Source: Constant', () => { 6 | 7 | let dataSources: IDataSourceDictionary; 8 | 9 | beforeAll(done => { 10 | setupTests(dashboardMock, ds => dataSources = ds, () => setTimeout(done, 100)); 11 | }); 12 | 13 | it ('Check basic data == 3 rows', () => { 14 | 15 | expect(dataSources).toHaveProperty('data');; 16 | expect(dataSources.data).toHaveProperty('store'); 17 | expect(dataSources.data).toHaveProperty('action'); 18 | expect(dataSources.data.store).toHaveProperty('state'); 19 | expect(dataSources.data.store.state).toMatchObject({ 20 | "selectedValue": "default", 21 | "someJsonValues": [ 22 | { "count": 2, "id": 1 }, 23 | { "count": 0, "id": 2 }, 24 | { "count": 10, "id": 3 } 25 | ], 26 | "values": [], 27 | }); 28 | }); 29 | 30 | it ('Check updating of store state', () => { 31 | 32 | dataSources.data.action.updateDependencies(); 33 | expect(dataSources.data.store.state).toMatchObject({ 34 | "selectedValue": "default", 35 | "someJsonValues": [ 36 | { "count": 2, "id": 1 }, 37 | { "count": 0, "id": 2 }, 38 | { "count": 10, "id": 3 }, 39 | { "count": 10, "id": 3 } 40 | ], 41 | "values": [], 42 | }); 43 | 44 | }); 45 | 46 | }); 47 | -------------------------------------------------------------------------------- /client/src/tests/data-sources/CosmosDB.Query.test.ts: -------------------------------------------------------------------------------- 1 | import { IDataSourceDictionary } from '../../data-sources'; 2 | import { setupTests } from '../utils/setup'; 3 | import { COSMOS_DB_QUERY_URL } from '../../data-sources/plugins/CosmosDB/Query'; 4 | 5 | import { mockRequests } from '../mocks/requests/cosmosdb'; 6 | import dashboardMock from '../mocks/dashboards/cosmosdb'; 7 | 8 | describe('Data Source: CosmosDB: Query', () => { 9 | 10 | let dataSources: IDataSourceDictionary = {}; 11 | 12 | beforeAll(done => { 13 | mockRequests(); 14 | setupTests(dashboardMock, ds => dataSources = ds, done); 15 | }); 16 | 17 | it('Query for data', () => { 18 | 19 | expect(dataSources).toHaveProperty('events'); 20 | expect(dataSources.timespan).toHaveProperty('store'); 21 | expect(dataSources.events).toHaveProperty('store'); 22 | expect(dataSources.events).toHaveProperty('action'); 23 | expect(dataSources.events.store).toHaveProperty('state'); 24 | 25 | return new Promise((resolve, reject) => { 26 | var stateUpdate = (state => { 27 | try { 28 | expect(state).toMatchSnapshot('cosmosDBStubResponse') 29 | return resolve(); 30 | } catch (e) { 31 | return reject(e); 32 | } finally { 33 | dataSources.events.store.unlisten(stateUpdate); 34 | } 35 | }); 36 | dataSources.events.store.listen(stateUpdate); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /client/src/tests/data-sources/DirectLine.Query.test.ts: -------------------------------------------------------------------------------- 1 | import { IDataSourceDictionary } from '../../data-sources'; 2 | import { setupTests } from '../utils/setup'; 3 | import { DIRECT_LINE_URL } from '../../data-sources/plugins/BotFramework/DirectLine'; 4 | 5 | import { mockRequests } from '../mocks/requests/directline'; 6 | import dashboardMock from '../mocks/dashboards/bot-framework-directline'; 7 | 8 | describe('Data Source: DirectLine: Query', () => { 9 | 10 | let dataSources: IDataSourceDictionary = {}; 11 | 12 | beforeAll(done => { 13 | mockRequests(); 14 | setupTests(dashboardMock, ds => dataSources = ds, done); 15 | }); 16 | 17 | it ('Query for data', () => { 18 | expect(dataSources).toHaveProperty('events'); 19 | expect(dataSources.timespan).toHaveProperty('store'); 20 | expect(dataSources.events).toHaveProperty('store'); 21 | expect(dataSources.events).toHaveProperty('action'); 22 | expect(dataSources.events.store).toHaveProperty('state'); 23 | 24 | return new Promise((resolve, reject) => { 25 | var stateUpdate = (state => { 26 | try { 27 | expect(state).toMatchSnapshot('directLineStubResponse') 28 | return resolve(); 29 | } catch (e) { 30 | return reject(e); 31 | } finally { 32 | dataSources.events.store.unlisten(stateUpdate); 33 | } 34 | }); 35 | dataSources.events.store.listen(stateUpdate); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /client/src/tests/data-sources/Samples.test.ts: -------------------------------------------------------------------------------- 1 | import { IDataSourceDictionary } from '../../data-sources'; 2 | import { setupTests } from '../utils/setup'; 3 | import dashboardMock from '../mocks/dashboards/samples'; 4 | import { setTimeout } from 'timers'; 5 | 6 | describe('Data Source: Samples', () => { 7 | 8 | let dataSources: IDataSourceDictionary; 9 | 10 | beforeAll(done => { 11 | setupTests(dashboardMock, ds => dataSources = ds, () => setTimeout(done, 100)); 12 | }); 13 | 14 | it ('Check basic data == 3 rows', () => { 15 | 16 | expect(dataSources).toHaveProperty('samples'); 17 | expect(dataSources.samples).toHaveProperty('store'); 18 | expect(dataSources.samples).toHaveProperty('action'); 19 | expect(dataSources.samples.store).toHaveProperty('state', { 20 | values: [ 21 | { id: "value1", count: 60 }, 22 | { id: "value2", count: 10 }, 23 | { id: "value3", count: 30 } 24 | ] 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /client/src/tests/data-sources/__snapshots__/CosmosDB.Query.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`cosmosDBStubResponse 1`] = ` 4 | Store { 5 | "Documents": "fakedata", 6 | "_count": 0, 7 | "_rid": undefined, 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /client/src/tests/data-sources/__snapshots__/DirectLine.Query.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`directLineStubResponse 1`] = ` 4 | Store { 5 | "values": "fakedata", 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /client/src/tests/elements/Area.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import * as TestUtils from 'react-dom/test-utils'; 4 | 5 | import { Card } from 'react-md/lib/Cards'; 6 | 7 | import AreaComponent from '../../components/generic/Area'; 8 | import { DataSourceConnector, IDataSourceDictionary } from '../../data-sources'; 9 | 10 | import dashboardMock from '../mocks/dashboards/timeline'; 11 | import ElementConnector from '../../components/ElementConnector'; 12 | 13 | describe('Area', () => { 14 | 15 | let dataSources: IDataSourceDictionary = {}; 16 | let area; 17 | 18 | beforeAll((done) => { 19 | 20 | DataSourceConnector.createDataSources(dashboardMock, dashboardMock.config.connections); 21 | dataSources = DataSourceConnector.getDataSources(); 22 | 23 | setTimeout(() => { 24 | 25 | try { 26 | let {id, dependencies, source, actions, title } = dashboardMock.elements[0]; 27 | let atts = {id, dependencies, source, actions, title }; 28 | let timelineElement = ElementConnector.createGenericElement( 29 | AreaComponent, 30 | 'timeline', 31 | 0, 32 | source, 33 | dependencies, 34 | actions, 35 | null, 36 | title); 37 | area = TestUtils.renderIntoDocument(timelineElement); 38 | TestUtils.isElementOfType(area, 'div'); 39 | 40 | // Adding a timeout to make sure Flux cycle is complete 41 | setTimeout(done, 100); 42 | 43 | } catch (e) { 44 | return done(e); 45 | } 46 | 47 | }, 100); 48 | }) 49 | 50 | it('Render inside a Card', () => { 51 | let card = TestUtils.scryRenderedComponentsWithType(area, Card); 52 | expect(card.length).toBe(1); 53 | }); 54 | 55 | it('Render a timeline entity', () => { 56 | let timelineElement = TestUtils.scryRenderedDOMComponentsWithClass(area, "md-card"); 57 | expect(timelineElement.length).toBe(1); 58 | }); 59 | 60 | afterAll(() => { 61 | ReactDOM.unmountComponentAtNode(area); 62 | }) 63 | }); -------------------------------------------------------------------------------- /client/src/tests/elements/AutoRefreshSelector.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import * as TestUtils from 'react-dom/test-utils'; 4 | 5 | import { Card } from 'react-md/lib/Cards'; 6 | 7 | import { AutoRefreshSelector, RefreshActions, RefreshStore } from '../../components/AutoRefreshSelector'; 8 | import { DataSourceConnector, IDataSourceDictionary } from '../../data-sources'; 9 | 10 | import dashboardMock from '../mocks/dashboards/pie'; 11 | import ElementConnector from '../../components/ElementConnector'; 12 | 13 | describe('AutoRefreshSelector', () => { 14 | 15 | let dataSources: IDataSourceDictionary = {}; 16 | let refresher; 17 | 18 | beforeAll((done) => { 19 | DataSourceConnector.createDataSources(dashboardMock, dashboardMock.config.connections); 20 | dataSources = DataSourceConnector.getDataSources(); 21 | 22 | setTimeout(() => { 23 | 24 | try { 25 | let {id, dependencies, source, actions, props, title, subtitle } = dashboardMock.elements[0]; 26 | let atts = {id, dependencies, source, actions, props, title, subtitle }; 27 | let autoRefresherElement = ElementConnector.createGenericElement( 28 | AutoRefreshSelector, 29 | 'autorefresher', 30 | 0, 31 | source, 32 | dependencies, 33 | actions, 34 | props, 35 | title, 36 | subtitle); 37 | refresher = TestUtils.renderIntoDocument(autoRefresherElement); 38 | TestUtils.isElementOfType(refresher, 'div'); 39 | 40 | // Adding a timeout to make sure Flux cycle is complete 41 | setTimeout(done, 100); 42 | 43 | } catch (e) { 44 | return done(e); 45 | } 46 | 47 | }, 100); 48 | }) 49 | 50 | it('Update interval to "None"', () => { 51 | RefreshStore.listen((state) => { 52 | expect(state.refreshInterval).toBe(-1); 53 | }); 54 | RefreshActions.setRefreshTimer( 55 | -1, 56 | // empty call back. we don't care which logic is scheduled... 57 | () => { 58 | }); 59 | }); 60 | 61 | it('Update interval to a positive value', () => { 62 | RefreshStore.listen((state) => { 63 | expect(state.refreshInterval).toBe(10000); 64 | }); 65 | RefreshActions.setRefreshTimer( 66 | 10000, 67 | // empty call back. we don't care which logic is scheduled... 68 | () => { 69 | }); 70 | }); 71 | 72 | afterAll(() => { 73 | ReactDOM.unmountComponentAtNode(refresher); 74 | }) 75 | }); -------------------------------------------------------------------------------- /client/src/tests/elements/CardSettings.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import * as TestUtils from 'react-dom/test-utils'; 4 | 5 | import { Card } from 'react-md/lib/Cards'; 6 | 7 | import { Settings, SettingsActions, SettingsStore } from '../../components/Card/Settings'; 8 | import { DataSourceConnector, IDataSourceDictionary } from '../../data-sources'; 9 | 10 | import dashboardMock from '../mocks/dashboards/dashboard'; 11 | import ElementConnector from '../../components/ElementConnector'; 12 | 13 | describe('Card settings store', () => { 14 | 15 | let dataSources: IDataSourceDictionary = {}; 16 | 17 | let settings; 18 | 19 | beforeAll((done) => { 20 | DataSourceConnector.createDataSources(dashboardMock, dashboardMock.config.connections); 21 | dataSources = DataSourceConnector.getDataSources(); 22 | 23 | setTimeout(() => { 24 | 25 | try { 26 | let {id, dependencies, source, actions, props, title, subtitle } = dashboardMock.elements[0]; 27 | let atts = {id, dependencies, source, actions, props, title, subtitle }; 28 | let autoRefresherElement = ElementConnector.createGenericElement( 29 | Settings, 30 | 'settings', 31 | 0, 32 | source, 33 | dependencies, 34 | actions, 35 | props, 36 | title, 37 | subtitle); 38 | settings = TestUtils.renderIntoDocument(autoRefresherElement); 39 | TestUtils.isElementOfType(settings, 'div'); 40 | 41 | // Adding a timeout to make sure Flux cycle is complete 42 | setTimeout(done, 100); 43 | 44 | } catch (e) { 45 | return done(e); 46 | } 47 | 48 | }, 100); 49 | }) 50 | 51 | it('rendering the Card\'s setting component', () => { 52 | }); 53 | 54 | afterAll(() => { 55 | ReactDOM.unmountComponentAtNode(settings); 56 | }) 57 | }); -------------------------------------------------------------------------------- /client/src/tests/elements/CardSettingsStore.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import * as TestUtils from 'react-dom/test-utils'; 4 | 5 | import { Card } from 'react-md/lib/Cards'; 6 | 7 | import { Settings, SettingsActions, SettingsStore } from '../../components/Card/Settings'; 8 | import { DataSourceConnector, IDataSourceDictionary } from '../../data-sources'; 9 | 10 | import dashboardMock from '../mocks/dashboards/dashboard'; 11 | import ElementConnector from '../../components/ElementConnector'; 12 | 13 | describe('Card settings store', () => { 14 | 15 | let dataSources: IDataSourceDictionary = {}; 16 | 17 | it('Update interval to "None"', () => { 18 | (SettingsActions.getExportData as any).defer(dashboardMock); 19 | }); 20 | }); -------------------------------------------------------------------------------- /client/src/tests/elements/Dialog.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import * as TestUtils from 'react-dom/test-utils'; 4 | 5 | import { Dialog, DialogsActions, loadDialogsFromDashboard } from '../../components/generic/Dialogs'; 6 | import MDDialog from 'react-md/lib/Dialogs'; 7 | 8 | import emptyDashboard from '../mocks/dashboards/dashboard'; 9 | import dashboardWithDialog from '../mocks/dashboards/dialogs'; 10 | import dialogData from '../mocks/dialog'; 11 | 12 | describe('Dialog', () => { 13 | 14 | let dialog; 15 | 16 | it('Load empty dialogs', () => { 17 | let dialogs = loadDialogsFromDashboard(emptyDashboard); 18 | expect(dialogs).toHaveLength(0); 19 | }); 20 | 21 | it('Dynamic loading of 1 dialog', () => { 22 | let dialogs = loadDialogsFromDashboard(dashboardWithDialog); 23 | expect(dialogs).toHaveLength(1); 24 | }); 25 | 26 | it('First render without content', () => { 27 | dialog = TestUtils.renderIntoDocument(); 28 | let elements = TestUtils.scryRenderedComponentsWithType(dialog, MDDialog); 29 | expect(elements.length).toBe(0); 30 | }); 31 | 32 | it('Opening a dialog', function () { 33 | DialogsActions.openDialog(dialogData.id, { title: 'Title', intent: 'Intent', queryspan: '30D' }); 34 | let elements = TestUtils.scryRenderedComponentsWithType(dialog, MDDialog); 35 | expect(elements.length).toBe(1); 36 | }); 37 | 38 | it('Closing a dialog', function () { 39 | DialogsActions.closeDialog(); 40 | let elements = TestUtils.scryRenderedComponentsWithType(dialog, MDDialog); 41 | expect(elements.length).toBe(0); 42 | }); 43 | 44 | afterAll(() => { 45 | ReactDOM.unmountComponentAtNode(dialog); 46 | }) 47 | }); -------------------------------------------------------------------------------- /client/src/tests/elements/IconPicker.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import * as TestUtils from 'react-dom/test-utils'; 4 | 5 | import Snackbar from 'react-md/lib/Snackbars'; 6 | import IconPicker from '../../components/Home/IconPicker'; 7 | 8 | import icons from '../../constants/icons'; 9 | 10 | const DEFAULT_ICON = 'dashboard'; 11 | const DEFAULT_ICON2 = 'view_quilt'; 12 | 13 | describe('IconPicker', () => { 14 | 15 | let element, element2; 16 | 17 | beforeAll(() => { 18 | element = TestUtils.renderIntoDocument(); 19 | TestUtils.isElementOfType(element, 'div'); 20 | 21 | element2 = TestUtils.renderIntoDocument(); 22 | TestUtils.isElementOfType(element2, 'div'); 23 | }); 24 | 25 | it('First render component', () => { 26 | let component = TestUtils.scryRenderedComponentsWithType(element, IconPicker); 27 | expect(component.length).toBe(1); 28 | }); 29 | 30 | it('Should populate with icons constant', () => { 31 | let component = TestUtils.scryRenderedComponentsWithType(element, IconPicker); 32 | expect(IconPicker.listItems.length).toEqual(icons.length); 33 | }); 34 | 35 | it('Default icon should be ' + DEFAULT_ICON, () => { 36 | let component = TestUtils.scryRenderedComponentsWithType(element, IconPicker); 37 | expect(component[0].getIcon()).toBe(DEFAULT_ICON); 38 | }); 39 | 40 | it('Default icon should be ' + DEFAULT_ICON2, () => { 41 | let component = TestUtils.scryRenderedComponentsWithType(element2, IconPicker); 42 | expect(component[0].getIcon()).toBe(DEFAULT_ICON2); 43 | }); 44 | 45 | afterAll(() => { 46 | ReactDOM.unmountComponentAtNode(element); 47 | }); 48 | 49 | }); -------------------------------------------------------------------------------- /client/src/tests/elements/Spinner.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import * as TestUtils from 'react-dom/test-utils'; 4 | 5 | import CircularProgress from 'react-md/lib/Progress/CircularProgress'; 6 | import { Spinner, SpinnerActions } from '../../components/Spinner'; 7 | 8 | describe('Spinner', () => { 9 | 10 | let spinner; 11 | 12 | beforeAll(() => { 13 | spinner = TestUtils.renderIntoDocument(); 14 | TestUtils.isElementOfType(spinner, 'div'); 15 | }) 16 | 17 | it('First render without content', () => { 18 | let progress = TestUtils.scryRenderedComponentsWithType(spinner, CircularProgress); 19 | expect(progress.length).toBe(0); 20 | }); 21 | 22 | it ('Start page loading', () => { 23 | SpinnerActions.startPageLoading(null); 24 | 25 | let progress = TestUtils.scryRenderedComponentsWithType(spinner, CircularProgress); 26 | expect(progress.length).toBe(1); 27 | }); 28 | 29 | it ('Stop page loading', () => { 30 | SpinnerActions.endPageLoading(null); 31 | 32 | let progress = TestUtils.scryRenderedComponentsWithType(spinner, CircularProgress); 33 | expect(progress.length).toBe(0); 34 | }); 35 | 36 | it ('Start request loading', () => { 37 | SpinnerActions.startRequestLoading(null); 38 | 39 | let progress = TestUtils.scryRenderedComponentsWithType(spinner, CircularProgress); 40 | expect(progress.length).toBe(1); 41 | }); 42 | 43 | it ('Start request loading', () => { 44 | SpinnerActions.endRequestLoading(null); 45 | 46 | let progress = TestUtils.scryRenderedComponentsWithType(spinner, CircularProgress); 47 | expect(progress.length).toBe(0); 48 | }); 49 | 50 | afterAll(() => { 51 | ReactDOM.unmountComponentAtNode(spinner); 52 | }) 53 | }); -------------------------------------------------------------------------------- /client/src/tests/elements/Table.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import * as TestUtils from 'react-dom/test-utils'; 4 | 5 | import { Card } from 'react-md/lib/Cards'; 6 | import { DataTable, TableRow } from 'react-md/lib/DataTables'; 7 | import { Spinner, SpinnerActions } from '../../components/Spinner'; 8 | import Table from '../../components/generic/Table'; 9 | import { DataSourceConnector, IDataSourceDictionary } from '../../data-sources'; 10 | 11 | //import dataSourceMock from '../mocks/dataSource'; 12 | import dashboardMock from '../mocks/dashboards/table'; 13 | 14 | describe('Table', () => { 15 | 16 | let dataSources: IDataSourceDictionary = {}; 17 | let table; 18 | 19 | beforeAll((done) => { 20 | 21 | DataSourceConnector.createDataSources(dashboardMock, dashboardMock.config.connections); 22 | dataSources = DataSourceConnector.getDataSources(); 23 | 24 | let {id, dependencies, actions, props, title, subtitle } = dashboardMock.elements[0]; 25 | let atts = {id, dependencies, actions, props, title, subtitle }; 26 | table = TestUtils.renderIntoDocument(); 27 | TestUtils.isElementOfType(table, 'div'); 28 | 29 | setTimeout(done, 10); 30 | }) 31 | 32 | it('Render inside a Card', () => { 33 | dataSources['samples'].action.updateDependencies({ 34 | 'table-values': dataSources['samples'].store.state['values'] 35 | }); 36 | let card = TestUtils.scryRenderedComponentsWithType(table, Card); 37 | expect(card.length).toBe(1); 38 | }); 39 | 40 | it('Render a Data Table entity', () => { 41 | let progress = TestUtils.scryRenderedComponentsWithType(table, DataTable); 42 | expect(progress.length).toBe(1); 43 | }); 44 | 45 | it('Rows == 4', () => { 46 | let rows = TestUtils.scryRenderedComponentsWithType(table, TableRow); 47 | expect(rows.length).toBe(4); 48 | }); 49 | 50 | it('Rows == 0', () => { 51 | dataSources['samples'].action.updateDependencies({ 52 | 'table-values': [] 53 | }); 54 | let rows = TestUtils.scryRenderedComponentsWithType(table, TableRow); 55 | expect(rows.length).toBe(1); 56 | }); 57 | 58 | afterAll(() => { 59 | ReactDOM.unmountComponentAtNode(table); 60 | }) 61 | }); -------------------------------------------------------------------------------- /client/src/tests/elements/Timeline.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import * as TestUtils from 'react-dom/test-utils'; 4 | 5 | import { Card } from 'react-md/lib/Cards'; 6 | 7 | import TimelineComponent from '../../components/generic/Timeline'; 8 | import { DataSourceConnector, IDataSourceDictionary } from '../../data-sources'; 9 | 10 | import dashboardMock from '../mocks/dashboards/timeline'; 11 | import ElementConnector from '../../components/ElementConnector'; 12 | 13 | describe('Timeline', () => { 14 | 15 | let dataSources: IDataSourceDictionary = {}; 16 | let timeline; 17 | 18 | beforeAll((done) => { 19 | 20 | DataSourceConnector.createDataSources(dashboardMock, dashboardMock.config.connections); 21 | dataSources = DataSourceConnector.getDataSources(); 22 | 23 | setTimeout(() => { 24 | 25 | try { 26 | let {id, dependencies, source, actions, title } = dashboardMock.elements[0]; 27 | let atts = {id, dependencies, source, actions, title }; 28 | let timelineElement = ElementConnector.createGenericElement( 29 | TimelineComponent, 30 | 'timeline', 31 | 0, 32 | source, 33 | dependencies, 34 | actions, 35 | null, 36 | title); 37 | timeline = TestUtils.renderIntoDocument(timelineElement); 38 | TestUtils.isElementOfType(timeline, 'div'); 39 | 40 | // Adding a timeout to make sure Flux cycle is complete 41 | setTimeout(done, 100); 42 | 43 | } catch (e) { 44 | return done(e); 45 | } 46 | 47 | }, 100); 48 | }) 49 | 50 | it('Render inside a Card', () => { 51 | let card = TestUtils.scryRenderedComponentsWithType(timeline, Card); 52 | expect(card.length).toBe(1); 53 | }); 54 | 55 | it('Render a timeline entity', () => { 56 | let timelineElement = TestUtils.scryRenderedDOMComponentsWithClass(timeline, "md-card"); 57 | expect(timelineElement.length).toBe(1); 58 | }); 59 | 60 | afterAll(() => { 61 | ReactDOM.unmountComponentAtNode(timeline); 62 | }) 63 | }); -------------------------------------------------------------------------------- /client/src/tests/elements/Toast.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import * as TestUtils from 'react-dom/test-utils'; 4 | 5 | import Snackbar from 'react-md/lib/Snackbars'; 6 | import { Toast, IToast, ToastActions } from '../../components/Toast'; 7 | 8 | const TEST_TOAST_1: IToast = { 9 | text: "first test toast" 10 | }; 11 | const TEST_TOAST_2: IToast = { 12 | text: "second test toast" 13 | }; 14 | 15 | describe('Toast', () => { 16 | 17 | let element; 18 | 19 | beforeAll(() => { 20 | element = TestUtils.renderIntoDocument(); 21 | TestUtils.isElementOfType(element, 'div'); 22 | }); 23 | 24 | it('First render component', () => { 25 | let component = TestUtils.scryRenderedComponentsWithType(element, Toast); 26 | expect(component.length).toBe(1); 27 | }); 28 | 29 | it('First render without toasts', () => { 30 | let component = TestUtils.scryRenderedComponentsWithType(element, Toast); 31 | expect(component[0].state.toasts.length).toBe(0); 32 | }); 33 | 34 | it('Render after adding toast', () => { 35 | let component = TestUtils.scryRenderedComponentsWithType(element, Toast); 36 | ToastActions.addToast(TEST_TOAST_1); 37 | expect(component[0].state.toasts.length).toBe(1); 38 | }); 39 | 40 | it('Render only once after adding duplicate toast', () => { 41 | let component = TestUtils.scryRenderedComponentsWithType(element, Toast); 42 | ToastActions.addToast(TEST_TOAST_1); 43 | ToastActions.addToast(TEST_TOAST_1); 44 | expect(component[0].state.toasts.length).toBe(1); 45 | }); 46 | 47 | it('Render add subsequent toasts to queue', () => { 48 | let component = TestUtils.scryRenderedComponentsWithType(element, Toast); 49 | ToastActions.addToast(TEST_TOAST_1); 50 | ToastActions.addToast(TEST_TOAST_2); 51 | expect(component[0].state.toasts.length && component[0].state.queued.length).toBe(1); 52 | }); 53 | 54 | afterAll(() => { 55 | ReactDOM.unmountComponentAtNode(element); 56 | }); 57 | 58 | }); -------------------------------------------------------------------------------- /client/src/tests/mocks/dashboards/application-insights-fail.ts: -------------------------------------------------------------------------------- 1 | import timespan from './timespan'; 2 | import { createDashboard } from "./utils"; 3 | 4 | let dashboard = createDashboard(timespan); 5 | dashboard.config.connections["application-insights"] = { appId: 'id_fail', apiKey: '1' }; 6 | dashboard.dataSources.push({ 7 | id: 'events', 8 | type: 'ApplicationInsights/Query', 9 | dependencies: { timespan: 'timespan', queryTimespan: 'timespan:queryTimespan' }, 10 | params: { 11 | query: `customEvents`, 12 | mappings: { 13 | name: (val, row, idx) => `name: ${val}`, 14 | rowname: (val, row, idx) => `RowName: ${row.name}`, 15 | rowindex: (val, row, idx) => `RowIndex: ${idx}` 16 | } 17 | } 18 | }); 19 | 20 | export default dashboard; -------------------------------------------------------------------------------- /client/src/tests/mocks/dashboards/application-insights-forked.ts: -------------------------------------------------------------------------------- 1 | import timespan from './timespan'; 2 | import { createDashboard } from "./utils"; 3 | 4 | let dashboard = createDashboard(timespan); 5 | dashboard.config.connections["application-insights"] = { appId: '1', apiKey: '1' }; 6 | dashboard.dataSources.push({ 7 | id: "events", 8 | type: "ApplicationInsights/Query", 9 | dependencies: { timespan: "timespan",queryTimespan: "timespan:queryTimespan",granularity: "timespan:granularity" }, 10 | params: { 11 | table: "customEvents", 12 | queries: { 13 | array1: { 14 | query: () => `customEvents`, 15 | calculated: (filterChannels, dependencies, prevState) => { 16 | return { "array1-calc": filterChannels }; 17 | } 18 | }, 19 | array2: { 20 | query: () => `customEvents2` 21 | } 22 | } 23 | } 24 | }); 25 | 26 | export default dashboard; -------------------------------------------------------------------------------- /client/src/tests/mocks/dashboards/application-insights.ts: -------------------------------------------------------------------------------- 1 | import timespan from './timespan'; 2 | import { createDashboard } from "./utils"; 3 | 4 | let dashboard = createDashboard(timespan); 5 | dashboard.config.connections["application-insights"] = { appId: '1', apiKey: '1' }; 6 | dashboard.dataSources.push({ 7 | id: 'events', 8 | type: 'ApplicationInsights/Query', 9 | dependencies: { timespan: 'timespan', queryTimespan: 'timespan:queryTimespan' }, 10 | params: { 11 | query: `customEvents`, 12 | mappings: { 13 | name: (val, row, idx) => `name: ${val}`, 14 | rowname: (val, row, idx) => `RowName: ${row.name}`, 15 | rowindex: (val, row, idx) => `RowIndex: ${idx}` 16 | } 17 | } 18 | }); 19 | 20 | export default dashboard; -------------------------------------------------------------------------------- /client/src/tests/mocks/dashboards/area.ts: -------------------------------------------------------------------------------- 1 | import samples from './rich-samples'; 2 | import { createDashboard } from "./utils"; 3 | 4 | let dashboard = createDashboard(samples); 5 | dashboard.elements.push({ 6 | id: "area", 7 | type: "Area", 8 | title: "Area title", 9 | size: { w: 3,h: 8 }, 10 | source: "data", 11 | props: { showLegend: true,compact: true,entityType: "Messages" } 12 | }); 13 | 14 | export default dashboard; -------------------------------------------------------------------------------- /client/src/tests/mocks/dashboards/bot-framework-directline.ts: -------------------------------------------------------------------------------- 1 | import timespan from './timespan'; 2 | import { createDashboard } from "./utils"; 3 | 4 | let dashboard = createDashboard(timespan); 5 | dashboard.config.connections["bot-framework"] = { directLine: 'AbCdEf123456' }; 6 | dashboard.dataSources.push({ 7 | id: 'events', 8 | type: 'BotFramework/DirectLine', 9 | dependencies: { timespan: 'timespan', queryTimespan: 'timespan:queryTimespan' }, 10 | params: { 11 | } 12 | }); 13 | 14 | export default dashboard; -------------------------------------------------------------------------------- /client/src/tests/mocks/dashboards/constants.ts: -------------------------------------------------------------------------------- 1 | import { createDashboard } from "./utils"; 2 | 3 | let someJsonValues = [ 4 | { id: 1, count: 2 }, 5 | { id: 2, count: 0 } 6 | ]; 7 | 8 | let dashboard = createDashboard(); 9 | dashboard.dataSources.push({ 10 | id: 'data', 11 | type: 'Constant', 12 | params: { 13 | values: [], 14 | selectedValue: 'default' 15 | }, 16 | calculated: (state, dependencies) => { 17 | 18 | someJsonValues.push({ id: 3, count: 10 }); 19 | return { someJsonValues }; 20 | } 21 | }); 22 | 23 | export let dashboard2 = createDashboard(); 24 | dashboard2.dataSources.push( 25 | { 26 | id: "timespan", 27 | type: "Constant", 28 | params: { values: ["24 hours","1 week","1 month","3 months"],selectedValue: "1 month" }, 29 | calculated: (state, dependencies) => { 30 | var queryTimespan = 31 | state.selectedValue === '24 hours' ? 'PT24H' : 32 | state.selectedValue === '1 week' ? 'P7D' : 33 | state.selectedValue === '1 month' ? 'P30D' : 34 | 'P90D'; 35 | var granularity = 36 | state.selectedValue === '24 hours' ? '5m' : 37 | state.selectedValue === '1 week' ? '1d' : '1d'; 38 | 39 | return { queryTimespan, granularity }; 40 | } 41 | } 42 | ); 43 | 44 | export default dashboard; -------------------------------------------------------------------------------- /client/src/tests/mocks/dashboards/cosmosdb.ts: -------------------------------------------------------------------------------- 1 | import timespan from './timespan'; 2 | import { createDashboard } from "./utils"; 3 | 4 | let dashboard = createDashboard(timespan); 5 | dashboard.config.connections["cosmos-db"] = { host: 'http://localhost:3000', key: 'someKey' }; 6 | dashboard.dataSources.push({ 7 | id: "events", 8 | type: "CosmosDB/Query", 9 | dependencies: { timespan: "timespan", queryTimespan: "timespan:queryTimespan" }, 10 | params: { 11 | databaseId: "admin", 12 | collectionId: "conversations", 13 | query: () => `SELECT * FROM conversations WHERE (conversations.state = 0)`, 14 | parameters: [] 15 | }, 16 | calculated: (result) => { 17 | return result; 18 | } 19 | }); 20 | 21 | export default dashboard; -------------------------------------------------------------------------------- /client/src/tests/mocks/dashboards/dashboard.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | id: 'id', 3 | url: 'url', 4 | icon: 'icon', 5 | name: 'name', 6 | config: { 7 | connections: {}, 8 | layout: { 9 | cols: {lg: 12, md: 10, sm: 6, xs: 4, xxs: 2}, 10 | breakpoints: {lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0} 11 | } 12 | }, 13 | dataSources: [], 14 | filters: [], 15 | elements: [], 16 | dialogs: [] 17 | }; -------------------------------------------------------------------------------- /client/src/tests/mocks/dashboards/dialogs.ts: -------------------------------------------------------------------------------- 1 | import { createDashboard } from './utils'; 2 | 3 | let dashboard = createDashboard(); 4 | dashboard.dialogs.push({ 5 | id: "conversations", 6 | width: '60%', 7 | params: [ 'title', 'intent', 'queryspan' ], 8 | dataSources: [ 9 | { 10 | id: 'timespan', 11 | type: 'Constant', 12 | params: { 13 | values: [], 14 | selectedValue: 'default' 15 | }, 16 | calculated: (state, dependencies) => { 17 | 18 | var someJsonValues = [ 19 | { 20 | id: 1, 21 | count: 2, 22 | }, 23 | { 24 | id: 2, 25 | count: 0, 26 | }, 27 | { 28 | id: 3, 29 | count: 10, 30 | } 31 | ]; 32 | 33 | return { someJsonValues }; 34 | } 35 | } 36 | ], 37 | elements: [ 38 | { 39 | id: 'conversations-list', 40 | type: 'Table', 41 | title: 'Conversations', 42 | size: { w: 12, h: 16}, 43 | dependencies: { values: 'timespan:someJsonValues' }, 44 | props: { 45 | cols: [{ 46 | header: 'Conversation Id', 47 | field: 'id' 48 | }, { 49 | header: 'Count', 50 | field: 'count' 51 | }, { 52 | type: 'button', 53 | value: 'chat', 54 | onClick: 'openMessagesDialog' 55 | }] 56 | } 57 | } 58 | ] 59 | }); 60 | 61 | export default dashboard; -------------------------------------------------------------------------------- /client/src/tests/mocks/dashboards/pie.ts: -------------------------------------------------------------------------------- 1 | import samples from './rich-samples'; 2 | import { createDashboard } from "./utils"; 3 | 4 | let dashboard = createDashboard(samples); 5 | dashboard.elements.push({ 6 | id: "pie", 7 | type: "PieData", 8 | title: "Pie Data", 9 | subtitle: "Pie Data sub-title", 10 | size: { w: 3,h: 8 }, 11 | source: "data", 12 | props: { showLegend: true,compact: true,entityType: "Messages" } 13 | }); 14 | 15 | export default dashboard; -------------------------------------------------------------------------------- /client/src/tests/mocks/dashboards/rich-samples.ts: -------------------------------------------------------------------------------- 1 | import { createDashboard } from './utils'; 2 | 3 | let dashboard = createDashboard(); 4 | dashboard.dataSources.push({ 5 | id: 'data', 6 | type: 'Constant', 7 | params: { 8 | selectedValue: 'default', 9 | values: [ 10 | { timestamp: '2015-01-01 00:00:01', line: 'red', value: 40 }, 11 | { timestamp: '2015-01-02 00:00:01', line: 'red', value: 50 }, 12 | { timestamp: '2015-01-03 00:00:01', line: 'red', value: 60 }, 13 | { timestamp: '2015-01-01 00:00:01', line: 'blue', value: 10 }, 14 | { timestamp: '2015-01-02 00:00:01', line: 'blue', value: 77 }, 15 | { timestamp: '2015-01-03 00:00:01', line: 'blue', value: 30 } 16 | ] 17 | }, 18 | format: { 19 | type: 'timeline', 20 | args: { 21 | timeField: 'timestamp', 22 | lineField: 'line', 23 | valueField: 'value' 24 | } 25 | } 26 | }); 27 | 28 | export default dashboard; -------------------------------------------------------------------------------- /client/src/tests/mocks/dashboards/samples.ts: -------------------------------------------------------------------------------- 1 | import { createDashboard } from "./utils"; 2 | 3 | let dashboard = createDashboard(); 4 | dashboard.dataSources.push({ 5 | id: "samples", 6 | type: "Sample", 7 | params: { 8 | samples: { 9 | values: [ 10 | { id: "value1", count: 60 }, 11 | { id: "value2", count: 10 }, 12 | { id: "value3", count: 30 } 13 | ] 14 | } 15 | } 16 | }); 17 | 18 | export default dashboard; -------------------------------------------------------------------------------- /client/src/tests/mocks/dashboards/scorecard.ts: -------------------------------------------------------------------------------- 1 | import samples from './rich-samples'; 2 | import { createDashboard } from "./utils"; 3 | 4 | let dashboard = createDashboard(); 5 | dashboard.dataSources.push({ 6 | id: "samples", 7 | type: "Sample", 8 | params: { 9 | samples: { 10 | scorecard_data_value: 3000000 11 | } 12 | } 13 | }); 14 | dashboard.elements.push({ 15 | id: "scorecard", 16 | type: "Scorecard", 17 | title: "Value", 18 | size: { w: 3,h: 8 }, 19 | source: "samples", 20 | dependencies: { 21 | value: "samples:scorecard_data_value", 22 | color: "::#2196F3", 23 | icon: "::av_timer" 24 | } 25 | }); 26 | 27 | export default dashboard; -------------------------------------------------------------------------------- /client/src/tests/mocks/dashboards/splitpanel.ts: -------------------------------------------------------------------------------- 1 | import { createDashboard } from "./utils"; 2 | 3 | let dashboard = createDashboard(); 4 | dashboard.dataSources.push( 5 | { 6 | id: "samples", 7 | type: "Sample", 8 | params: { 9 | samples: { } 10 | } 11 | } 12 | ); 13 | dashboard.elements.push( 14 | { 15 | id: "splitpanel", 16 | type: "SplitPanel", 17 | title: "Values", 18 | size: { w: 12,h: 16 }, 19 | dependencies: { groups: "samples:groups", values: "samples:values" }, 20 | props: { 21 | group: { field: "title",secondaryField: "subtitle", countField: "count" }, 22 | cols: [ 23 | { header: "Id",field: "id",secondaryHeader: "Repeat Id",secondaryField: "id" }, 24 | { header: "Count",field: "count" }, 25 | ] 26 | }, 27 | actions: { 28 | select: { 29 | action: "samples:updateDependencies", 30 | params: { title: "args:title",type: "args:title" } 31 | } 32 | } 33 | } 34 | ); 35 | 36 | export default dashboard; -------------------------------------------------------------------------------- /client/src/tests/mocks/dashboards/table.ts: -------------------------------------------------------------------------------- 1 | import samples from './samples'; 2 | import { createDashboard } from "./utils"; 3 | 4 | let dashboard = createDashboard(samples); 5 | dashboard.elements.push({ 6 | id: 'table', 7 | type: 'Table', 8 | size: { w: 1, h: 1 }, 9 | title: 'Table', 10 | subtitle: 'Table', 11 | dependencies: { values: 'samples:table-values' }, 12 | props: { 13 | cols: [ 14 | { 15 | header: 'Conversation Id', 16 | field: 'id' 17 | }, 18 | { 19 | header: 'Count', 20 | field: 'count' 21 | } 22 | ] 23 | } 24 | }); 25 | 26 | export default dashboard; -------------------------------------------------------------------------------- /client/src/tests/mocks/dashboards/timeline.ts: -------------------------------------------------------------------------------- 1 | import samples from './rich-samples'; 2 | import { createDashboard } from "./utils"; 3 | 4 | let dashboard = createDashboard(samples); 5 | dashboard.elements.push({ 6 | id: "timeline", 7 | type: "Timeline", 8 | title: "Timeline title", 9 | size: { w: 3,h: 8 }, 10 | source: "data", 11 | props: { showLegend: true,compact: true,entityType: "Messages" } 12 | }); 13 | 14 | export default dashboard; -------------------------------------------------------------------------------- /client/src/tests/mocks/dashboards/timespan.ts: -------------------------------------------------------------------------------- 1 | import { createDashboard } from './utils'; 2 | 3 | let dashboard = createDashboard(); 4 | dashboard.dataSources.push({ 5 | id: 'timespan', 6 | type: 'Constant', 7 | params: { 8 | values: ['24 hours', '1 week', '1 month'], 9 | selectedValue: '1 month' 10 | }, 11 | calculated: (state, dependencies) => { 12 | var queryTimespan = state.selectedValue === '24 hours' ? 'PT24H' : state.selectedValue === '1 week' ? 'P7D' : 'P30D'; 13 | var granularity = state.selectedValue === '24 hours' ? '5m' : state.selectedValue === '1 week' ? '1d' : '1d'; 14 | 15 | return { queryTimespan, granularity }; 16 | } 17 | }); 18 | 19 | export default dashboard; -------------------------------------------------------------------------------- /client/src/tests/mocks/dashboards/utils.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import dashboard from './dashboard'; 3 | 4 | let createDashboard = (fromDashboard?: IDashboardConfig) : IDashboardConfig => _.cloneDeep(fromDashboard || dashboard); 5 | 6 | export { 7 | createDashboard 8 | }; -------------------------------------------------------------------------------- /client/src/tests/mocks/dialog.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | id: "conversations", 3 | width: '60%', 4 | params: [ 'title', 'intent', 'queryspan' ], 5 | dataSources: [ 6 | { 7 | id: 'timespan', 8 | type: 'Constant', 9 | params: { 10 | values: [], 11 | selectedValue: 'default' 12 | }, 13 | calculated: (state, dependencies) => { 14 | return { 15 | someJsonValues: [ 16 | { id: 1, count: 2 }, 17 | { id: 2, count: 0 }, 18 | { id: 3, count: 10 } 19 | ] 20 | }; 21 | } 22 | } 23 | ], 24 | elements: [ 25 | { 26 | id: 'conversations-list', 27 | type: 'Table', 28 | title: 'Conversations', 29 | size: { w: 12, h: 16}, 30 | dependencies: { values: 'timespan:someJsonValues' }, 31 | props: { 32 | cols: [{ 33 | header: 'Conversation Id', 34 | field: 'id' 35 | }, { 36 | header: 'Count', 37 | field: 'count' 38 | }, { 39 | type: 'button', 40 | value: 'chat', 41 | onClick: 'openMessagesDialog' 42 | }] 43 | } 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /client/src/tests/mocks/requests/account.ts: -------------------------------------------------------------------------------- 1 | import * as nock from 'nock'; 2 | 3 | function mockRequests() { 4 | 5 | nock('http://localhost') 6 | .get('/auth/account') 7 | .once() 8 | .reply(404, { 9 | error: 'Some exception' 10 | }); 11 | 12 | nock('http://localhost') 13 | .get('/auth/account') 14 | .twice() 15 | .reply(200, { 16 | account: 'account' 17 | }); 18 | } 19 | export { 20 | mockRequests 21 | }; -------------------------------------------------------------------------------- /client/src/tests/mocks/requests/application-insights/index.ts: -------------------------------------------------------------------------------- 1 | import * as nock from 'nock'; 2 | import dashboardMock from '../../dashboards/application-insights'; 3 | import query24HResponseMock from './query.24h.mock'; 4 | import query30DResponseMock from './query.30d.mock'; 5 | import { appInsightsUri } from '../../../../data-sources/plugins/ApplicationInsights/common'; 6 | import { REPLACE } from 'history/lib/actions'; 7 | 8 | const { appId, apiKey } = dashboardMock.config.connections['application-insights']; 9 | 10 | /** 11 | * Mocking application insights requets 12 | */ 13 | function mockRequests() { 14 | 15 | nock('http://localhost') 16 | .post('/applicationInsights/query', { 17 | query: /(.*?)/g, 18 | appId: appId, 19 | dashboardId: dashboardMock.id, 20 | queryTimespan: 'PT24H' 21 | }) 22 | .delay(10) 23 | .reply(200, query24HResponseMock) 24 | .post('/applicationInsights/query', { 25 | query: /(.*?)/g, 26 | appId: appId, 27 | dashboardId: dashboardMock.id, 28 | queryTimespan: 'P30D' 29 | }) 30 | .delay(10) 31 | .reply(200, query30DResponseMock) 32 | .post('/applicationInsights/query', { 33 | query: /(.*?)/g, 34 | appId: 'id_fail', 35 | dashboardId: dashboardMock.id, 36 | queryTimespan: 'P30D' 37 | }) 38 | .delay(10) 39 | .reply(404, 'Some error'); 40 | } 41 | 42 | export { 43 | mockRequests 44 | }; -------------------------------------------------------------------------------- /client/src/tests/mocks/requests/configuration.ts: -------------------------------------------------------------------------------- 1 | import * as nock from 'nock'; 2 | import dashboard from '../dashboards/dashboard'; 3 | 4 | function mockRequests() { 5 | 6 | nock('http://localhost') 7 | .get('/api/dashboards') 8 | .reply(200, ` 9 | (function (window) { 10 | var dashboardTemplate = (function () { 11 | return ${JSON.stringify(dashboard)}; 12 | })(); 13 | window.dashboardTemplates = window.dashboardTemplates || []; 14 | window.dashboardTemplates.push(dashboardTemplate); 15 | })(window); 16 | (function (window) { 17 | var dashboard = (function () { 18 | return ${JSON.stringify(dashboard)}; 19 | })(); 20 | window.dashboardDefinitions = window.dashboardDefinitions || []; 21 | window.dashboardDefinitions.push(dashboard); 22 | })(window); 23 | `); 24 | 25 | nock('http://localhost') 26 | .get('/api/dashboards/id_fail') 27 | .reply(404, `Some error`); 28 | 29 | nock('http://localhost') 30 | .get('/api/dashboards/id_success') 31 | .reply(200, ` 32 | (function (window) { 33 | var dashboard = (function () { 34 | return ${JSON.stringify(dashboard)}; 35 | })(); 36 | window.dashboard = dashboard || null; 37 | })(window); 38 | `); 39 | 40 | nock('http://localhost') 41 | .get('/api/templates/id_fail') 42 | .reply(404, `Some error`); 43 | 44 | nock('http://localhost') 45 | .get('/api/templates/id_success') 46 | .reply(200, ` 47 | (function (window) { 48 | var template = (function () { 49 | return ${JSON.stringify(dashboard)}; 50 | })(); 51 | window.template = template || null; 52 | })(window); 53 | `); 54 | 55 | nock('http://localhost') 56 | .put('/api/dashboards/id') 57 | .reply(200, { success: true }); 58 | 59 | nock('http://localhost') 60 | .put('/api/templates/id') 61 | .reply(200, { script: true }); 62 | } 63 | export { 64 | mockRequests 65 | }; -------------------------------------------------------------------------------- /client/src/tests/mocks/requests/cosmosdb/index.ts: -------------------------------------------------------------------------------- 1 | import * as nock from 'nock'; 2 | import dashboardMock from '../../dashboards/cosmosdb'; 3 | import { COSMOS_DB_QUERY_URL } from '../../../../data-sources/plugins/CosmosDB/Query'; 4 | 5 | const { host, key } = dashboardMock.config.connections['cosmos-db']; 6 | 7 | /** 8 | * Mocking application insights requets 9 | */ 10 | function mockRequests() { 11 | nock('http://localhost', { 12 | }) 13 | .post('/cosmosdb/query') 14 | .delay(100) 15 | .reply(200, { Documents: 'fakedata' }) 16 | } 17 | export { 18 | mockRequests 19 | }; 20 | -------------------------------------------------------------------------------- /client/src/tests/mocks/requests/directline/index.ts: -------------------------------------------------------------------------------- 1 | import * as nock from 'nock'; 2 | import dashboardMock from '../../dashboards/bot-framework-directline'; 3 | import { DIRECT_LINE_URL } from '../../../../data-sources/plugins/BotFramework/DirectLine'; 4 | 5 | const { directLine } = dashboardMock.config.connections['bot-framework']; 6 | 7 | /** 8 | * Mocking application insights requets 9 | */ 10 | function mockRequests() { 11 | let bearer = 'Bearer ' + directLine; 12 | nock('https://directline.botframework.com/', { 13 | reqheaders: { 14 | "Authorization": bearer 15 | } 16 | }) 17 | .post('/v3/directline/conversations') 18 | .delay(100) 19 | // DirectLine doesn't support timespan based query, so the result is a stub JSON 20 | .reply(200, { values: 'fakedata' }) 21 | } 22 | export { 23 | mockRequests 24 | }; 25 | -------------------------------------------------------------------------------- /client/src/tests/pages/Dashboard.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import * as TestUtils from 'react-dom/test-utils'; 4 | 5 | import { Card } from 'react-md/lib/Cards'; 6 | 7 | import Dashboard from '../../components/Dashboard'; 8 | 9 | import DashboardPage from '../../pages/Dashboard'; 10 | 11 | import { DataSourceConnector, IDataSourceDictionary } from '../../data-sources'; 12 | 13 | import dashboardMock from '../mocks/dashboards/dashboard'; 14 | import ElementConnector from '../../components/ElementConnector'; 15 | 16 | describe('HomePage', () => { 17 | 18 | let dataSources: IDataSourceDictionary = {}; 19 | let dashboard; 20 | 21 | it('Check Dashboard Page is loading', () => { 22 | var a = TestUtils.renderIntoDocument(); 23 | 24 | // This test is just to make sure the component is able to render 25 | }); 26 | 27 | it('Check Dashboard is loading', () => { 28 | var a = TestUtils.renderIntoDocument(); 29 | 30 | // This test is just to make sure the component is able to render 31 | }); 32 | 33 | afterAll(() => { 34 | ReactDOM.unmountComponentAtNode(dashboard); 35 | }) 36 | }); -------------------------------------------------------------------------------- /client/src/tests/pages/Home.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import * as TestUtils from 'react-dom/test-utils'; 4 | 5 | import HomeComponent from '../../components/Home'; 6 | import dashboardMock from '../mocks/dashboards/pie'; 7 | import ElementConnector from '../../components/ElementConnector'; 8 | 9 | describe('HomePage', () => { 10 | 11 | let homepage; 12 | 13 | beforeAll((done) => { 14 | 15 | setTimeout(() => { 16 | 17 | try { 18 | let {id, dependencies, source, actions, props, title, subtitle } = dashboardMock.elements[0]; 19 | let atts = {id, dependencies, source, actions, props, title, subtitle }; 20 | let homeElement = ElementConnector.createGenericElement( 21 | HomeComponent, 22 | 'home', 23 | 0, 24 | source, 25 | dependencies, 26 | actions, 27 | props, 28 | title, 29 | subtitle); 30 | homepage = TestUtils.renderIntoDocument(homeElement); 31 | TestUtils.isElementOfType(homepage, 'div'); 32 | 33 | // Adding a timeout to make sure Flux cycle is complete 34 | setTimeout(done, 100); 35 | 36 | } catch (e) { 37 | return done(e); 38 | } 39 | 40 | }, 100); 41 | }) 42 | 43 | it('Check Home page is loading', (done) => { 44 | // This test is just to make sure the component is able to render 45 | setTimeout(() => done(), 500); 46 | }); 47 | 48 | afterAll(() => { 49 | ReactDOM.unmountComponentAtNode(homepage); 50 | }) 51 | }); -------------------------------------------------------------------------------- /client/src/tests/pages/HomePage.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import * as TestUtils from 'react-dom/test-utils'; 4 | 5 | import Home from '../../pages/Home' 6 | 7 | describe('Home', () => { 8 | let setup; 9 | let setupComponent; 10 | 11 | it('Home is loading', (done) => { 12 | setup = TestUtils.renderIntoDocument(); 13 | TestUtils.isElementOfType(setup, 'div'); 14 | 15 | setTimeout(() => done(), 500); 16 | }); 17 | }); -------------------------------------------------------------------------------- /client/src/tests/pages/NavBar.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import * as TestUtils from 'react-dom/test-utils'; 4 | 5 | import { Card } from 'react-md/lib/Cards'; 6 | 7 | import Navbar from '../../components/Navbar'; 8 | 9 | import { DataSourceConnector, IDataSourceDictionary } from '../../data-sources'; 10 | 11 | import dashboardMock from '../mocks/dashboards/pie'; 12 | import ElementConnector from '../../components/ElementConnector'; 13 | 14 | describe('HomePage', () => { 15 | 16 | let dataSources: IDataSourceDictionary = {}; 17 | let navbar; 18 | 19 | beforeAll((done) => { 20 | DataSourceConnector.createDataSources(dashboardMock, dashboardMock.config.connections); 21 | dataSources = DataSourceConnector.getDataSources(); 22 | 23 | setTimeout(() => { 24 | 25 | try { 26 | let {id, dependencies, source, actions, props, title, subtitle } = dashboardMock.elements[0]; 27 | let atts = {id, dependencies, source, actions, props, title, subtitle }; 28 | let navbarElement = ElementConnector.createGenericElement( 29 | Navbar, 30 | 'autorefresher', 31 | 0, 32 | source, 33 | dependencies, 34 | actions, 35 | props, 36 | title, 37 | subtitle); 38 | navbar = TestUtils.renderIntoDocument(navbarElement); 39 | TestUtils.isElementOfType(navbar, 'div'); 40 | 41 | // Adding a timeout to make sure Flux cycle is complete 42 | setTimeout(done, 100); 43 | 44 | } catch (e) { 45 | return done(e); 46 | } 47 | 48 | }, 100); 49 | }) 50 | 51 | it('Check NavBar is loading', () => { 52 | // This test is just to make sure the component is able to render 53 | }); 54 | 55 | afterAll(() => { 56 | ReactDOM.unmountComponentAtNode(navbar); 57 | }) 58 | }); -------------------------------------------------------------------------------- /client/src/tests/pages/NotFound.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import * as TestUtils from 'react-dom/test-utils'; 4 | 5 | import NotFound from '../../pages/NotFound'; 6 | 7 | describe('NotFound', () => { 8 | let setup; 9 | let setupComponent; 10 | 11 | it('not found is loading', () => { 12 | setup = TestUtils.renderIntoDocument(); 13 | TestUtils.isElementOfType(setup, 'div'); 14 | }); 15 | }); -------------------------------------------------------------------------------- /client/src/tests/pages/Setup.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import * as TestUtils from 'react-dom/test-utils'; 4 | 5 | import Setup from '../../components/Setup'; 6 | import SetupComponent from '../../pages/Setup' 7 | 8 | describe('Setup', () => { 9 | let setup; 10 | let setupComponent; 11 | 12 | beforeEach(() => { 13 | Object.defineProperty(window, "matchMedia", { 14 | value: jest.fn(() => { return { matches: true } }) 15 | }); 16 | }); 17 | 18 | it('check setup is loading', (done) => { 19 | setup = TestUtils.renderIntoDocument(); 20 | TestUtils.isElementOfType(setup, 'div'); 21 | 22 | setTimeout(() => done(), 500); 23 | }); 24 | 25 | it('check setup component is loading', (done) => { 26 | setupComponent = TestUtils.renderIntoDocument(); 27 | TestUtils.isElementOfType(setupComponent, 'div'); 28 | 29 | let switchContainer = TestUtils.scryRenderedDOMComponentsWithClass(setupComponent, "md-switch-track"); 30 | expect(switchContainer.length).toBe(1); 31 | 32 | setTimeout(() => done(), 500); 33 | }); 34 | }); -------------------------------------------------------------------------------- /client/src/tests/stores/Account.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import * as TestUtils from 'react-dom/test-utils'; 4 | 5 | import { Toast } from '../../components/Toast'; 6 | import AccountStore from '../../stores/AccountStore'; 7 | import AccountActions from '../../actions/AccountActions'; 8 | import { mockRequests } from '../mocks/requests/account'; 9 | 10 | describe('Data Source: Samples', () => { 11 | 12 | let element; 13 | 14 | beforeAll(() => { 15 | element = TestUtils.renderIntoDocument(); 16 | mockRequests(); 17 | }); 18 | 19 | it ('AccountActions failure', done => { 20 | 21 | let component = TestUtils.scryRenderedComponentsWithType(element, Toast); 22 | expect(component[0].state.toasts.length).toBe(0); 23 | AccountActions.updateAccount(); 24 | 25 | setTimeout( 26 | () => { 27 | try { 28 | component = TestUtils.scryRenderedComponentsWithType(element, Toast); 29 | expect(component[0].state.toasts.length).toBe(1); 30 | done(); 31 | } catch (e) { 32 | done.fail(e); 33 | } 34 | }, 35 | 10); 36 | }); 37 | 38 | it ('Testing AccountActions', done => { 39 | 40 | const stateUpdate = (state) => { 41 | expect(state).toHaveProperty('account'); 42 | AccountStore.unlisten(stateUpdate); 43 | done(); 44 | }; 45 | AccountStore.listen(stateUpdate); 46 | AccountActions.updateAccount(); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /client/src/tests/stores/Connection.test.ts: -------------------------------------------------------------------------------- 1 | import ConnectionsStore from "../../stores/ConnectionsStore"; 2 | import ConnectionsActions from "../../actions/ConnectionsActions"; 3 | //import { mockRequests } from '../mocks/requests/account'; 4 | 5 | describe('Data Source: Samples', () => { 6 | 7 | beforeAll(() => { 8 | // mockRequests(); 9 | }) 10 | 11 | it ('Testing SetupActions', (done) => { 12 | 13 | ConnectionsStore.listen((state) => { 14 | expect(state).toHaveProperty('connections'); 15 | done(); 16 | }); 17 | ConnectionsActions.updateConnection('Conn name', {}); 18 | }); 19 | }) 20 | -------------------------------------------------------------------------------- /client/src/tests/stores/Settings.test.ts: -------------------------------------------------------------------------------- 1 | import SettingsStore from "../../stores/SettingsStore"; 2 | import SettingsActions from "../../actions/SettingsActions"; 3 | 4 | describe('Data Source: Samples', () => { 5 | 6 | this.timeout = 10000; 7 | 8 | beforeAll(() => { 9 | }); 10 | 11 | 12 | it ('Testing SetupActions - failure message', done => { 13 | 14 | //fff(Store, test, callback) 15 | 16 | let checkState = state => { 17 | expect(state).toHaveProperty('isSavingSettings'); 18 | expect(state.isSavingSettings).toEqual(false); 19 | 20 | SettingsStore.unlisten(checkState); 21 | done(); 22 | }; 23 | SettingsStore.listen(checkState); 24 | 25 | (SettingsActions.saveSettingsCompleted as any).defer(); 26 | }); 27 | 28 | 29 | it ('Testing SetupActions', done => { 30 | 31 | let checkState = state => { 32 | expect(state).toHaveProperty('isSavingSettings'); 33 | expect(state.isSavingSettings).toEqual(true); 34 | 35 | SettingsStore.unlisten(checkState); 36 | done(); 37 | }; 38 | SettingsStore.listen(checkState); 39 | 40 | (SettingsActions.saveSettings as any).defer(); 41 | }); 42 | }) 43 | -------------------------------------------------------------------------------- /client/src/tests/stores/Setup.test.ts: -------------------------------------------------------------------------------- 1 | import SetupStore from "../../stores/SetupStore"; 2 | import SetupActions from "../../actions/SetupActions"; 3 | 4 | describe('Data Source: Samples', () => { 5 | 6 | this.timeout = 10000; 7 | beforeAll(() => { 8 | }) 9 | 10 | it('Testing SetupActions', (done) => { 11 | 12 | let checkState = (state) => { 13 | expect(state).toMatchSnapshot('SetupActionsLoad') 14 | 15 | SetupStore.unlisten(checkState); 16 | done(); 17 | }; 18 | 19 | SetupStore.listen(checkState); 20 | (SetupActions.load as any).defer(); 21 | }); 22 | }) 23 | -------------------------------------------------------------------------------- /client/src/tests/stores/__snapshots__/Setup.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`SetupActionsLoad 1`] = ` 4 | Store { 5 | "admins": Array [], 6 | "allowHttp": false, 7 | "clientID": "", 8 | "clientSecret": "", 9 | "enableAuthentication": false, 10 | "issuer": "", 11 | "loaded": true, 12 | "redirectUrl": "", 13 | "saveSuccess": true, 14 | "stage": "", 15 | } 16 | `; 17 | -------------------------------------------------------------------------------- /client/src/tests/utils/__snapshots__/utils.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`sample dashboard 1`] = ` 4 | "{ 5 | id: \\"id\\", 6 | url: \\"url\\", 7 | icon: \\"icon\\", 8 | name: \\"name\\", 9 | config: { 10 | connections: { }, 11 | layout: { cols: { lg: 12,md: 10,sm: 6,xs: 4,xxs: 2 },breakpoints: { lg: 1200,md: 996,sm: 768,xs: 480,xxs: 0 } } 12 | }, 13 | dataSources: [ 14 | { 15 | id: \\"samples\\", 16 | type: \\"Sample\\", 17 | params: { 18 | samples: { values: [{ id: \\"value1\\",count: 60 },{ id: \\"value2\\",count: 10 },{ id: \\"value3\\",count: 30 }] } 19 | } 20 | } 21 | ], 22 | filters: [], 23 | elements: [], 24 | dialogs: [] 25 | }" 26 | `; 27 | -------------------------------------------------------------------------------- /client/src/tests/utils/setup.ts: -------------------------------------------------------------------------------- 1 | import ConfigurationsActions from '../../actions/ConfigurationsActions'; 2 | import ConfigurationsStore from '../../stores/ConfigurationsStore'; 3 | import { DataSourceConnector, IDataSourceDictionary } from '../../data-sources'; 4 | 5 | function setupTests( 6 | dashboardMock: IDashboardConfig, 7 | setDataSources: (ds: IDataSourceDictionary) => void, 8 | done: () => void): void { 9 | 10 | // Waiting for all defered functions to complete their execution 11 | ConfigurationsStore.listen(() => { 12 | let dataSources = DataSourceConnector.getDataSources(); 13 | setDataSources(dataSources); 14 | return done(); 15 | }); 16 | 17 | ConfigurationsActions.loadDashboardComplete(dashboardMock); 18 | } 19 | 20 | export { 21 | setupTests 22 | }; -------------------------------------------------------------------------------- /client/src/tests/utils/utils.test.ts: -------------------------------------------------------------------------------- 1 | import utils from '../../utils'; 2 | import dashboardSample from '../mocks/dashboards/samples'; 3 | import dashboardAI from '../mocks/dashboards/application-insights'; 4 | import dashboardAIForked from '../mocks/dashboards/application-insights-forked'; 5 | 6 | describe('Utils', () => { 7 | 8 | it ('KM Number', () => { 9 | 10 | expect(utils.kmNumber(0)).toBe("0"); 11 | expect(utils.kmNumber(10)).toBe("10"); 12 | expect(utils.kmNumber(0.1)).toBe("0.1"); 13 | expect(utils.kmNumber(0.999)).toBe("1.0"); 14 | expect(utils.kmNumber(10, '%')).toBe("10%"); 15 | expect(utils.kmNumber(100)).toBe("100"); 16 | expect(utils.kmNumber(1000)).toBe("1.0K"); 17 | expect(utils.kmNumber(10000)).toBe("10.0K"); 18 | expect(utils.kmNumber(100000)).toBe("100.0K"); 19 | expect(utils.kmNumber(1000000)).toBe("1.0M"); 20 | 21 | }); 22 | 23 | it ('Ago', () => { 24 | let time = new Date(); 25 | time.setSeconds(time.getSeconds() - 10); 26 | expect(utils.ago(time)).toBe('a few seconds ago'); 27 | 28 | time = new Date(); 29 | time.setMinutes(time.getMinutes() - 10); 30 | expect(utils.ago(time)).toBe('10 minutes ago'); 31 | 32 | time = new Date(); 33 | time.setDate(time.getDate() - 1); 34 | expect(utils.ago(time)).toBe('a day ago'); 35 | 36 | time = new Date(); 37 | time.setDate(time.getDate() - 10); 38 | expect(utils.ago(time)).toBe('10 days ago'); 39 | 40 | time = new Date(); 41 | time.setMonth(time.getMonth() - 10); 42 | expect(utils.ago(time)).toBe('10 months ago'); 43 | }); 44 | 45 | it ('string <==> object', () => { 46 | 47 | let s_dashboardSample = utils.convertDashboardToString(dashboardSample); 48 | let s_dashboardAI = utils.convertDashboardToString(dashboardAI); 49 | let s_dashboardAIForked = utils.convertDashboardToString(dashboardAIForked); 50 | 51 | expect(s_dashboardSample).toMatchSnapshot('sample dashboard'); 52 | //expect(s_dashboardAI).toMatchSnapshot('ai dashboard'); 53 | //expect(s_dashboardAIForked).toMatchSnapshot('ai forked dashboard'); 54 | 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /client/src/tests/utils/utils.test.tsx: -------------------------------------------------------------------------------- 1 | import utils from '../../utils'; 2 | import dashboardWithDialog from '../mocks/dashboards/dialogs'; 3 | 4 | describe('Toast', () => { 5 | 6 | let element; 7 | 8 | it('test utility methods', () => { 9 | var dashboardStr = utils.objectToString(dashboardWithDialog); 10 | expect(dashboardStr.length).toBeGreaterThanOrEqual(1); 11 | }); 12 | 13 | }); -------------------------------------------------------------------------------- /client/src/utils/data-formats/common.ts: -------------------------------------------------------------------------------- 1 | import { ToastActions } from '../../components/Toast'; 2 | import { IDataSourcePlugin } from '../../data-sources/plugins/DataSourcePlugin'; 3 | 4 | export enum DataFormatTypes { 5 | none, 6 | timespan, 7 | flags, 8 | retention, 9 | timeline 10 | } 11 | 12 | export interface IDataFormat { 13 | type: string; 14 | args: any; 15 | } 16 | 17 | export function formatWarn(text: string, format: string, plugin: IDataSourcePlugin) { 18 | ToastActions.addToast({ text: `[format:${format}] text [data source:${plugin._props.id}]` }); 19 | } 20 | 21 | export function getPrefix(format: string | IDataFormat) { 22 | return (format && typeof format !== 'string' && format.args && format.args.prefix) || ''; 23 | } -------------------------------------------------------------------------------- /client/src/utils/data-formats/formats/filter.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import utils from '../../index'; 3 | import { DataFormatTypes, IDataFormat, formatWarn, getPrefix } from '../common'; 4 | import { IDataSourcePlugin } from '../../../data-sources/plugins/DataSourcePlugin'; 5 | 6 | /** 7 | * Formats a result to fit a filter. 8 | * 9 | * Receives a list of filtering values: 10 | * values: [ 11 | * { field: 'value 1' }, 12 | * { field: 'value 2' }, 13 | * { field: 'value 3' }, 14 | * ] 15 | * 16 | * And outputs the result in a consumable filter way: 17 | * result: { 18 | * "prefix-filters": [ 'value 1', 'value 2', 'value 3' ], 19 | * "prefix-selected": [ ], 20 | * } 21 | * 22 | * "prefix-selected" will be able to hold the selected values from the filter component 23 | * 24 | * @param format { 25 | * type: 'filter', 26 | * args: { 27 | * prefix: string - a prefix string for the exported variables (default to id). 28 | * data: string - the state property holding the data (default is 'values'). 29 | * field: string - the field holding the filter values in the results (default = "value") 30 | * } 31 | * } 32 | * @param state Current received state from data source 33 | * @param dependencies Dependencies for the plugin 34 | * @param plugin The entire plugin (for id generation, params etc...) 35 | * @param prevState The previous state to compare for changing filters 36 | */ 37 | export function filter ( 38 | format: string | IDataFormat, 39 | state: any, 40 | dependencies: IDictionary, 41 | plugin: IDataSourcePlugin, 42 | prevState: any) { 43 | 44 | const args = typeof format !== 'string' ? format.args : {}; 45 | const prefix = getPrefix(format); 46 | const field = args.field || 'value'; 47 | const unknown = args.unknown || 'unknown'; 48 | 49 | const values = state[args.data || 'values']; 50 | if (!values) { return null; } 51 | 52 | // This code is meant to fix the following scenario: 53 | // When "Timespan" filter changes, to "channels-selected" variable 54 | // is going to be reset into an empty set. 55 | // For this reason, using previous state to copy filter 56 | const filters = values.map(x => x[field] || unknown); 57 | let result = {}; 58 | result[prefix + 'values-all'] = filters; 59 | 60 | if (prevState[prefix + 'values-selected'] === undefined) { 61 | result[prefix + 'values-selected'] = []; 62 | } 63 | 64 | return result; 65 | } -------------------------------------------------------------------------------- /client/src/utils/data-formats/formats/flags.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import utils from '../../index'; 3 | import { DataFormatTypes, IDataFormat, formatWarn, getPrefix } from '../common'; 4 | import { IDataSourcePlugin } from '../../../data-sources/plugins/DataSourcePlugin'; 5 | 6 | /** 7 | * Turns a list of values into a list of flags 8 | * 9 | * Receives a list of filtering values (on the data source params variable): 10 | * params: { 11 | * values: [ 'value1', 'value2', 'value3' ] 12 | * } 13 | * 14 | * And outputs the result in a consumable filter way: 15 | * result: { 16 | * "value1": false, (will be true if prefix-selected contains "value1") 17 | * "value2": false, 18 | * "value3": false, 19 | * "prefix-filters": [ 'value 1', 'value 2', 'value 3' ], 20 | * "prefix-selected": [ ], 21 | * } 22 | * 23 | * "prefix-selected" will be able to hold the selected values from the filter component 24 | * 25 | * @param format 'filter' | { 26 | * type: 'filter', 27 | * args: { 28 | * prefix: string - a prefix string for the exported variables (default to id). 29 | * data: string - the state property holding the data (default is 'values'). 30 | * } 31 | * } 32 | * @param state Current received state from data source 33 | * @param dependencies Dependencies for the plugin 34 | * @param plugin The entire plugin (for id generation, params etc...) 35 | * @param prevState The previous state to compare for changing filters 36 | */ 37 | export function flags( 38 | format: string | IDataFormat, 39 | state: any, 40 | dependencies: IDictionary, 41 | plugin: IDataSourcePlugin, 42 | prevState: any) { 43 | 44 | const prefix = getPrefix(format); 45 | const args = typeof format !== 'string' && format.args || {}; 46 | const params = plugin.getParams(); 47 | 48 | if (!params || !Array.isArray(params.values)) { 49 | return formatWarn('A paramerter "values" is expected as an array on "params" in the data source', 'filter', plugin); 50 | } 51 | 52 | if (!state) { return null; } 53 | 54 | let values = params[args.data || 'values']; 55 | let flagsobj = {}; 56 | values.forEach(key => { flagsobj[key] = state.selectedValue === key; }); 57 | 58 | flagsobj[prefix + 'values-all'] = values; 59 | flagsobj[prefix + 'values-selected'] = state.selectedValue || []; 60 | 61 | return flagsobj; 62 | } -------------------------------------------------------------------------------- /client/src/utils/data-formats/formats/pie.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import utils from '../../index'; 3 | import { DataFormatTypes, IDataFormat, formatWarn, getPrefix } from '../common'; 4 | import { IDataSourcePlugin } from '../../../data-sources/plugins/DataSourcePlugin'; 5 | 6 | /** 7 | * Formats a result to suite a pie chart 8 | * 9 | * Receives a list of filtering values: 10 | * values: [ 11 | * { count: 10, field: 'piece 1' }, 12 | * { count: 15, field: 'piece 2' }, 13 | * { count: 44, field: 'piece 3' }, 14 | * ] 15 | * 16 | * And outputs the result in a consumable filter way: 17 | * result: { 18 | * "prefix-pieData": [ 19 | * { name: 'bar 1', value: 10}, 20 | * { name: 'bar 2', value: 15}, 21 | * { name: 'bar 3', value: 20}, 22 | * ], 23 | * } 24 | * 25 | * "prefix-selected" will be able to hold the selected values from the filter component 26 | * 27 | * @param format { 28 | * type: 'pie', 29 | * args: { 30 | * value: string - The field name holding the value the pie piece 31 | * data: string - the state property holding the data (default is 'values'). 32 | * label: string - The field name holding the series name (aggregation in a specific field) 33 | * maxLength: number - At what length to cut string values (default: 13), 34 | * } 35 | * } 36 | * @param state Current received state from data source 37 | * @param plugin The entire plugin (for id generation, params etc...) 38 | * @param prevState The previous state to compare for changing filters 39 | */ 40 | export function pie( 41 | format: string | IDataFormat, 42 | state: any, 43 | plugin: IDataSourcePlugin, 44 | prevState: any) { 45 | 46 | if (typeof format === 'string') { 47 | return formatWarn('format should be an object with args', 'timeline', plugin); 48 | } 49 | 50 | const args = format.args || {}; 51 | let values: any[] = state[args.data || 'values'] || []; 52 | const prefix = getPrefix(format); 53 | 54 | const labelField = args.label || 'name'; 55 | const valueField = args.value || 'value'; 56 | let maxLength = args.maxLength && parseInt(args.maxLength, 10) || 13; 57 | let maxLengthCut = Math.max(0, maxLength - 3); 58 | 59 | let result = {}; 60 | result[prefix + 'pieData'] = values.map(value => ({ 61 | name: maxLength && value[labelField].length > maxLength 62 | ? value[labelField].substr(0, maxLengthCut) + '...' 63 | : value[labelField], 64 | value: value[valueField] 65 | })); 66 | 67 | return result; 68 | } -------------------------------------------------------------------------------- /client/src/utils/data-formats/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | export * from './formats/bars'; 3 | export * from './formats/filter'; 4 | export * from './formats/flags'; 5 | export * from './formats/pie'; 6 | export * from './formats/retention'; 7 | export * from './formats/scorecard'; 8 | export * from './formats/timeline'; 9 | export * from './formats/timespan'; 10 | export * from './formats/filtered-samples'; -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build/dist", 4 | "module": "commonjs", 5 | "target": "es5", 6 | "lib": ["es6", "dom"], 7 | "sourceMap": true, 8 | "allowJs": true, 9 | "jsx": "react", 10 | "moduleResolution": "node", 11 | "rootDir": "src", 12 | "forceConsistentCasingInFileNames": true, 13 | "noImplicitReturns": true, 14 | "noImplicitThis": false, 15 | "noImplicitAny": false, 16 | "strictNullChecks": false, 17 | "suppressImplicitAnyIndexErrors": true, 18 | "noUnusedLocals": false 19 | }, 20 | "exclude": [ 21 | "node_modules", 22 | "src/setupTests.ts" 23 | ], 24 | "types": [ 25 | "typePatches" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /client/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build/dist", 4 | "module": "commonjs", 5 | "target": "es5", 6 | "lib": ["es6", "dom"], 7 | "sourceMap": true, 8 | "allowJs": true, 9 | "jsx": "react", 10 | "moduleResolution": "node", 11 | "rootDir": "src", 12 | "forceConsistentCasingInFileNames": true, 13 | "noImplicitReturns": true, 14 | "noImplicitThis": false, 15 | "noImplicitAny": false, 16 | "strictNullChecks": false, 17 | "suppressImplicitAnyIndexErrors": true, 18 | "noUnusedLocals": false 19 | }, 20 | "exclude": [ 21 | "node_modules", 22 | "src/setupTests.ts" 23 | ], 24 | "types": [ 25 | "typePatches" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Ibex Dashboard Development Guide 2 | 3 | ## Framework 4 | This project is built using [create-react-app](https://github.com/facebookincubator/create-react-app). 5 | The server side appraoch was addopted [through this link](https://www.fullstackreact.com/articles/using-create-react-app-with-a-server/). 6 | 7 | * [Creating a new Dashboard Template](dashboard-creation.md) 8 | 9 | # Orchestrators 10 | 11 | ## DataSourceConnector 12 | [DataSourceConnector](../client/src/data-sources/DataSourceConnector.ts) is a class that created and initializes the various data sources. 13 | 14 | [ElementConnector](../client/src/components/ElementConnector.tsx) is a class the creates and initializes Visual component for the dashboard and consecutive dialogs. 15 | 16 | ## Plugins 17 | Many of the aspects in this project are extendible. The following are possibilities to donate your own plugins. 18 | 19 | ## Connection Plugins 20 | 21 | Connection plugins are connected to Data Source plugins. A Data Source can have a connection plugin which will provide all the instances of the Data Source with a single connection to receive credentials information from. 22 | 23 | ## Data Source Plugins 24 | 25 | [How to create a Data Source Plugin](add-new-data-source.md) 26 | 27 | * Constant 28 | * Sample 29 | * Application Insights 30 | * [CosmosDB](data-sources/cosmos-db.md) 31 | * [Bot Framework](data-sources/bot-framework.md) 32 | * GraphQL 33 | * Azure 34 | 35 | ## Elements Plugins 36 | 37 | [How to create a Visual Plugin](add-new-element.md) 38 | 39 | * [Area Chart](components/area.md) 40 | * [Bar Chart](components/bar.md) 41 | * [Detail View](components/detail.md) 42 | * [Pie Chart](components/pie.md) 43 | * [Request Button](components/requestbutton.md) 44 | * [Scatter Chart](components/scatter.md) 45 | * [Scorecard](components/scorecard.md) 46 | * [Split View Panel](components/splitpanel.md) 47 | * [Table](components/table.md) 48 | * [Timeline Chart](components/timeline.md) 49 | 50 | ## data-formats plugins 51 | 52 | [A short excerpt on data-formats](data-formats) 53 | 54 | # Additional Features 55 | 56 | * [Two Modes Elements](two-modes-element.md) 57 | * [Filter Plugins](filter.md) 58 | * [Dialogs](dialog.md) 59 | * [Actions](actions.md) -------------------------------------------------------------------------------- /docs/bot-framedash-msgs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevstar/Dashborad/84d34bee2227de8742a0990deaf0e8a1941ce619/docs/bot-framedash-msgs.png -------------------------------------------------------------------------------- /docs/bot-framedash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevstar/Dashborad/84d34bee2227de8742a0990deaf0e8a1941ce619/docs/bot-framedash.png -------------------------------------------------------------------------------- /docs/components/area.md: -------------------------------------------------------------------------------- 1 | # Area 2 | 3 | This article explains how define an Area element. This element is composed of an [AreaChart](http://recharts.org/#/en-US/api/AreaChart) component. 4 | 5 | ## Basic properties 6 | 7 | | Property | Type | Value | Description 8 | | :--------|:-----|:------|:------------ 9 | | `id`| `string` || ID of the element on the page 10 | | `type`| `string` | "Area" | 11 | | `title`| `string` || Title that will appear at the top of the view 12 | | `subtitle`| `string` || Description of the chart (displayed as tooltip) 13 | | `size`| `{ w: number, h: number}` || Width/Height of the view 14 | | `dependencies`| `object` || Dependencies that are required for this element 15 | | `props`| `object` || Additional properties to define for this element 16 | 17 | ## Dependencies 18 | 19 | Define `dependencies` as follows: 20 | 21 | | Property | Type | Description 22 | | :--------|:-----|:----------- 23 | | `values`| `string` | Reference to data source values 24 | | `lines`| `string` | Reference to data source lines 25 | | `timeFormat`| `string` | Reference to data source timeline 26 | 27 | #### Dependencies sample 28 | 29 | ```js 30 | dependencies: { 31 | values: "ai:timeline-graphData", 32 | lines: "ai:timeline-channels", 33 | timeFormat: "ai:timeline-timeFormat" 34 | } 35 | ``` 36 | 37 | ## Props 38 | 39 | Define `props` as follows: 40 | 41 | | Property | Type | Description 42 | | :--------|:-----|:----------- 43 | | `isStacked`| `boolean` | Display as stacked area 44 | | `showLegend`| `boolean` | Display legend 45 | | `areaProps`| `object` | [AreaChart](http://recharts.org/#/en-US/api/AreaChart) properties 46 | 47 | #### Props sample 48 | 49 | ```js 50 | props: { 51 | isStacked: true, 52 | showLegend: false 53 | } 54 | ``` 55 | 56 | #### AreaChart properties 57 | - Tip: `areaProps` can be used to specify additional properties of the [AreaChart](http://recharts.org/#/en-US/api/AreaChart) chart component such as `syncId` to link related charts. Refer to the [AreaChart API](http://recharts.org/#/en-US/api/AreaChart) for more info. 58 | 59 | ```js 60 | props: { 61 | isStacked: true, 62 | showLegend: false 63 | areaProps: { 64 | syncId: "sharedId" 65 | } 66 | } 67 | ``` -------------------------------------------------------------------------------- /docs/components/bar.md: -------------------------------------------------------------------------------- 1 | # BarData 2 | 3 | This article explains how define a BarData element. This element is composed of a [BarChart](http://recharts.org/#/en-US/api/BarChart) component. 4 | 5 | ## Basic properties 6 | 7 | | Property | Type | Value | Description 8 | | :--------|:-----|:------|:------------ 9 | | `id`| `string` || ID of the element on the page 10 | | `type`| `string` | "BarData" | 11 | | `title`| `string` || Title that will appear at the top of the view 12 | | `subtitle`| `string` || Description of the chart (displayed as tooltip) 13 | | `size`| `{ w: number, h: number}` || Width/Height of the view 14 | | `dependencies`| `object` || Dependencies that are required for this element 15 | | `props`| `object` || Additional properties to define for this element 16 | | `actions`| `object` || Actions to trigger when bar is clicked 17 | 18 | ## Dependencies 19 | 20 | Define `dependencies` as follows: 21 | 22 | | Property | Type | Description 23 | | :--------|:-----|:----------- 24 | | `values`| `string` | Reference to data source values 25 | | `bars`| `string` | Reference to data source bars 26 | 27 | #### Dependencies sample 28 | 29 | ```js 30 | dependencies: { 31 | values: "ai:intents", 32 | bars: "ai:intents-bars", 33 | } 34 | ``` 35 | 36 | ## Props 37 | 38 | Define `props` as follows: 39 | 40 | | Property | Type | Description 41 | | :--------|:-----|:----------- 42 | | `nameKey`| `string` | Data key to use for x-axis 43 | | `barProps`| `object` | [BarChart](http://recharts.org/#/en-US/api/BarChart) properties 44 | 45 | #### Props sample 46 | 47 | ```js 48 | props: { 49 | nameKey: "intent" 50 | } 51 | ``` 52 | 53 | #### BarChart properties 54 | - Tip: `barProps` can be used to specify additional properties of the [BarChart](http://recharts.org/#/en-US/api/BarChart). Refer to the [BarChart API](http://recharts.org/#/en-US/api/BarChart) for more info. 55 | 56 | ## Actions 57 | 58 | Define an `onBarClick` action as follows: 59 | 60 | | Property | Type | Description 61 | | :--------|:-----|:----------- 62 | | `action`| `string` | Reference to dialog id 63 | | `params`| `object` | Arguments or properties that need to be passed to run the dialog's query 64 | 65 | #### Actions sample 66 | 67 | ```js 68 | actions: { 69 | onBarClick: { 70 | action: "dialog:conversations", 71 | params: { 72 | title: "args:intent", 73 | intent: "args:intent", 74 | queryspan: "timespan:queryTimespan" 75 | } 76 | } 77 | } 78 | ``` 79 | 80 | -------------------------------------------------------------------------------- /docs/components/detail.md: -------------------------------------------------------------------------------- 1 | # Detail 2 | 3 | The detail view can be used to display a JSON result with HTML formatting to make it easier to inspect nested information. 4 | 5 | ## Basic properties 6 | 7 | | Property | Type | Value | Description 8 | | :--------|:-----|:------|:------------ 9 | | `id`| `string` || ID of the element on the page 10 | | `type`| `string` | "Detail" | 11 | | `title`| `string` || Title that will appear at the top of the view 12 | | `size`| `{ w: number, h: number}` || Width/Height of the view 13 | | `dependencies`| `object` || Dependencies that will be requested for this element 14 | | `props`| `object` || Additional properties to define for this element 15 | 16 | ## Dependencies 17 | 18 | Define `values` as follows: 19 | 20 | | Property | Type | Description 21 | | :--------|:-----|:----------- 22 | | `values`| `string` | Reference to data source's id 23 | 24 | #### Dependencies sample: 25 | 26 | ```js 27 | dependencies: { 28 | values: "errordetail-data" 29 | }, 30 | ``` 31 | 32 | ## Props 33 | 34 | Define `props` as follows: 35 | 36 | | Property | Type | Description 37 | | :--------|:-----|:----------- 38 | | `cols`| `object[]` | Collection of table column properties 39 | 40 | Define `props.cols` as follows: 41 | 42 | | Property | Type | Description 43 | | :--------|:-----|:----------- 44 | | `header`| `string` | Column header 45 | | `field`| `string` | Defines the query field 46 | 47 | #### Props sample: 48 | 49 | ```js 50 | props: { 51 | cols: [{ 52 | header: "Handle", 53 | field: "handledAt" 54 | },{ 55 | header: "Type", 56 | field: "type" 57 | },{ 58 | header: "Message", 59 | field: "innermostMessage" 60 | },{ 61 | header: "Conversation ID", 62 | field: "conversationId" 63 | },{ 64 | header: "Operation ID", 65 | field: "operation_Id" 66 | },{ 67 | header: "Timestamp", 68 | field: "timestamp" 69 | },{ 70 | header: "Details", 71 | field: "details" 72 | }] 73 | } 74 | ``` -------------------------------------------------------------------------------- /docs/components/pie.md: -------------------------------------------------------------------------------- 1 | # PieData 2 | 3 | This article explains how define an PieData element. This element is composed of a [PieChart](http://recharts.org/#/en-US/api/PieChart) component. 4 | 5 | ## Basic properties 6 | 7 | | Property | Type | Value | Description 8 | | :--------|:-----|:------|:------------ 9 | | `id`| `string` || ID of the element on the page 10 | | `type`| `string` | "PieData" | 11 | | `title`| `string` || Title that will appear at the top of the view 12 | | `subtitle`| `string` || Description of the chart (displayed as tooltip) 13 | | `size`| `{ w: number, h: number}` || Width/Height of the view 14 | | `dependencies`| `object` || Dependencies that are required for this element 15 | | `props`| `object` || Additional properties to define for this element 16 | 17 | ## Dependencies 18 | 19 | Define `dependencies` as follows: 20 | 21 | | Property | Type | Description 22 | | :--------|:-----|:----------- 23 | | `values`| `string` | Reference to data source values 24 | 25 | #### Dependencies sample 26 | 27 | ```js 28 | dependencies: { 29 | values: "ai:timeline-channelUsage", 30 | } 31 | ``` 32 | 33 | ## Props 34 | 35 | Define `props` as follows: 36 | 37 | | Property | Type | Description 38 | | :--------|:-----|:----------- 39 | | `showLegend`| `boolean` | Display legend 40 | | `compact`| `boolean` | Display as compact chart 41 | | `pieProps`| `object` | [PieChart](http://recharts.org/#/en-US/api/PieChart) properties 42 | 43 | #### Props sample 44 | 45 | ```js 46 | props: { 47 | showLegend: false, 48 | compact: true 49 | } 50 | ``` 51 | 52 | #### PieChart properties 53 | - Tip: `pieProps` can be used to specify additional properties of the [PieChart](http://recharts.org/#/en-US/api/PieChart). Refer to the [PieChart API](http://recharts.org/#/en-US/api/PieChart) for more info. -------------------------------------------------------------------------------- /docs/components/scatter.md: -------------------------------------------------------------------------------- 1 | # Scatter 2 | 3 | This article explains how define a Scatter element. This element is composed of an [ScatterChart](http://recharts.org/#/en-US/api/ScatterChart) component. 4 | 5 | ## Basic properties 6 | 7 | | Property | Type | Value | Description 8 | | :--------|:-----|:------|:------------ 9 | | `id`| `string` || ID of the element on the page 10 | | `type`| `string` | "Scatter" | 11 | | `title`| `string` || Title that will appear at the top of the view 12 | | `subtitle`| `string` || Description of the chart (displayed as tooltip) 13 | | `size`| `{ w: number, h: number}` || Width/Height of the view 14 | | `dependencies`| `object` || Dependencies that are required for this element 15 | | `props`| `object` || Additional properties to define for this element 16 | 17 | ## Dependencies 18 | 19 | Define `dependencies` as follows: 20 | 21 | | Property | Type | Description 22 | | :--------|:-----|:----------- 23 | | `groupedValues`| `string` | Reference to data source grouped values. Each group is a Scatter line. 24 | 25 | #### Dependencies sample 26 | 27 | ```js 28 | dependencies: { 29 | groupedValues: "ai:channelActivity-groupedValues" 30 | } 31 | ``` 32 | 33 | ## Props 34 | 35 | Define `props` as follows: 36 | 37 | | Property | Type | Description 38 | | :--------|:-----|:----------- 39 | | `xDataKey`| `string` | x-axis data key 40 | | `yDataKey`| `string` | y-axis data key 41 | | `zDataKey`| `string` | z-axis data key 42 | | `zRange`| `number[]` | Range of the z-axis used for scale of scatter points. 43 | | `scatterProps`| `object` | [ScatterChart](http://recharts.org/#/en-US/api/ScatterChart) properties 44 | 45 | ```js 46 | props: { 47 | xDataKey: "hourOfDay", 48 | yDataKey: "duration", 49 | zDataKey: "count", 50 | zRange: [10,500] 51 | } 52 | ``` 53 | 54 | #### ScatterChart properties 55 | - Tip: `scatterProps` can be used to specify additional properties of the [ScatterChart](http://recharts.org/#/en-US/api/ScatterChart). Refer to the [ScatterChart API](http://recharts.org/#/en-US/api/ScatterChart) for more info. -------------------------------------------------------------------------------- /docs/components/timeline.md: -------------------------------------------------------------------------------- 1 | # Timeline 2 | 3 | This article explains how define a Timeline element. This element is composed of an [LineChart](http://recharts.org/#/en-US/api/LineChart) component. 4 | 5 | ## Basic properties 6 | 7 | | Property | Type | Value | Description 8 | | :--------|:-----|:------|:------------ 9 | | `id`| `string` || ID of the element on the page 10 | | `type`| `string` | "Timeline" | 11 | | `title`| `string` || Title that will appear at the top of the view 12 | | `subtitle`| `string` || Description of the chart (displayed as tooltip) 13 | | `size`| `{ w: number, h: number}` || Width/Height of the view 14 | | `dependencies`| `object` || Dependencies that are required for this element 15 | | `props`| `object` || Additional properties to define for this element 16 | 17 | ## Dependencies 18 | 19 | Define `dependencies` as follows: 20 | 21 | | Property | Type | Description 22 | | :--------|:-----|:----------- 23 | | `values`| `string` | Reference to data source values 24 | | `lines`| `string` | Reference to data source lines 25 | | `timeFormat`| `string` | Reference to data source timeline 26 | 27 | #### Dependencies sample 28 | 29 | ```js 30 | dependencies: { 31 | values: "ai:timeline-graphData", 32 | lines: "ai:timeline-channels", 33 | timeFormat: "ai:timeline-timeFormat" 34 | } 35 | ``` 36 | 37 | ## Props 38 | 39 | Define `props` as follows: 40 | 41 | | Property | Type | Description 42 | | :--------|:-----|:----------- 43 | | `lineProps`| `object` | [LineChart](http://recharts.org/#/en-US/api/LineChart) properties 44 | 45 | #### LineChart properties 46 | - Tip: `lineProps` can be used to specify additional properties of the [LineChart](http://recharts.org/#/en-US/api/LineChart) chart component such as `syncId` to link related charts. Refer to the [LineChart API](http://recharts.org/#/en-US/api/LineChart) for more info. 47 | 48 | ```js 49 | props: { 50 | lineProps: { 51 | syncId: "sharedId" 52 | } 53 | } 54 | ``` -------------------------------------------------------------------------------- /docs/dashboard-creation.md: -------------------------------------------------------------------------------- 1 | # Dashboard Creation 2 | Creating a dashboard relies upon the following assumptions: 3 | 4 | ## Listing Available Templates 5 | The list of available template will appear in the home page and will enable the user to choose a template to create from. 6 | The following fields will appear in the list: 7 | 8 | - `preview` (as an image) 9 | - `name` 10 | - `description` 11 | 12 | # Creation Screen 13 | When clicking on one of the template a dialog will be open with the following fields: 14 | 15 | - `id`: Will be used as an id for the dashboard and in the url 16 | - `name`: Will be used in the title of the dashboard 17 | - `icon`: Will be used in the navigation bar -------------------------------------------------------------------------------- /docs/data-formats/README.md: -------------------------------------------------------------------------------- 1 | # Data Formats 2 | Data formats are data transformers that align the data received from a data source to the format required by a visual component. 3 | 4 | The available data formats are: 5 | 6 | * [bars](../../client/src/utils/data-formats/formats/bars.ts) 7 | * [filter](../../client/src/utils/data-formats/formats/filter.ts) 8 | * [filtered-samples](../../client/src/utils/data-formats/formats/filtered-samples.ts) 9 | * [flags](../../client/src/utils/data-formats/formats/flags.ts) 10 | * [retention](../../client/src/utils/data-formats/formats/retention.ts) 11 | * [scorecard](../../client/src/utils/data-formats/formats/scorecard.ts) 12 | * [timeline](../../client/src/utils/data-formats/formats/timeline.ts) 13 | * [timespan](../../client/src/utils/data-formats/formats/timespan.ts) 14 | 15 | To create a new Visual Element with a correlating data-format [click here](../add-new-element.md). -------------------------------------------------------------------------------- /docs/images/bot-fmk-dashboard-intent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevstar/Dashborad/84d34bee2227de8742a0990deaf0e8a1941ce619/docs/images/bot-fmk-dashboard-intent.png -------------------------------------------------------------------------------- /docs/images/bot-fmk-dashboard-msgs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevstar/Dashborad/84d34bee2227de8742a0990deaf0e8a1941ce619/docs/images/bot-fmk-dashboard-msgs.png -------------------------------------------------------------------------------- /docs/images/bot-fmk-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevstar/Dashborad/84d34bee2227de8742a0990deaf0e8a1941ce619/docs/images/bot-fmk-dashboard.png -------------------------------------------------------------------------------- /docs/requestbutton.md: -------------------------------------------------------------------------------- 1 | # Request Button 2 | 3 | A button that opens a url in new window, or sends a request. 4 | 5 | ## Basic properties 6 | 7 | | Property | Type | Value | Description 8 | | :--------|:-----|:------|:------------ 9 | | `id`| `string` || ID of the element on the page 10 | | `type`| `string` | "RequestButton" | 11 | | `title`| `string` || Button title 12 | | `size`| `{ w: number, h: number}` || Width/Height of the view 13 | | `location`| `{ x: number, y: number}` || Grid position of the view 14 | | `dependencies`| `object` || Dependencies that will be requested for this element 15 | | `props`| `object` || Additional properties to define for this element 16 | 17 | ## Dependencies 18 | 19 | Define `dependencies` as follows: 20 | 21 | | Property | Type | Value | Description 22 | | :--------|:-----|:------|:------------ 23 | | `body`| `object` | `''` | Request body as JSON object 24 | | `headers`| `object` | `{}` | Request headers in `{ key : value }` dictionary format 25 | | `disabled`| `boolean` | `false` | Button state 26 | 27 | ## Props 28 | 29 | Define `props` as follows: 30 | 31 | | Property | Type | Description 32 | | :--------|:-----|:----------- 33 | | `url`| `string | function` | Static url string, or a function with string injection 34 | | `link`| `boolean` | Opens url in new window if set to `true` 35 | | `method`| `string` | Send request using 'GET', 'POST', 'PUT', or 'DELETE'. Default is 'GET' 36 | | `disableAfterFirstClick`| `boolean` | Only allows button to be clicked once if set to `true` 37 | | `icon`| `string` | [Material design icon name](https://material.io/icons/) (use underscores instead of spaces) 38 | | `buttonProps`| `string` | For additional [react-md Button props](https://react-md.mlaursen.com/components/buttons?tab=1) 39 | 40 | NB. A Request Button can act as a link or as an xhr request, but not both. 41 | 42 | #### Request Button sample 43 | 44 | ```js 45 | { 46 | id: "agent-button", 47 | type: "RequestButton", 48 | title: "Open Webchat", 49 | size: { w: 2, h: 1 }, 50 | location: { x: 2, y: 0 }, 51 | dependencies: { token: "directLine:token", host: "::localhost:3978/webchat" }, 52 | props: { 53 | url: ({token, host}) => `http://${host}/?s=${token}`, 54 | link: true, 55 | icon: "open_in_new", 56 | buttonProps: { 57 | iconBefore: false, 58 | secondary: true 59 | } 60 | } 61 | } 62 | ``` -------------------------------------------------------------------------------- /docs/two-modes-element.md: -------------------------------------------------------------------------------- 1 | # Two Modes Element 2 | This article describes how to create two modes for an element in the dashboard 3 | 4 | ## Data Source 5 | First you'll need to create a data source that exposes a set boolean variable: 6 | 7 | ```js 8 | { 9 | id: "modes", 10 | type: "Constant", 11 | params: { 12 | values: ["messages","users"], 13 | selectedValue: "messages" 14 | }, 15 | calculated: (state, dependencies) => { 16 | let flags = {}; 17 | flags['messages'] = (state.selectedValue === 'messages'); 18 | flags['users'] = (state.selectedValue !== 'messages'); 19 | return flags; 20 | } 21 | } 22 | ``` 23 | 24 | Second, you'll need to create a filter to control that constant: 25 | 26 | ```js 27 | { 28 | type: "TextFilter", 29 | dependencies: { 30 | selectedValue: "modes", 31 | values: "modes:values" 32 | }, 33 | actions: { 34 | onChange: "modes:updateSelectedValue" 35 | } 36 | } 37 | ``` 38 | 39 | And last, you need to create two instances of the same element (with the same id), where each appears only when their flag is on: 40 | 41 | ```js 42 | elements: [ 43 | { 44 | id: "timeline", 45 | type: "Timeline", 46 | title: "Message Rate", 47 | subtitle: "How many messages were sent per timeframe", 48 | size: { w: 5, h: 8 }, 49 | dependencies: { 50 | visible: "modes:messages", 51 | values: "ai:timeline-graphData", 52 | lines: "ai:timeline-channels", 53 | timeFormat: "ai:timeline-timeFormat" 54 | } 55 | }, 56 | { 57 | id: "timeline", 58 | type: "Timeline", 59 | title: "Users Rate", 60 | subtitle: "How many users were sent per timeframe", 61 | size: { w: 5, h: 8 }, 62 | dependencies: { 63 | visible: "modes:users", 64 | values: "ai:timeline-users-graphData", 65 | lines: "ai:timeline-users-channels", 66 | timeFormat: "ai:timeline-users-timeFormat" 67 | } 68 | } 69 | }] 70 | ``` 71 | 72 | Notice that each instance has a `visible` property defined under dependencies and a different set of properties and `dependencies`. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ibex-dashboard", 3 | "version": "1.3.0", 4 | "private": true, 5 | "dependencies": { 6 | "npm-run-all": "^4.0.2" 7 | }, 8 | "scripts": { 9 | "build": "cd client && yarn build", 10 | "start": "node server", 11 | "start:client": "cd client && yarn start", 12 | "start:server": "cd server && yarn start", 13 | "start:dev": "npm-run-all -p start:client start:server", 14 | "postinstall": "cd client && yarn && cd ../server && yarn" 15 | }, 16 | "license": "MIT" 17 | } 18 | -------------------------------------------------------------------------------- /scripts/deployment/webapponlinux/azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "appName": { 6 | "defaultValue": "ibex-dashboard-linux", 7 | "type": "string" 8 | } 9 | }, 10 | "variables": { 11 | "servicePlanName": "[concat('serviceplan',uniqueString(resourceGroup().id))]" 12 | }, 13 | "resources": [ 14 | { 15 | "type": "Microsoft.Web/serverfarms", 16 | "name": "[variables('servicePlanName')]", 17 | "apiVersion": "2016-03-01", 18 | "location": "[resourceGroup().location]", 19 | "sku": { 20 | "name": "S1", 21 | "capacity": 1 22 | }, 23 | "kind": "linux", 24 | "properties": { 25 | "workerSizeId": 0, 26 | "reserved": true, 27 | "hostingEnvironment": "" 28 | } 29 | }, 30 | { 31 | "type": "Microsoft.Web/sites", 32 | "name": "[parameters('appName')]", 33 | "apiVersion": "2016-03-01", 34 | "location": "[resourceGroup().location]", 35 | "properties": { 36 | "name": "[parameters('appName')]", 37 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]" 38 | }, 39 | "dependsOn": [ 40 | "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]" 41 | ], 42 | "resources": [ 43 | { 44 | "name": "appsettings", 45 | "type": "config", 46 | "apiVersion": "2016-03-01", 47 | "dependsOn": [ 48 | "[resourceId('Microsoft.Web/sites', parameters('appName'))]" 49 | ], 50 | "tags": { 51 | "displayName": "appSettings" 52 | }, 53 | "properties": { 54 | "DOCKER_CUSTOM_IMAGE_NAME": "azure/ibex-dashboard" 55 | } 56 | } 57 | ] 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | // server/app.js 2 | const express = require('express'); 3 | const expressSession = require('express-session'); 4 | const cookieParser = require('cookie-parser'); 5 | const morgan = require('morgan'); 6 | const path = require('path'); 7 | const fs = require('fs'); 8 | const bodyParser = require('body-parser'); 9 | const authRouter = require('./routes/auth'); 10 | const apiRouter = require('./routes/api'); 11 | const graphQLRouter = require('./routes/graphql'); 12 | const cosmosDBRouter = require('./routes/cosmos-db'); 13 | const azureRouter = require('./routes/azure'); 14 | const applicationInsightsRouter = require('./routes/applicationInsights'); 15 | 16 | const app = express(); 17 | 18 | 19 | app.use((req, res, next) => { 20 | console.log(`Request URL: ${req.url}`); 21 | return next(); 22 | }); 23 | 24 | app.use(cookieParser()); 25 | app.use(expressSession({ secret: 'keyboard cat', resave: true, saveUninitialized: false })); 26 | app.use(bodyParser.json()); 27 | app.use(bodyParser.urlencoded({ extended: true })); 28 | 29 | // Setup logger 30 | app.use(morgan(':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] :response-time ms')); 31 | 32 | app.use(authRouter.authenticationMiddleware('/auth', '/api/setup')); 33 | app.use('/auth', authRouter.router); 34 | app.use('/api', apiRouter.router); 35 | app.use('/cosmosdb', cosmosDBRouter.router); 36 | app.use('/azure', azureRouter.router); 37 | app.use('/graphql', graphQLRouter.router); 38 | app.use('/applicationinsights', applicationInsightsRouter.router); 39 | 40 | app.use(express.static(path.resolve(__dirname, '..', 'build'))); 41 | 42 | // Always return the main index.html, so react-router render the route in the client 43 | app.get('*', (req, res) => { 44 | res.sendFile(path.resolve(__dirname, '..', 'build', 'index.html')); 45 | }); 46 | 47 | module.exports = app; -------------------------------------------------------------------------------- /server/common/paths.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const resourcesPaths = () => ({ 4 | persistentFolder: path.join(__dirname, '..', 'dashboards', 'persistent'), 5 | privateDashboard: path.join(__dirname, '..', 'dashboards','persistent','private'), 6 | preconfDashboard: path.join(__dirname, '..', 'dashboards', 'preconfigured'), 7 | privateTemplate: path.join(__dirname, '..', 'dashboards','persistent', 'customTemplates') 8 | }); 9 | 10 | module.exports = { 11 | resourcesPaths 12 | } -------------------------------------------------------------------------------- /server/config/auth.basic.json: -------------------------------------------------------------------------------- 1 | { 2 | "identityMetadata": "https://login.microsoftonline.com/common/.well-known/openid-configuration", 3 | "validateIssuer": true, 4 | "skipUserProfile": false, 5 | "responseType": "id_token code", 6 | "responseMode": "form_post" 7 | } 8 | -------------------------------------------------------------------------------- /server/config/setup.initial.json: -------------------------------------------------------------------------------- 1 | { 2 | "admins": [], 3 | "stage": "none", 4 | "allowHttp": false, 5 | "enableAuthentication": false, 6 | "redirectUrl": "", 7 | "clientID": "", 8 | "clientSecret": "" 9 | } -------------------------------------------------------------------------------- /server/dashboards/preconfigured/application-insights-sample.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import * as _ from 'lodash'; 3 | 4 | // The following line is important to keep in that format so it can be rendered into the page 5 | export const config: IDashboardConfig = /*return*/ { 6 | id: "app_insights_sample", 7 | name: "Application Insights Sample", 8 | icon: "dashboard", 9 | url: "app_insights_sample", 10 | description: "A basic Application Insights dashboard", 11 | preview: "/images/sample.png", 12 | category: 'Samples', 13 | html: ` 14 |
15 | This is a basic Application Insights dashboard.
16 | This dashboard is built to view Requests that are sent to Application Insights. 17 |
18 | `, 19 | config: { 20 | connections: {}, 21 | layout: { 22 | isDraggable: true, 23 | isResizable: true, 24 | rowHeight: 30, 25 | verticalCompact: false, 26 | cols: { lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }, 27 | breakpoints: { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 } 28 | } 29 | }, 30 | dataSources: [ 31 | { 32 | id: "timespan", 33 | type: "Constant", 34 | params: { values: ["24 hours", "1 week", "1 month"], selectedValue: "24 hours" }, 35 | format: "timespan" 36 | }, 37 | { 38 | id: "requests", 39 | type: "ApplicationInsights/Query", 40 | dependencies: { timespan: "timespan", queryTimespan: "timespan:queryTimespan", granularity: "timespan:granularity" }, 41 | params: { 42 | table: "requests", 43 | queries: { 44 | count: { 45 | query: ({ granularity }) => { 46 | return ` 47 | summarize count= count() by bin(timestamp, ${granularity}) | 48 | order by timestamp asc 49 | ` 50 | }, 51 | format: { type: "timeline", args: { timeField: "timestamp", valueField: "count" } } 52 | }, 53 | } 54 | } 55 | } 56 | ], 57 | filters: [ 58 | { 59 | type: "TextFilter", 60 | title: "Timespan", 61 | source: "timespan", 62 | actions: { onChange: "timespan:updateSelectedValue" }, 63 | first: true 64 | }, 65 | ], 66 | elements: [ 67 | { 68 | id: "serverResponseTime", 69 | type: "Timeline", 70 | title: "Server requests count", 71 | subtitle: "The server requests count in the selected time range", 72 | size: { w: 5, h: 8 }, 73 | source: "requests:count", 74 | }, 75 | ], 76 | dialogs: [ 77 | ] 78 | } -------------------------------------------------------------------------------- /server/helpers/resourceFieldProvider.js: -------------------------------------------------------------------------------- 1 | // Template/Dashboard metadata is stored inside the files 2 | // To read the metadata we could eval() the files, but that's dangerous. 3 | // Instead, we use regular expressions to pull them out 4 | // Assumes that: 5 | // * the metadata comes before config information 6 | // * fields in the file look like fieldname: "string" or fieldname: 'string' 7 | // * or, special case, html: `string` 8 | 9 | const fields = { 10 | id: /\s*id:\s*("|')(.*)("|')/, 11 | name: /\s*name:\s*("|')(.*)("|')/, 12 | description: /\s*description:\s*("|')(.*)("|')/, 13 | icon: /\s*icon:\s*("|')(.*)("|')/, 14 | logo: /\s*logo:\s*("|')(.*)("|')/, 15 | url: /\s*url:\s*("|')(.*)("|')/, 16 | preview: /\s*preview:\s*("|')(.*)("|')/, 17 | category: /\s*category:\s*("|')(.*)("|')/, 18 | html: /\s*html:\s*(`)([\s\S]*?)(`)/gm, 19 | featured: /\s*featured:\s*(true|false)/, 20 | connections: /\s*connections:( )([\S\s]*)(,)\s*layout:/ 21 | } 22 | 23 | const MASK_STRING = '***********'; 24 | const MASKING_REQUIREMENTS = [ 25 | 'connections|application-insights|apiKey' 26 | ]; 27 | 28 | const getField = (fieldName, text) => { 29 | regExp = fields[fieldName]; 30 | regExp.lastIndex = 0; 31 | 32 | const matches = regExp.exec(text); 33 | let match = matches && matches.length >= 3 && matches[2]; 34 | if (!match) { 35 | match = matches && matches.length > 0 && matches[0]; 36 | } 37 | return match; 38 | } 39 | 40 | const getMetadata = (text) => { 41 | const metadata = {} 42 | for (key in fields) { 43 | metadata[key] = getField(key, text); 44 | } 45 | return metadata; 46 | } 47 | 48 | module.exports = { 49 | getField, 50 | getMetadata, 51 | MASK_STRING, 52 | MASKING_REQUIREMENTS 53 | } -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | // server/index.js 2 | 'use strict'; 3 | 4 | const app = require('./app'); 5 | 6 | const PORT = process.env.PORT || 4000; 7 | 8 | app.listen(PORT, () => { 9 | console.log(`App listening on port ${PORT}!`); 10 | }); -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ibex-dashboard-server", 3 | "version": "0.1.0", 4 | "private": true, 5 | "proxy": "http://localhost:4000/", 6 | "dependencies": { 7 | "@types/lodash": "^4.14.67", 8 | "body-parser": "^1.17.1", 9 | "cookie-parser": "^1.4.3", 10 | "express": "^4.15.2", 11 | "express-session": "^1.15.2", 12 | "lodash": "^4.17.4", 13 | "morgan": "^1.8.1", 14 | "ms-rest-azure": "^2.1.2", 15 | "passport": "^0.3.2", 16 | "passport-azure-ad": "^3.0.5", 17 | "xhr-request": "^1.0.1" 18 | }, 19 | "scripts": { 20 | "start": "node index.js" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /server/routes/azure.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const express = require('express'); 4 | 5 | const msRestAzure = require('ms-rest-azure'); 6 | const AzureServiceClient = msRestAzure.AzureServiceClient; 7 | 8 | const router = new express.Router(); 9 | 10 | router.post('/query', (req, res) => { 11 | 12 | let { servicePrincipalId, servicePrincipalKey, servicePrincipalDomain, subscriptionId, options } = req.body || { }; 13 | 14 | // Interactive Login 15 | msRestAzure.loginWithServicePrincipalSecret(servicePrincipalId, servicePrincipalKey, servicePrincipalDomain, function(err, credentials) { 16 | 17 | if (err) { return this.failure(err); } 18 | 19 | let client = new AzureServiceClient(credentials, null); 20 | 21 | options.method = options.method || 'GET'; 22 | options.url = `https://management.azure.com` + options.url; 23 | 24 | return client.sendRequest(options, (err, result) => { 25 | if (err) { throw err; } 26 | 27 | let values = result.value || []; 28 | return res.json(values); 29 | }); 30 | }); 31 | 32 | }); 33 | 34 | module.exports = { 35 | router 36 | } -------------------------------------------------------------------------------- /server/routes/cosmos-db.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const request = require('xhr-request'); 3 | const router = new express.Router(); 4 | const crypto = require('crypto'); 5 | 6 | router.post('/query', (req, res) => { 7 | const { host, key, verb, resourceType, databaseId, collectionId, query, parameters } = req.body; 8 | 9 | if ( !host || !key || !verb || !resourceType || !databaseId || !collectionId || !query ) { 10 | console.log('Invalid request parameters'); 11 | return res.send({ error: 'Invalid request parameters' }); 12 | } 13 | 14 | const date = new Date().toUTCString(); 15 | const resourceLink = `dbs/${databaseId}/colls/${collectionId}`; 16 | const auth = getAuthorizationTokenUsingMasterKey(verb, resourceType, resourceLink, date, key); 17 | 18 | let cosmosQuery = { 19 | query: query, 20 | parameters: parameters || [], 21 | }; 22 | 23 | const url = `https://${host}.documents.azure.com/${resourceLink}/docs`; 24 | 25 | request(url, { 26 | method: 'POST', 27 | headers: { 28 | 'Content-Type': 'application/query+json', 29 | 'Accept': 'application/json', 30 | 'Authorization': auth, 31 | 'x-ms-date': date, 32 | 'x-ms-version': '2015-04-08', 33 | 'x-ms-documentdb-isquery': true, 34 | }, 35 | body: cosmosQuery, 36 | responseType: 'json', 37 | json: true, 38 | }, (err, doc) => { 39 | if (err) { 40 | console.log(err); 41 | return res.send({ error: err }); 42 | } 43 | res.send(doc); 44 | }); 45 | 46 | }); 47 | 48 | function getAuthorizationTokenUsingMasterKey(verb, resourceType, resourceLink, date, masterKey) { 49 | var key = new Buffer(masterKey, "base64"); 50 | var text = (verb || "").toLowerCase() + "\n" + 51 | (resourceType || "").toLowerCase() + "\n" + 52 | (resourceLink || "") + "\n" + 53 | date.toLowerCase() + "\n" + 54 | "" + "\n"; 55 | var body = new Buffer(text, "utf8"); 56 | var signature = crypto.createHmac("sha256", key).update(body).digest("base64"); 57 | var MasterToken = "master"; 58 | var TokenVersion = "1.0"; 59 | return encodeURIComponent("type=" + MasterToken + "&ver=" + TokenVersion + "&sig=" + signature); 60 | } 61 | 62 | module.exports = { 63 | router 64 | } -------------------------------------------------------------------------------- /server/routes/graphql.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const request = require('xhr-request'); 3 | 4 | const router = new express.Router(); 5 | 6 | router.post('/query', (req, res) => { 7 | const { serviceUrl, query, variables } = req.body || { }; 8 | 9 | const requestOptions = { 10 | method: 'POST', 11 | json: true, 12 | body: { 13 | query: query, 14 | variables: variables 15 | }, 16 | }; 17 | 18 | request(serviceUrl, requestOptions, function(err, data) { 19 | if (err) { throw err; } 20 | return res.json(data); 21 | }); 22 | }); 23 | 24 | module.exports = { 25 | router 26 | } --------------------------------------------------------------------------------