├── .deployment ├── .editorconfig ├── .gitignore ├── .travis.yml ├── .travis ├── build.sh ├── ci.sh ├── coverage.sh └── setup.sh ├── .vscode ├── launch.json └── settings.json ├── Dockerfile ├── README.md ├── build ├── asset-manifest.json ├── favicon.ico ├── images │ ├── apollo-gql-preview.png │ ├── azure.png │ ├── bot-ai-base.png │ ├── bot-ai-cs.png │ ├── bot-framework-preview.png │ ├── default.png │ ├── human-to-bot-handoff.png │ └── sample.png ├── index.html └── static │ ├── css │ ├── main.43e63d1e.css │ └── main.43e63d1e.css.map │ └── js │ ├── main.f204f21f.js │ └── main.f204f21f.js.map ├── client ├── @types │ ├── ai.d.ts │ └── types.d.ts ├── build │ ├── asset-manifest.json │ ├── favicon.ico │ ├── images │ │ ├── apollo-gql-preview.png │ │ └── bot-framework-preview.png │ ├── index.html │ └── static │ │ ├── css │ │ └── main.43e63d1e.css.map │ │ └── js │ │ ├── main.f204f21f.js │ │ └── main.f204f21f.js.map ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── images │ │ ├── apollo-gql-preview.png │ │ └── bot-framework-preview.png │ └── index.html ├── src │ ├── actions │ │ ├── AccountActions.ts │ │ ├── ConfigurationsActions.ts │ │ ├── ConnectionsActions.ts │ │ ├── FilterActions.ts │ │ ├── SettingsActions.ts │ │ ├── SetupActions.ts │ │ └── VisibilityActions.ts │ ├── alt.ts │ ├── apollo │ │ └── components │ │ │ ├── DropDownQueryRenderer.tsx │ │ │ ├── LineChartQueryRenderer.tsx │ │ │ ├── QueryRenderer.tsx │ │ │ ├── SimpleBarChartQueryRenderer.tsx │ │ │ └── StraightAnglePieChartQueryRenderer.tsx │ ├── components │ │ ├── App.tsx │ │ ├── Card.tsx │ │ ├── Dashboard │ │ │ ├── DownloadFile.tsx │ │ │ ├── Editor │ │ │ │ ├── Editor.tsx │ │ │ │ ├── EditorActions.ts │ │ │ │ ├── EditorStore.ts │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── ElementConnector.tsx │ │ ├── ElementConnectorForGQL.tsx │ │ ├── Home │ │ │ └── index.tsx │ │ ├── Navbar │ │ │ ├── NavigationLink.tsx │ │ │ ├── index.tsx │ │ │ └── style.scss │ │ ├── Settings │ │ │ ├── ConnectionsSettings │ │ │ │ └── index.tsx │ │ │ ├── DatasourceSettings │ │ │ │ └── index.tsx │ │ │ ├── ElementsSettings │ │ │ │ ├── ElementSettingsFactory.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── readme.md │ │ │ ├── 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.tsx │ │ ├── TooltipFontIcon.tsx │ │ ├── colors │ │ │ └── index.ts │ │ ├── common │ │ │ ├── ArrayInput.tsx │ │ │ ├── BaseDatasourceSettings.tsx │ │ │ ├── BaseSettings.tsx │ │ │ ├── Dependency.tsx │ │ │ ├── InfoDrawer.tsx │ │ │ └── TokenInput.tsx │ │ └── generic │ │ │ ├── Area │ │ │ ├── Settings.tsx │ │ │ └── index.tsx │ │ │ ├── BarData │ │ │ ├── Settings.tsx │ │ │ └── index.tsx │ │ │ ├── CheckboxFilter.tsx │ │ │ ├── Detail │ │ │ ├── Detail.tsx │ │ │ └── index.ts │ │ │ ├── Dialogs │ │ │ ├── Dialog.tsx │ │ │ ├── DialogsActions.ts │ │ │ ├── DialogsStore.ts │ │ │ └── index.tsx │ │ │ ├── GenericComponent.tsx │ │ │ ├── MapData.tsx │ │ │ ├── MenuFilter.tsx │ │ │ ├── PieData │ │ │ ├── Settings.tsx │ │ │ └── index.tsx │ │ │ ├── RadarChartCard.tsx │ │ │ ├── RadialBarChartCard.tsx │ │ │ ├── RequestButton.tsx │ │ │ ├── Scatter │ │ │ ├── Settings.tsx │ │ │ └── index.tsx │ │ │ ├── Scorecard │ │ │ ├── Settings.tsx │ │ │ └── index.tsx │ │ │ ├── SimpleRadialBarChartCard.tsx │ │ │ ├── SplitPanel │ │ │ ├── SplitPanel.tsx │ │ │ └── index.ts │ │ │ ├── Table │ │ │ ├── Table.tsx │ │ │ └── index.ts │ │ │ ├── TextFilter.tsx │ │ │ ├── Timeline │ │ │ ├── Settings.tsx │ │ │ └── index.tsx │ │ │ ├── generic.scss │ │ │ └── plugins.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 │ │ ├── FilterStore.ts │ │ ├── SettingsStore.ts │ │ ├── SetupStore.ts │ │ └── VisibilityStore.ts │ ├── tests │ │ ├── data-sources │ │ │ ├── Constants.test.ts │ │ │ ├── Samples.test.ts │ │ │ └── plugins │ │ │ │ └── Constant │ │ │ │ └── Settings.test.tsx │ │ ├── elements │ │ │ ├── Dialog.test.tsx │ │ │ ├── Spinner.test.tsx │ │ │ ├── SplitPanel.test.tsx │ │ │ ├── Table.test.tsx │ │ │ └── Toast.test.tsx │ │ ├── mocks │ │ │ ├── dashboards │ │ │ │ ├── application-insights.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── dashboard.ts │ │ │ │ ├── dialogs.ts │ │ │ │ ├── samples.ts │ │ │ │ ├── splitpanel.ts │ │ │ │ ├── table.ts │ │ │ │ ├── timespan.ts │ │ │ │ └── utils.ts │ │ │ ├── dialog.ts │ │ │ └── requests │ │ │ │ ├── account.ts │ │ │ │ ├── application-insights │ │ │ │ ├── query.24h.mock.ts │ │ │ │ └── query.30d.mock.ts │ │ │ │ └── configuration.ts │ │ ├── stores │ │ │ ├── Account.test.ts │ │ │ └── Configuration.test.ts │ │ └── utils │ │ │ ├── rewire-module.tsx │ │ │ └── setup.ts │ └── utils │ │ ├── graphqlResultsUtils.ts │ │ └── index.ts ├── tsconfig.json ├── tslint.json └── yarn.lock ├── docs ├── area.md ├── bar.md ├── bot-framedash-msgs.png ├── bot-framedash.png ├── bot-framework-apollo-gql-preview.png ├── bot-framework.md ├── cosmos-db.md ├── dashboard-creation.md ├── detail.md ├── dialog.md ├── filter.md ├── pie-settings.PNG ├── pie.md ├── requestbutton.md ├── scatter.md ├── scorecard.md ├── splitpanel.md ├── table.md ├── template-for-sample.PNG ├── timeline.md └── two-modes-element.md ├── package-lock.json ├── package.json ├── scripts └── deployment │ ├── azure-deploy.cmd │ ├── dashboard.template.json │ ├── webapp │ └── azuredeploy.json │ └── webapponlinux │ └── azuredeploy.json ├── server ├── apollo │ ├── appInsightsConvertors │ │ └── ai-to-graph-ds.js │ ├── resolvers │ │ ├── index.js │ │ └── queries │ │ │ ├── ai.js │ │ │ ├── localFile.js │ │ │ └── resolversRedirect.js │ ├── schema.js │ └── typeDefs.js ├── app.js ├── config │ ├── auth.basic.json │ └── setup.initial.json ├── dashboards │ ├── backup │ │ └── graph-types.ts │ ├── preconfigured │ │ ├── MBF.Advanced.Analytics.ts │ │ ├── MBF.Advanced.Health.ts │ │ ├── apollo_graphql.ts │ │ ├── apollo_graphql_sample_data.ts │ │ ├── azure.ts │ │ ├── bot-framework-inst.ts │ │ ├── bot-framework.ts │ │ ├── cosmosdb-handoff.ts │ │ ├── qna.dashboard.ts │ │ └── sample.ts │ └── sample.basic.private.js ├── index.js ├── package-lock.json ├── package.json ├── routes │ ├── api.js │ ├── application-insights.js │ ├── auth.js │ ├── azure.js │ ├── cosmos-db.js │ └── graphql.js ├── sampleData │ └── sampleDB.json └── yarn.lock ├── src ├── components │ ├── Navbar │ │ └── style.css │ └── generic │ │ └── generic.css └── index.css └── yarn.lock /.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 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # testing 7 | /coverage 8 | 9 | # misc 10 | .DS_Store 11 | .env 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # generated files 17 | client/**/*.css 18 | 19 | # generated merge temp files 20 | *.orig 21 | 22 | # private files 23 | *.private.* 24 | !*sample.basic.private.js -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: 2 | node_js 3 | 4 | node_js: 5 | - "7" 6 | 7 | sudo: 8 | required 9 | 10 | before_install: 11 | - curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash - 12 | - sudo apt-key adv --keyserver pgp.mit.edu --recv D101F7899D41F3C3 13 | - echo "deb http://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list 14 | - sudo apt-get update -qq 15 | - sudo apt-get install -y -qq yarn 16 | 17 | cache: 18 | directories: 19 | - $HOME/.yarn-cache 20 | 21 | install: 22 | - .travis/setup.sh 23 | 24 | script: 25 | - .travis/ci.sh 26 | - .travis/coverage.sh 27 | 28 | after_success: 29 | - .travis/build.sh -------------------------------------------------------------------------------- /.travis/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | pushd client 4 | yarn build 5 | rm -rf ../build 6 | mv build .. 7 | popd -------------------------------------------------------------------------------- /.travis/ci.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | (cd client; CI=true yarn lint) 4 | (cd client; CI=true yarn test) -------------------------------------------------------------------------------- /.travis/coverage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | min_coverage="${MIN_COVERAGE:-38}" 4 | line_coverage="$((cd client; 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/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | (cd server; yarn install) 4 | (cd client; yarn install; 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:6.11-alpine 2 | 3 | # Create app directory 4 | RUN mkdir -p /usr/src/app 5 | WORKDIR /usr/src/app 6 | 7 | # Install app dependencies 8 | COPY package.json /usr/src/app/ 9 | RUN npm install yarn -g 10 | RUN npm install 11 | 12 | # Bundle app source 13 | COPY . /usr/src/app 14 | 15 | CMD [ "npm", "start" ] 16 | -------------------------------------------------------------------------------- /build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "main.css": "static/css/main.43e63d1e.css", 3 | "main.css.map": "static/css/main.43e63d1e.css.map", 4 | "main.js": "static/js/main.f204f21f.js", 5 | "main.js.map": "static/js/main.f204f21f.js.map" 6 | } -------------------------------------------------------------------------------- /build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/ibex-dashboard-apollo-graphql/60e0b2174a54d376aea6541aeebb931f397172af/build/favicon.ico -------------------------------------------------------------------------------- /build/images/apollo-gql-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/ibex-dashboard-apollo-graphql/60e0b2174a54d376aea6541aeebb931f397172af/build/images/apollo-gql-preview.png -------------------------------------------------------------------------------- /build/images/azure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/ibex-dashboard-apollo-graphql/60e0b2174a54d376aea6541aeebb931f397172af/build/images/azure.png -------------------------------------------------------------------------------- /build/images/bot-ai-base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/ibex-dashboard-apollo-graphql/60e0b2174a54d376aea6541aeebb931f397172af/build/images/bot-ai-base.png -------------------------------------------------------------------------------- /build/images/bot-ai-cs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/ibex-dashboard-apollo-graphql/60e0b2174a54d376aea6541aeebb931f397172af/build/images/bot-ai-cs.png -------------------------------------------------------------------------------- /build/images/bot-framework-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/ibex-dashboard-apollo-graphql/60e0b2174a54d376aea6541aeebb931f397172af/build/images/bot-framework-preview.png -------------------------------------------------------------------------------- /build/images/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/ibex-dashboard-apollo-graphql/60e0b2174a54d376aea6541aeebb931f397172af/build/images/default.png -------------------------------------------------------------------------------- /build/images/human-to-bot-handoff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/ibex-dashboard-apollo-graphql/60e0b2174a54d376aea6541aeebb931f397172af/build/images/human-to-bot-handoff.png -------------------------------------------------------------------------------- /build/images/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/ibex-dashboard-apollo-graphql/60e0b2174a54d376aea6541aeebb931f397172af/build/images/sample.png -------------------------------------------------------------------------------- /build/index.html: -------------------------------------------------------------------------------- 1 | React App
-------------------------------------------------------------------------------- /build/static/css/main.43e63d1e.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":[],"names":[],"mappings":"","file":"static/css/main.43e63d1e.css","sourceRoot":""} -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /client/build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "main.css": "static/css/main.43e63d1e.css", 3 | "main.css.map": "static/css/main.43e63d1e.css.map", 4 | "main.js": "static/js/main.f204f21f.js", 5 | "main.js.map": "static/js/main.f204f21f.js.map" 6 | } -------------------------------------------------------------------------------- /client/build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/ibex-dashboard-apollo-graphql/60e0b2174a54d376aea6541aeebb931f397172af/client/build/favicon.ico -------------------------------------------------------------------------------- /client/build/images/apollo-gql-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/ibex-dashboard-apollo-graphql/60e0b2174a54d376aea6541aeebb931f397172af/client/build/images/apollo-gql-preview.png -------------------------------------------------------------------------------- /client/build/images/bot-framework-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/ibex-dashboard-apollo-graphql/60e0b2174a54d376aea6541aeebb931f397172af/client/build/images/bot-framework-preview.png -------------------------------------------------------------------------------- /client/build/index.html: -------------------------------------------------------------------------------- 1 | React App
-------------------------------------------------------------------------------- /client/build/static/css/main.43e63d1e.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":[],"names":[],"mappings":"","file":"static/css/main.43e63d1e.css","sourceRoot":""} -------------------------------------------------------------------------------- /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.0.3", 19 | "leaflet-geosearch": "^2.2.0", 20 | "leaflet.markercluster": "^1.0.5", 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.6.1", 29 | "react-grid-layout": "^0.14.4", 30 | "react-leaflet": "^1.1.6", 31 | "react-leaflet-div-icon": "^1.0.2", 32 | "react-leaflet-markercluster": "^1.1.5", 33 | "react-md": "^1.0.10", 34 | "react-render-html": "^0.1.6", 35 | "react-router": "3.0.0", 36 | "react-scripts-ts": "1.4", 37 | "recharts": "^0.21.2", 38 | "tslint": "^4.0.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 | "fs": "^0.0.1-security", 46 | "graphql": "^0.10.1", 47 | "graphql-tools": "^1.0.0", 48 | "lodash": "^4.17.4", 49 | "lodash.throttle": "^4.1.1", 50 | "material-colors": "^1.2.5", 51 | "moment": "^2.18.0", 52 | "morgan": "^1.8.1", 53 | "ms-rest-azure": "^2.1.2", 54 | "passport": "^0.3.2", 55 | "passport-azure-ad": "^3.0.5", 56 | "react-ace": "^5.0.1", 57 | "react-apollo": "^1.4.3", 58 | "react-json-tree": "^0.10.9", 59 | "xhr-request": "^1.0.1" 60 | }, 61 | "scripts": { 62 | "css:build": "node-sass src/ -o src/", 63 | "css:watch": "yarn run css:build && node-sass src/ -o src/ --watch --recursive", 64 | "client:start": "react-scripts-ts start", 65 | "start": "npm-run-all -p css:watch client:start", 66 | "coverage": "react-scripts-ts test --env=jsdom --coverage --collectCoverageFrom=src/**/*.{ts,tsx} --collectCoverageFrom=!src/**/*.d.ts", 67 | "lint": "tslint src", 68 | "build": "yarn run css:build && react-scripts-ts build && rm -rf ../build/static && cp -rf build ../", 69 | "test": "react-scripts-ts test --env=jsdom" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/ibex-dashboard-apollo-graphql/60e0b2174a54d376aea6541aeebb931f397172af/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/images/apollo-gql-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/ibex-dashboard-apollo-graphql/60e0b2174a54d376aea6541aeebb931f397172af/client/public/images/apollo-gql-preview.png -------------------------------------------------------------------------------- /client/public/images/bot-framework-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/ibex-dashboard-apollo-graphql/60e0b2174a54d376aea6541aeebb931f397172af/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 | 4 | interface IAccountActions { 5 | failure(error: any): any; 6 | updateAccount(): any; 7 | } 8 | 9 | class AccountActions extends AbstractActions implements IAccountActions { 10 | constructor(alt: AltJS.Alt) { 11 | super(alt); 12 | } 13 | 14 | updateAccount() { 15 | 16 | return (dispatcher: (account: IDictionary) => void) => { 17 | 18 | request('/auth/account', { json: true }, (error: any, result: any) => { 19 | if (error) { 20 | return this.failure(error); 21 | } 22 | return dispatcher({ account: result.account }); 23 | } 24 | ); 25 | 26 | }; 27 | } 28 | 29 | failure(error: any) { 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 | constructor(alt: AltJS.Alt) { 9 | super(alt); 10 | } 11 | 12 | updateConnection(connectionName: string, args: IDictionary) { 13 | return { connectionName, args }; 14 | } 15 | } 16 | 17 | const connectionsActions = alt.createActions(ConnectionsActions); 18 | 19 | export default connectionsActions; 20 | -------------------------------------------------------------------------------- /client/src/actions/FilterActions.ts: -------------------------------------------------------------------------------- 1 | import alt, { AbstractActions } from '../alt'; 2 | import * as request from 'xhr-request'; 3 | 4 | interface IFilterActions { 5 | filterChanged(filterId: string, selectedValues: string[]): any; 6 | } 7 | 8 | class FilterActions extends AbstractActions implements IFilterActions { 9 | constructor(alt: AltJS.Alt) { 10 | super(alt); 11 | } 12 | 13 | filterChanged(filterId: string, selectedValues: string[]): any { 14 | return {filterId , selectedValues}; 15 | } 16 | } 17 | 18 | const filterActions = alt.createActions(FilterActions); 19 | 20 | export default filterActions; 21 | -------------------------------------------------------------------------------- /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 | constructor(alt: AltJS.Alt) { 10 | super(alt); 11 | } 12 | 13 | saveSettings() { 14 | return { }; 15 | } 16 | 17 | saveSettingsCompleted() { 18 | return { }; 19 | } 20 | } 21 | 22 | const settingsActions = alt.createActions(SettingsActions); 23 | 24 | export default settingsActions; 25 | -------------------------------------------------------------------------------- /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 | constructor(alt: AltJS.Alt) { 13 | super(alt); 14 | } 15 | 16 | load() { 17 | 18 | return (dispatcher: (setupConfig: ISetupConfig) => void) => { 19 | 20 | request('/api/setup', { json: true }, (setupError: any, setupConfig: ISetupConfig) => { 21 | return dispatcher(setupConfig); 22 | }); 23 | }; 24 | } 25 | 26 | save(setupConfig: ISetupConfig, successCallback: () => void) { 27 | return (dispatcher: (setupConfig: ISetupConfig) => void) => { 28 | 29 | setupConfig.stage = 'configured'; 30 | let stringConfig = JSON.stringify(setupConfig); 31 | 32 | request('/api/setup', { 33 | method: 'POST', 34 | json: true, 35 | body: { json: stringConfig } 36 | }, 37 | (setupError: any, setupJson: any) => { 38 | 39 | if (setupError) { 40 | return this.failure(setupError); 41 | } 42 | 43 | return request('/auth/init', 44 | (authError: any, authJson: any) => { 45 | 46 | if (authError) { 47 | return this.failure(authError); 48 | } 49 | 50 | let toast: IToast = { text: 'Setup was saved successfully.' }; 51 | ToastActions.addToast(toast); 52 | 53 | try { 54 | if (successCallback) { 55 | successCallback(); 56 | } 57 | } catch (e) { } 58 | 59 | return dispatcher(authJson); 60 | } 61 | ); 62 | } 63 | ); 64 | }; 65 | } 66 | 67 | failure(error: any) { 68 | return { error }; 69 | } 70 | } 71 | 72 | const setupActions = alt.createActions(SetupActions); 73 | 74 | export default setupActions; 75 | -------------------------------------------------------------------------------- /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 | constructor(alt: AltJS.Alt) { 11 | super(alt); 12 | } 13 | 14 | setFlags(flags: IDict): any { 15 | return flags; 16 | } 17 | 18 | initializeFlag(flagName: string): any { 19 | 20 | } 21 | 22 | turnFlagOn(flagName: string): any { 23 | let flag = {}; 24 | flag[flagName] = true; 25 | return flag; 26 | } 27 | turnFlagOff(flagName: string): any { 28 | let flag = {}; 29 | flag[flagName] = false; 30 | return flag; 31 | } 32 | } 33 | 34 | const visibilityActions = alt.createActions(VisibilityActions); 35 | 36 | export default visibilityActions; 37 | -------------------------------------------------------------------------------- /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/apollo/components/QueryRenderer.tsx: -------------------------------------------------------------------------------- 1 | // This is a temp file that should be deleted 2 | import * as React from 'react'; 3 | 4 | export interface IQueryRendererProps { 5 | results: any; 6 | } 7 | 8 | export default class QueryRenderer extends React.PureComponent { 9 | render() { 10 | return ( 11 |
12 |
{this.props.results}
13 |
14 | ); 15 | } 16 | } -------------------------------------------------------------------------------- /client/src/apollo/components/SimpleBarChartQueryRenderer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Card from '../../components/Card'; 3 | import {BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer} from 4 | 'recharts'; 5 | 6 | import dialogActions from '../../components/generic/Dialogs/DialogsActions'; 7 | 8 | import colors from '../../components/colors'; 9 | var { ThemeColors } = colors; 10 | 11 | export interface ISimpleBarChartQueryRendererProps { 12 | results: any; 13 | dialog: string; 14 | title: string; 15 | subtitle: string; 16 | filterValues: [string]; 17 | filterKey: string; 18 | } 19 | 20 | export default class SimpleBarChartQueryRenderer extends 21 | React.PureComponent { 22 | 23 | constructor(props: any) { 24 | super(props); 25 | 26 | this.onDialogOpen = this.onDialogOpen.bind(this); 27 | } 28 | 29 | onDialogOpen(filterValues: any) { 30 | var dialogId = this.props.dialog; 31 | if (!dialogId) { 32 | return; 33 | } 34 | 35 | var customTitle = 'More info on \'' + filterValues + '\''; 36 | dialogActions.openDialog( 37 | dialogId, { 38 | title: customTitle, keyToFilterOn: this.props.filterKey, valuesToFilterOn: filterValues 39 | }); 40 | } 41 | 42 | shouldComponentUpdate(nextProps: any, nextState: any) { 43 | return !nextProps.loading; 44 | } 45 | 46 | // This method extracts from the data all the unique series names. not efficient at the moment 47 | // and should be updated 48 | naiveGetAllDifferentBars(data: any) { 49 | var bars = []; 50 | var saw = []; 51 | var ind = 0; 52 | for (var i = 0; i < data.length; i++) { 53 | for (var key in data[i]) { 54 | if (data[i].hasOwnProperty(key)) { 55 | if (!saw[key] && key !== 'name') { 56 | saw[key] = {}; 57 | 58 | bars.push( 59 | 64 | ); 65 | ind++; 66 | } 67 | } 68 | } 69 | } 70 | 71 | return bars; 72 | } 73 | 74 | render() { 75 | var bars = this.naiveGetAllDifferentBars(this.props.results); 76 | 77 | return ( 78 |
79 | 80 | 81 | 83 | 84 | 85 | 86 | 87 | 88 | {bars} 89 | 90 | 91 | 92 |
93 | ); 94 | } 95 | } -------------------------------------------------------------------------------- /client/src/apollo/components/StraightAnglePieChartQueryRenderer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Card from '../../components/Card'; 3 | import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Pie, PieChart } from 4 | 'recharts'; 5 | 6 | export interface IStraightAnglePieChartQueryRendererProps { 7 | results: any; 8 | title: string; 9 | subtitle: string; 10 | } 11 | 12 | export default class StraightAnglePieChartQueryRenderer extends 13 | React.PureComponent { 14 | render() { 15 | return ( 16 |
17 | 18 | 19 | 20 | 30 | 31 | 32 | 33 | 34 |
35 | ); 36 | } 37 | } -------------------------------------------------------------------------------- /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/Card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Media } from 'react-md/lib/Media'; 3 | import { Card, CardTitle } from 'react-md/lib/Cards'; 4 | import TooltipFontIcon from './TooltipFontIcon'; 5 | import Button from 'react-md/lib/Buttons'; 6 | 7 | export default ({children = {}, title = '', subtitle = ''}) => { 8 | const titleNode = {title}; 9 | const tooltipNode = ( 10 | 18 | info 19 | 20 | ); 21 | 22 | return ( 23 | 24 | 25 | 26 | {children} 27 | 28 | 29 | ); 30 | }; -------------------------------------------------------------------------------- /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 | constructor(alt: AltJS.Alt) { 14 | super(alt); 15 | } 16 | 17 | openDialog() { 18 | return {}; 19 | } 20 | 21 | closeDialog() { 22 | return {}; 23 | } 24 | 25 | loadDashboard(dashboardId: string) { 26 | this.openDialog(); 27 | return (dispatch) => { 28 | request('/api/dashboards/' + dashboardId + '?format=raw', {}, function (err: any, data: any) { 29 | if (err) { 30 | throw err; 31 | } 32 | return dispatch( data ); 33 | }); 34 | }; 35 | } 36 | 37 | selectTheme(index: number) { 38 | return index; 39 | } 40 | 41 | updateValue(newValue: string): string { 42 | return newValue; 43 | } 44 | 45 | } 46 | 47 | const editorActions = alt.createActions(EditorActions); 48 | 49 | export default editorActions; 50 | -------------------------------------------------------------------------------- /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, '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/Navbar/NavigationLink.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Link } from 'react-router'; 3 | 4 | export default class NavigationLink extends React.PureComponent { 5 | // NOTE: Don't try using Stateless (function) component here. `ref` is 6 | // required by React-MD/AccessibleFakeButton, but Stateless components 7 | // don't have one by design: 8 | // https://github.com/facebook/react/issues/4936 9 | render () { 10 | const { href, as, children, ..._props } = this.props 11 | return ( 12 |
13 | 14 | 15 | {children} 16 | 17 | 18 |
19 | ) 20 | } 21 | } -------------------------------------------------------------------------------- /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/Settings/ElementsSettings/ElementSettingsFactory.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import plugins from '../../generic/plugins'; 4 | 5 | export default class ElementSettingsFactory { 6 | 7 | static uniqueId = 0; 8 | 9 | static getSettingsEditor(element: IElement): JSX.Element { 10 | 11 | if (!element) { return null; } 12 | 13 | let ReactElementClass = plugins[element.type]; 14 | if (ReactElementClass.editor) { 15 | let SettingsEditor = ReactElementClass.editor; 16 | return ; 17 | } 18 | return null; 19 | } 20 | } -------------------------------------------------------------------------------- /client/src/components/Settings/ElementsSettings/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as _ from 'lodash'; 3 | import ListItem from 'react-md/lib/Lists/ListItem'; 4 | import List from 'react-md/lib/Lists/List'; 5 | 6 | import ElementSettingsFactory from './ElementSettingsFactory'; 7 | 8 | interface IElementsSettingsProps { 9 | settings: IElementsContainer; 10 | } 11 | 12 | interface IElementsSettingsState { 13 | selectedIndex: number; 14 | } 15 | 16 | export default class ElementsSettings extends React.Component { 17 | 18 | state = { 19 | selectedIndex: 0 20 | }; 21 | 22 | constructor(props: IElementsSettingsProps) { 23 | super(props); 24 | 25 | this.onMenuClick = this.onMenuClick.bind(this); 26 | } 27 | 28 | onMenuClick(index: number) { 29 | this.setState({ selectedIndex: index }); 30 | } 31 | 32 | render() { 33 | 34 | let menuItems = () => { 35 | let { selectedIndex } = this.state; 36 | let items = this.props.settings.elements.map((item, idx) => ( 37 | 44 | )); 45 | 46 | return ( 47 | 48 | {items} 49 | 50 | ); 51 | }; 52 | 53 | let getSettingsEditor = () => { 54 | 55 | // Create settings element 56 | let selectedSettings = 57 | this.props.settings.elements.length > this.state.selectedIndex ? 58 | this.props.settings.elements[this.state.selectedIndex] : 59 | null; 60 | if (!selectedSettings) { 61 | return

No elements were found

; 62 | } 63 | 64 | let editor = ElementSettingsFactory.getSettingsEditor(selectedSettings); 65 | if (editor) { return editor; } 66 | 67 | return

No settings yet for {selectedSettings.type}

; 68 | }; 69 | 70 | return ( 71 |
72 | {menuItems()} 73 |
74 | {getSettingsEditor()} 75 |
76 |
77 | ); 78 | } 79 | } -------------------------------------------------------------------------------- /client/src/components/Settings/ElementsSettings/readme.md: -------------------------------------------------------------------------------- 1 | # Pie Settings 2 | Setting up the Pie chart is very easy. 3 | [![Preview pie settings](../../docs/pie-settings.png)] 4 | The UI allows you to change the config file with much ease. It will map the following config: 5 | ``` 6 | { 7 | id: "channels", 8 | type: "PieData", 9 | title: "Channel Usage 1", 10 | subtitle: "Total messages sent per channel", 11 | size: { 12 | w: 6, 13 | h: 8 14 | }, 15 | dependencies: { 16 | values: "ai:timeline-channelUsage" 17 | }, 18 | props: { 19 | showLegend: false 20 | } 21 | } 22 | ``` 23 | Please note, you should not change the **"dependencies"** property, and it is not being reflected in the UI as well. -------------------------------------------------------------------------------- /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 | import {Toast, ToastActions, IToast} from '../Toast'; 13 | 14 | interface ISpinnerState extends ISpinnerStoreState { 15 | } 16 | 17 | export default class Spinner extends React.Component { 18 | 19 | constructor(props: any) { 20 | super(props); 21 | 22 | this.state = SpinnerStore.getState(); 23 | 24 | this.onChange = this.onChange.bind(this); 25 | this._429ApplicationInsights = this._429ApplicationInsights.bind(this); 26 | 27 | var self = this; 28 | var openOriginal = XMLHttpRequest.prototype.open; 29 | var sendOriginal = XMLHttpRequest.prototype.send; 30 | 31 | XMLHttpRequest.prototype.open = function(method: string, url: string, async?: boolean, _?: string, __?: string) { 32 | SpinnerActions.startRequestLoading(); 33 | openOriginal.apply(this, arguments); 34 | }; 35 | 36 | XMLHttpRequest.prototype.send = function(data: any) { 37 | let _xhr: XMLHttpRequest = this; 38 | _xhr.onreadystatechange = (response) => { 39 | 40 | // readyState === 4: means the response is complete 41 | if (_xhr.readyState === 4) { 42 | SpinnerActions.endRequestLoading(); 43 | 44 | if (_xhr.status === 429) { 45 | self._429ApplicationInsights(); 46 | } 47 | } 48 | }; 49 | sendOriginal.apply(_xhr, arguments); 50 | }; 51 | 52 | // Todo: Add timeout to requests - if no reply received, turn spinner off 53 | } 54 | 55 | componentDidMount() { 56 | SpinnerStore.listen(this.onChange); 57 | } 58 | 59 | _429ApplicationInsights() { 60 | let toast: IToast = { text: 'You have reached the maximum number of Application Insights requests.' }; 61 | ToastActions.addToast(toast); 62 | } 63 | 64 | onChange(state: ISpinnerState) { 65 | this.setState(state); 66 | } 67 | 68 | render () { 69 | 70 | let refreshing = this.state.pageLoading || this.state.requestLoading || false; 71 | 72 | return ( 73 |
74 | {refreshing && } 75 |
76 | ); 77 | } 78 | } -------------------------------------------------------------------------------- /client/src/components/Spinner/SpinnerActions.ts: -------------------------------------------------------------------------------- 1 | import alt, { AbstractActions } from '../../alt'; 2 | 3 | interface ISpinnerActions { 4 | startPageLoading(): void; 5 | endPageLoading(): void; 6 | startRequestLoading(): void; 7 | endRequestLoading(): void; 8 | } 9 | 10 | class SpinnerActions extends AbstractActions /*implements ISpinnerActions*/ { 11 | constructor(alt: AltJS.Alt) { 12 | super(alt); 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 spinnerActions from './SpinnerActions'; 4 | 5 | export interface ISpinnerStoreState { 6 | pageLoading?: number; 7 | requestLoading?: number; 8 | mounted: boolean; 9 | currentBreakpoint: string; 10 | layouts: object; 11 | } 12 | 13 | class SpinnerStore extends AbstractStoreModel implements ISpinnerStoreState { 14 | 15 | pageLoading: number; 16 | requestLoading: number; 17 | mounted: boolean; 18 | currentBreakpoint: string; 19 | layouts: any; 20 | 21 | constructor() { 22 | super(); 23 | 24 | this.pageLoading = 0; 25 | this.requestLoading = 0; 26 | this.mounted = false; 27 | this.currentBreakpoint = 'lg'; 28 | this.layouts = { }; 29 | 30 | this.bindListeners({ 31 | startPageLoading: spinnerActions.startPageLoading, 32 | endPageLoading: spinnerActions.endPageLoading, 33 | startRequestLoading: spinnerActions.startRequestLoading, 34 | endRequestLoading: spinnerActions.endRequestLoading, 35 | }); 36 | } 37 | 38 | startPageLoading(): void { 39 | this.pageLoading++; 40 | } 41 | 42 | endPageLoading(): void { 43 | this.pageLoading--; 44 | } 45 | 46 | startRequestLoading(): void { 47 | this.requestLoading++; 48 | } 49 | 50 | endRequestLoading(): void { 51 | this.requestLoading--; 52 | } 53 | } 54 | 55 | const spinnerStore = alt.createStore(SpinnerStore, 'SpinnerStore'); 56 | 57 | export default spinnerStore; 58 | -------------------------------------------------------------------------------- /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: any) { 20 | this.setState(state); 21 | } 22 | 23 | componentDidMount() { 24 | ToastStore.listen(this.onChange); 25 | } 26 | 27 | render() { 28 | return ( 29 | 35 | ); 36 | } 37 | 38 | private removeToast() { 39 | ToastActions.removeToast(); 40 | } 41 | } -------------------------------------------------------------------------------- /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(alt: AltJS.Alt) { 12 | super(alt); 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, '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.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/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/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/common/ArrayInput.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Paper from 'react-md/lib/Papers'; 3 | import Button from 'react-md/lib/Buttons'; 4 | import TextField from 'react-md/lib/TextFields'; 5 | 6 | interface IArrayInputProps{ 7 | value:any 8 | } 9 | interface IArrayInputState{ 10 | value:any 11 | } 12 | 13 | export default class ArrayInput extends React.Component{ 14 | 15 | constructor(props: IArrayInputProps) { 16 | super(props); 17 | 18 | this.add_more = this.add_more.bind(this); 19 | this.remove_item = this.remove_item.bind(this); 20 | this.onParamChange = this.onParamChange.bind(this); 21 | } 22 | 23 | state:IArrayInputState ={ 24 | value : (!this.props.value || this.props.value == '')?['']:this.props.value 25 | } 26 | 27 | 28 | add_more() { 29 | let new_val = this.state.value.concat([]); 30 | new_val.push(''); 31 | this.setState({ value: new_val }); 32 | } 33 | 34 | remove_item(i,e) { 35 | let new_state = this.state.value.concat([]); 36 | new_state[i] = undefined; 37 | this.setState({ value: new_state }); 38 | } 39 | 40 | onParamChange(value: any, event: any) { 41 | var arr = this.state.value.concat([]); 42 | arr[event.target.key] = value; 43 | this.setState({value:arr}); 44 | } 45 | 46 | render() { 47 | let me = this; 48 | 49 | let lines = this.state.value.map( function(e, i) { 50 | if (e == undefined) { 51 | return null; 52 | } 53 | 54 | return ( 55 |
56 | 57 | 58 |
59 | ) 60 | }).filter( function(e) { 61 | return e != undefined; 62 | }) 63 | 64 | return ( 65 | 66 | 67 | {lines} 68 | 69 | 70 | 71 | ) 72 | } 73 | }; -------------------------------------------------------------------------------- /client/src/components/common/InfoDrawer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import Button from 'react-md/lib/Buttons/Button'; 4 | import Toolbar from 'react-md/lib/Toolbars'; 5 | import Drawer from 'react-md/lib/Drawers'; 6 | import Media from 'react-md/lib/Media/Media'; 7 | import Card from 'react-md/lib/Cards/Card'; 8 | import CardTitle from 'react-md/lib/Cards/CardTitle'; 9 | 10 | interface IInfoDrawerState { 11 | open: boolean; 12 | } 13 | 14 | interface IInfoDrawerProps { 15 | title?: string; 16 | width?: number; 17 | children?: any; 18 | buttonLabel?: string; 19 | buttonTooltip?: string; 20 | buttonIcon?: string; 21 | buttonStyle?: any; 22 | } 23 | 24 | export default class InfoDrawer extends React.Component { 25 | 26 | state: IInfoDrawerState = { 27 | open: false 28 | }; 29 | 30 | constructor(props: IInfoDrawerProps) { 31 | super(props); 32 | 33 | this.open = this.open.bind(this); 34 | this.close = this.close.bind(this); 35 | } 36 | 37 | open() { 38 | this.setState({ open: true }); 39 | } 40 | 41 | close() { 42 | this.setState({ open: false }); 43 | } 44 | 45 | render() { 46 | 47 | let { open } = this.state; 48 | let { width, title, buttonTooltip, buttonIcon, buttonLabel, buttonStyle } = this.props; 49 | 50 | const drawerHeader = ( 51 | close} 54 | className="md-divider-border md-divider-border--bottom" 55 | /> 56 | ); 57 | 58 | return ( 59 |
60 |
61 | 69 | {buttonLabel && ( 70 | 71 | {buttonLabel} 72 | 73 | )} 74 |
75 | {}} 79 | position={'right'} 80 | type={Drawer.DrawerTypes.TEMPORARY} 81 | header={drawerHeader} 82 | style={{ zIndex: 100 }} 83 | > 84 | 85 | {this.props.children} 86 | 87 | 88 |
89 | ); 90 | } 91 | } -------------------------------------------------------------------------------- /client/src/components/common/TokenInput.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as _ from 'lodash'; 3 | import Paper from 'react-md/lib/Papers'; 4 | import Button from 'react-md/lib/Buttons'; 5 | import TextField from 'react-md/lib/TextFields'; 6 | import Chip from 'react-md/lib/Chips'; 7 | import Divider from 'react-md/lib/Dividers'; 8 | 9 | interface ITokenInputProps { 10 | tokens: any[]; 11 | zDepth: number; 12 | onTokensChanged(): void; 13 | }; 14 | 15 | interface ITokenInputState { 16 | newToken: any; 17 | }; 18 | 19 | /** 20 | * This is a UI for editing a string array. 21 | */ 22 | export default class TokenInput extends React.Component { 23 | 24 | state: ITokenInputState = { 25 | newToken: '' 26 | }; 27 | 28 | constructor(props: ITokenInputProps) { 29 | super(props); 30 | this.removeToken = this.removeToken.bind(this); 31 | this.onNewTokenChange = this.onNewTokenChange.bind(this); 32 | this.addToken = this.addToken.bind(this); 33 | } 34 | 35 | removeToken(token: any) { 36 | let tokens = this.props.tokens; 37 | _.remove(tokens, x => x === token); 38 | if (this.props.onTokensChanged) { 39 | this.props.onTokensChanged(); 40 | } 41 | this.setState(this.state); // foce the component to update 42 | } 43 | 44 | onNewTokenChange(newData: any) { 45 | this.setState({ newToken: newData }); 46 | } 47 | 48 | addToken() { 49 | if (this.state.newToken) { 50 | let {tokens} = this.props; 51 | tokens = tokens || []; 52 | tokens.push(this.state.newToken); 53 | this.setState({ 54 | newToken: '' 55 | }); 56 | if (this.props.onTokensChanged) { 57 | this.props.onTokensChanged(); 58 | } 59 | } 60 | } 61 | 62 | render() { 63 | let { tokens, zDepth } = this.props; 64 | let { newToken } = this.state; 65 | 66 | let chips = tokens.map((token: string, index: number) => ( 67 | 73 | )); 74 | 75 | return ( 76 | 77 |
78 | {chips} 79 |
80 | 81 |
82 | 90 | 91 |
92 |
93 | ); 94 | } 95 | }; -------------------------------------------------------------------------------- /client/src/components/generic/Area/Settings.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as _ from 'lodash'; 3 | 4 | import FontIcon from 'react-md/lib/FontIcons'; 5 | import Switch from 'react-md/lib/SelectionControls/Switch'; 6 | 7 | import { BaseSettings, IBaseSettingsProps, IBaseSettingsState } from '../../common/BaseSettings'; 8 | 9 | export default class AreaSettings extends BaseSettings { 10 | 11 | icon = 'flip_to_front'; 12 | 13 | renderChildren() { 14 | let { settings } = this.props; 15 | let { id, dependencies, actions, props, title, subtitle, size, theme, type } = settings; 16 | 17 | return ( 18 | 19 | 20 |
21 | insert_chart 22 | 23 | 30 | 31 |
32 |
33 | 34 |
35 | dns 36 | 37 | 44 | 45 |
46 |
47 |
48 | ); 49 | } 50 | } -------------------------------------------------------------------------------- /client/src/components/generic/BarData/Settings.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import FontIcon from 'react-md/lib/FontIcons'; 4 | import TextField from 'react-md/lib/TextFields'; 5 | 6 | import {BaseSettings, IBaseSettingsProps, IBaseSettingsState } from '../../common/BaseSettings'; 7 | 8 | export default class BarDataSettings extends BaseSettings { 9 | 10 | icon = 'insert_chart'; 11 | 12 | renderChildren() { 13 | let { settings } = this.props; 14 | let { props } = settings; 15 | 16 | return ( 17 | text_fields} 22 | className="md-cell md-cell--bottom md-cell--6" 23 | value={props.nameKey} 24 | onChange={this.onParamChange} 25 | /> 26 | ); 27 | } 28 | } -------------------------------------------------------------------------------- /client/src/components/generic/BarData/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as _ from 'lodash'; 3 | import * as moment from 'moment'; 4 | 5 | import Card from '../../Card'; 6 | import { GenericComponent, IGenericProps, IGenericState } from '../GenericComponent'; 7 | import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; 8 | 9 | import colors from '../../colors'; 10 | const { ThemeColors } = colors; 11 | 12 | import settings from './Settings'; 13 | 14 | interface IBarProps extends IGenericProps { 15 | props: { 16 | barProps: { [key: string]: Object }; 17 | showLegend: boolean; 18 | /** The name of the property in the data source that contains the name for the X axis */ 19 | nameKey: string; 20 | }; 21 | }; 22 | 23 | interface IBarState extends IGenericState { 24 | values: Object[]; 25 | bars: Object[]; 26 | } 27 | 28 | export default class BarData extends GenericComponent { 29 | 30 | static editor = settings; 31 | 32 | state = { 33 | values: [], 34 | bars: [] 35 | }; 36 | 37 | constructor(props: any) { 38 | super(props); 39 | 40 | this.handleClick = this.handleClick.bind(this); 41 | } 42 | 43 | handleClick(data: any, index: number) { 44 | this.trigger('onBarClick', data.payload); 45 | } 46 | 47 | render() { 48 | var { values, bars } = this.state; 49 | var { title, subtitle, props } = this.props; 50 | var { barProps, showLegend, nameKey } = props; 51 | 52 | if (!values) { 53 | return null; 54 | } 55 | 56 | if (!values || !values.length) { 57 | return ( 58 | 59 |
No data is available
60 |
61 | ); 62 | } 63 | 64 | var barElements = []; 65 | if (values && values.length && bars) { 66 | barElements = bars.map((bar, idx) => { 67 | return ( 68 | 75 | ); 76 | }); 77 | } 78 | 79 | // Todo: Receive the width of the SVG component from the container 80 | return ( 81 | 82 | 83 | 88 | 89 | 90 | 91 | 92 | {barElements} 93 | {showLegend !== false && 94 | 95 | } 96 | 97 | 98 | 99 | ); 100 | } 101 | } -------------------------------------------------------------------------------- /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 | constructor(alt: AltJS.Alt) { 10 | super(alt); 11 | } 12 | 13 | openDialog(dialogName: string, args: { [id: string]: Object }) { 14 | return { dialogName, args }; 15 | } 16 | 17 | closeDialog() { 18 | return {}; 19 | } 20 | } 21 | 22 | const dialogsActions = alt.createActions(DialogsActions); 23 | 24 | export default dialogsActions; 25 | -------------------------------------------------------------------------------- /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, '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.visualDialog) { 10 | return null; 11 | } 12 | 13 | var dialogs = dashboard.visualDialog.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/PieData/Settings.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as _ from 'lodash'; 3 | 4 | import FontIcon from 'react-md/lib/FontIcons'; 5 | import Switch from 'react-md/lib/SelectionControls/Switch'; 6 | 7 | import { BaseSettings, IBaseSettingsProps, IBaseSettingsState } from '../../common/BaseSettings'; 8 | 9 | export default class PieSettings extends BaseSettings { 10 | 11 | icon = 'pie_chart'; 12 | 13 | renderChildren() { 14 | let { settings } = this.props; 15 | let { id, dependencies, actions, props, title, subtitle, size, theme, type } = settings; 16 | 17 | return ( 18 | 19 |
20 | insert_chart 21 | 22 | 29 | 30 |
31 |
32 | ); 33 | } 34 | } -------------------------------------------------------------------------------- /client/src/components/generic/RadarChartCard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { GenericComponent, IGenericProps, IGenericState } from './GenericComponent'; 3 | import Card from '../Card'; 4 | import { RadarChart, Radar, PolarGrid, Legend, PolarAngleAxis, PolarRadiusAxis, ResponsiveContainer } from 'recharts'; 5 | 6 | import * as _ from 'lodash'; 7 | import * as moment from 'moment'; 8 | 9 | import colors from '../colors'; 10 | var { ThemeColors } = colors; 11 | 12 | interface IRadarProps extends IGenericProps { 13 | props: { 14 | 15 | nameKey: string; 16 | }; 17 | }; 18 | 19 | interface IRadarState extends IGenericState { 20 | values: Object[]; 21 | } 22 | 23 | export default class RadarChartCard extends GenericComponent { 24 | 25 | state = { 26 | values: [], 27 | bars: [] 28 | }; 29 | 30 | constructor(props: any) { 31 | super(props); 32 | 33 | this.handleClick = this.handleClick.bind(this); 34 | } 35 | 36 | handleClick(data: any, index: number) { 37 | this.trigger('onBarClick', data.payload); 38 | } 39 | 40 | render() { 41 | 42 | var { values } = this.state; 43 | var { title, subtitle, props } = this.props; 44 | 45 | if (!values) { 46 | return null; 47 | } 48 | 49 | const domain = 100; 50 | 51 | const data05 = [ 52 | { subject: 'Math', 'NFL': 120, 'NBA': 110, fullMark: domain }, 53 | { subject: 'Chinese', 'NFL': 98, 'NBA': 30, fullMark: domain }, 54 | { subject: 'English', 'NFL': 86, 'NBA': 130, fullMark: domain }, 55 | { subject: 'Geography', 'NFL': 110, 'NBA': 95, fullMark: domain }, 56 | { subject: 'Physics', 'NFL': 102, 'NBA': 90, fullMark: domain }, 57 | { subject: 'History', 'NFL': 65, 'NBA': 85, fullMark: domain }, 58 | ]; 59 | 60 | return ( 61 | 62 | 63 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | ); 79 | } 80 | } -------------------------------------------------------------------------------- /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 { RadialBarChart, RadialBar, PolarGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; 5 | 6 | import * as _ from 'lodash'; 7 | import * as moment from 'moment'; 8 | 9 | import colors from '../colors'; 10 | var { ThemeColors } = colors; 11 | 12 | interface IRadarProps extends IGenericProps { 13 | props: { 14 | 15 | nameKey: string; 16 | }; 17 | }; 18 | 19 | interface IRadarState extends IGenericState { 20 | values: Object[]; 21 | } 22 | 23 | export default class RadialBarChartCard extends GenericComponent { 24 | 25 | state = { 26 | values: [], 27 | bars: [] 28 | }; 29 | 30 | constructor(props: any) { 31 | super(props); 32 | 33 | this.handleClick = this.handleClick.bind(this); 34 | } 35 | 36 | handleClick(data: any, index: number) { 37 | this.trigger('onBarClick', data.payload); 38 | } 39 | 40 | render() { 41 | 42 | var { values } = this.state; 43 | var { title, subtitle, props } = this.props; 44 | 45 | if (!values) { 46 | return null; 47 | } 48 | 49 | const domain = 100; 50 | 51 | const data = [ 52 | { name: 'alarm.set', uv: 31.47, pv: 2400, fill: '#8884d8' }, 53 | { name: '*:/', uv: 26.69, pv: 4567, fill: '#83a6ed' }, 54 | { name: 'none', uv: 15.69, pv: 1398, fill: '#8dd1e1' }, 55 | { name: 'invalid property type object', uv: 8.22, pv: 9800, fill: '#82ca9d' } 56 | ]; 57 | 58 | return ( 59 | 60 | 61 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /client/src/components/generic/Scatter/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as moment from 'moment'; 3 | import * as _ from 'lodash'; 4 | 5 | import Card from '../../Card'; 6 | import { GenericComponent, IGenericProps, IGenericState } from '../GenericComponent'; 7 | import { ScatterChart, Scatter as ScatterLine, XAxis, YAxis, ZAxis, CartesianGrid } from 'recharts'; 8 | import { Tooltip, Legend, ResponsiveContainer } from 'recharts'; 9 | 10 | import colors from '../../colors'; 11 | const { ThemeColors } = colors; 12 | 13 | import settings from './Settings'; 14 | 15 | interface IScatterProps extends IGenericProps { 16 | theme?: string[]; 17 | xDataKey?: string; 18 | yDataKey?: string; 19 | zDataKey?: string; 20 | zRange?: number[]; 21 | } 22 | 23 | interface IScatterState extends IGenericState { 24 | } 25 | 26 | export default class Scatter extends GenericComponent { 27 | 28 | static editor = settings; 29 | 30 | static defaultProps = { 31 | xDataKey: 'x', 32 | yDataKey: 'y', 33 | zDataKey: 'z', 34 | zRange: [10, 1000] 35 | }; 36 | 37 | render() { 38 | var { groupedValues } = this.state; 39 | var { title, subtitle, theme, props } = this.props; 40 | var { scatterProps, groupTitles } = props; 41 | 42 | var { xDataKey, yDataKey, zDataKey, zRange } = this.props.props; 43 | if (xDataKey === undefined) { xDataKey = Scatter.defaultProps.xDataKey; } 44 | if (yDataKey === undefined) { yDataKey = Scatter.defaultProps.yDataKey; } 45 | if (zDataKey === undefined) { zDataKey = Scatter.defaultProps.zDataKey; } 46 | if (zRange === undefined) { zRange = Scatter.defaultProps.zRange; } 47 | 48 | var themeColors = theme || ThemeColors; 49 | 50 | let scatterLines = []; 51 | let idx = 0; 52 | if (groupedValues) { 53 | Object.keys(groupedValues).forEach((key) => { 54 | if (!key) { 55 | return; 56 | } 57 | let values = groupedValues[key]; 58 | let line = ( 59 | 66 | ); 67 | scatterLines.push(line); 68 | idx += 1; 69 | }); 70 | } 71 | 72 | return ( 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | {scatterLines} 83 | 84 | 85 | 86 | ); 87 | } 88 | } -------------------------------------------------------------------------------- /client/src/components/generic/SimpleRadialBarChartCard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { GenericComponent, IGenericProps, IGenericState } from './GenericComponent'; 3 | import Card from '../Card'; 4 | import { RadialBarChart, RadialBar, Legend, Tooltip, ResponsiveContainer } from 'recharts'; 5 | 6 | import * as _ from 'lodash'; 7 | import * as moment from 'moment'; 8 | 9 | import colors from '../colors'; 10 | var { ThemeColors } = colors; 11 | 12 | interface IRadarProps extends IGenericProps { 13 | props: { 14 | 15 | nameKey: string; 16 | }; 17 | }; 18 | 19 | interface IRadarState extends IGenericState { 20 | values: Object[]; 21 | } 22 | 23 | export default class SimpleRadialBarChartCard extends GenericComponent { 24 | 25 | state = { 26 | values: [], 27 | bars: [] 28 | }; 29 | 30 | constructor(props: any) { 31 | super(props); 32 | 33 | this.handleClick = this.handleClick.bind(this); 34 | } 35 | 36 | handleClick(data: any, index: number) { 37 | this.trigger('onBarClick', data.payload); 38 | } 39 | 40 | render() { 41 | 42 | var { values } = this.state; 43 | var { title, subtitle, props } = this.props; 44 | 45 | if (!values) { 46 | return null; 47 | } 48 | 49 | const style = { 50 | top: 0, 51 | left: 350, 52 | lineHeight: '24px' 53 | }; 54 | 55 | const data = [ 56 | { name: 'alarm.set', uv: 31.47, pv: 2400, fill: '#8884d8' }, 57 | { name: '*:/', uv: 26.69, pv: 4567, fill: '#83a6ed' }, 58 | { name: 'none', uv: 15.69, pv: 1398, fill: '#8dd1e1' }, 59 | { name: 'invalid property type object', uv: 8.22, pv: 9800, fill: '#82ca9d' } 60 | ]; 61 | 62 | return ( 63 | 64 | 65 | 75 | 76 | 84 | 85 | 86 | 87 | ); 88 | } 89 | } -------------------------------------------------------------------------------- /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 | constructor(props: any) { 12 | super(props); 13 | 14 | this.onChange = this.onChange.bind(this); 15 | } 16 | 17 | onChange(newValue: any) { 18 | this.trigger('onChange', newValue); 19 | } 20 | 21 | render() { 22 | var { selectedValue, values } = this.state; 23 | var { title } = this.props; 24 | values = values || []; 25 | 26 | return ( 27 | 37 | ); 38 | } 39 | } -------------------------------------------------------------------------------- /client/src/components/generic/Timeline/Settings.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as _ from 'lodash'; 3 | 4 | import {BaseSettings, IBaseSettingsProps, IBaseSettingsState } from '../../common/BaseSettings'; 5 | 6 | export default class TimelineSettings extends BaseSettings { 7 | 8 | icon = 'timeline'; 9 | 10 | renderChildren() { 11 | } 12 | } -------------------------------------------------------------------------------- /client/src/components/generic/Timeline/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { GenericComponent, IGenericProps, IGenericState } from '../GenericComponent'; 3 | import * as moment from 'moment'; 4 | import * as _ from 'lodash'; 5 | import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; 6 | import Card from '../../Card'; 7 | 8 | import Button from 'react-md/lib/Buttons/Button'; 9 | 10 | import colors from '../../colors'; 11 | var { ThemeColors } = colors; 12 | 13 | import settings from './Settings'; 14 | 15 | interface ITimelineProps extends IGenericProps { 16 | theme?: string[]; 17 | } 18 | 19 | interface ITimelineState extends IGenericState { 20 | timeFormat: string; 21 | values: Object[]; 22 | lines: Object[]; 23 | } 24 | 25 | export default class Timeline extends GenericComponent { 26 | 27 | static editor = settings; 28 | 29 | dateFormat(time: string) { 30 | return moment(time).format('MMM-DD'); 31 | } 32 | 33 | hourFormat(time: string) { 34 | return moment(time).format('HH:mm'); 35 | } 36 | 37 | render() { 38 | var { timeFormat, values, lines } = this.state; 39 | var { title, subtitle, theme, props } = this.props; 40 | var { lineProps } = props; 41 | 42 | var format = timeFormat === 'hour' ? this.hourFormat : this.dateFormat; 43 | var themeColors = theme || ThemeColors; 44 | 45 | var lineElements = []; 46 | if (values && values.length && lines) { 47 | lineElements = lines.map((line, idx) => { 48 | return ( 49 | 57 | ); 58 | }); 59 | } 60 | 61 | return ( 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | {lineElements} 71 | 72 | 73 | 74 | ); 75 | } 76 | } -------------------------------------------------------------------------------- /client/src/components/generic/generic.scss: -------------------------------------------------------------------------------- 1 | .md-card .widgets { 2 | position: absolute; 3 | top: 10px; 4 | right: 10px; 5 | } 6 | 7 | .md-card-scorecard { 8 | 9 | .scorecard { 10 | 11 | width: 100px; 12 | height: 90px; 13 | white-space: nowrap; 14 | float: left; 15 | margin: 10px 8px 20px; 16 | 17 | &.clickable-card { 18 | cursor: pointer; 19 | 20 | &:hover { 21 | background-color: #ECEFF1; 22 | } 23 | } 24 | 25 | &.color-bottom { 26 | .md-subheading-2 { 27 | margin-bottom: 3px; 28 | } 29 | .scorecard-subheading { 30 | padding-bottom: 4px; 31 | border-bottom: solid 4px transparent; 32 | } 33 | } 34 | 35 | &.color-left { 36 | padding-left: 5px; 37 | border-left: solid 3px transparent; 38 | } 39 | 40 | .md-icon { 41 | float: left; 42 | padding: 3px 3px 0 0; 43 | } 44 | 45 | 46 | .md-headline { 47 | margin-bottom: 0; 48 | font-weight: 500; 49 | } 50 | 51 | .md-subheading-2 { 52 | text-overflow: ellipsis; 53 | overflow-x: hidden; 54 | } 55 | 56 | b { 57 | margin-right: 5px; 58 | margin-bottom: 0; 59 | font-weight: 500; 60 | } 61 | } 62 | } 63 | 64 | /* MenuFilter */ 65 | 66 | @media screen and (min-width: 320px) { 67 | .md-multiselect-menu { 68 | margin-top: 50px; 69 | } 70 | } 71 | 72 | @media screen and (min-width: 1025px) { 73 | .md-multiselect-menu { 74 | margin-top: 58px; 75 | } 76 | } 77 | 78 | .md-value { 79 | white-space: nowrap; 80 | overflow: hidden; 81 | text-overflow: ellipsis; 82 | } 83 | 84 | /* Table */ 85 | 86 | .table > .primary { 87 | display: block; 88 | color: black; 89 | } 90 | 91 | .table > .secondary { 92 | color: grey; 93 | } 94 | 95 | td.text { 96 | white-space: normal; 97 | } 98 | 99 | td.summary { 100 | max-width: 240px; 101 | overflow: hidden; 102 | text-overflow: ellipsis; 103 | } 104 | 105 | /* Sentiment */ 106 | 107 | .sentiment_very_dissatisfied { 108 | color: red !important; 109 | } 110 | 111 | .sentiment_dissatisfied { 112 | color: orange !important; 113 | } 114 | 115 | .sentiment_neutral { 116 | color: grey !important; 117 | } 118 | 119 | .sentiment_satisfied { 120 | color: yellow !important; 121 | } 122 | 123 | .sentiment_very_satisfied { 124 | color: green !important; 125 | } 126 | 127 | .error_outline { 128 | color: lightgrey !important; 129 | } 130 | 131 | .tooltip { 132 | visibility: visible !important; 133 | } -------------------------------------------------------------------------------- /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 | // dialog views 16 | import Table from './Table'; 17 | import Detail from './Detail'; 18 | import SplitPanel from './SplitPanel'; 19 | import RequestButton from './RequestButton'; 20 | 21 | export default { 22 | PieData, 23 | Timeline, 24 | Scatter, 25 | BarData, 26 | Area, 27 | Scorecard, 28 | TextFilter, 29 | CheckboxFilter, 30 | MenuFilter, 31 | Table, 32 | Detail, 33 | SplitPanel, 34 | MapData, 35 | RadarChartCard, 36 | RadialBarChartCard, 37 | SimpleRadialBarChartCard, 38 | RequestButton, 39 | }; -------------------------------------------------------------------------------- /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/application-insights.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { IConnection, ConnectionEditor, IConnectionProps } from '../Connection'; 3 | import InfoDrawer from '../../../components/common/InfoDrawer'; 4 | import TextField from 'react-md/lib/TextFields'; 5 | import Card from 'react-md/lib/Cards/Card'; 6 | import CardTitle from 'react-md/lib/Cards/CardTitle'; 7 | import Avatar from 'react-md/lib/Avatars'; 8 | import FontIcon from 'react-md/lib/FontIcons'; 9 | import QueryTester from './QueryTester'; 10 | 11 | export default class ApplicationInsightsConnection implements IConnection { 12 | type = 'application-insights'; 13 | params = [ 'appId', 'apiKey' ]; 14 | editor = AIConnectionEditor; 15 | } 16 | 17 | class AIConnectionEditor extends ConnectionEditor { 18 | 19 | render() { 20 | 21 | let { connection } = this.props; 22 | connection = connection || {}; 23 | 24 | let accessApiUri = 'https://dev.int.applicationinsights.io/documentation/Authorization/API-key-and-App-ID'; 25 | 26 | return ( 27 | 28 | receipt} />} 31 | style={{ float: 'left'}} 32 | /> 33 | 38 | 44 |
45 | Follow the instructions 46 | in this link to 47 | get Application ID and Api Key 48 |
49 | This setup will creates credential for the dashboard to query telemetry 50 | information from Application Insights. 51 |
52 |
53 | 62 | 71 |
72 | ); 73 | } 74 | } -------------------------------------------------------------------------------- /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/azure.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { IConnection, ConnectionEditor, IConnectionProps } from './Connection'; 3 | import InfoDrawer from '../../components/common/InfoDrawer'; 4 | import TextField from 'react-md/lib/TextFields'; 5 | 6 | export default class AzureConnection implements IConnection { 7 | type = 'azure'; 8 | params = [ 'servicePrincipalId', 'servicePrincipalKey', 'servicePrincipalDomain', 'subscriptionId' ]; 9 | editor = AzureConnectionEditor; 10 | } 11 | 12 | class AzureConnectionEditor extends ConnectionEditor { 13 | 14 | render() { 15 | 16 | let { connection } = this.props; 17 | connection = connection || {}; 18 | 19 | let servicePrincipalUrl = 20 | 'https://docs.microsoft.com/en-us/azure/azure-resource-manager/' + 21 | 'resource-group-create-service-principal-portal'; 22 | 23 | return ( 24 |
25 |

Azure Connection

26 | 32 |
33 | Follow the instructions 34 | in this link to 35 | get Service Principal ID and Service Principal Key 36 |
37 | This setup will creates credential for the dashboard to query resources from Azure. 38 |
39 |
40 | 49 | 58 | 59 | 68 | 77 | 78 |
79 | ); 80 | } 81 | } -------------------------------------------------------------------------------- /client/src/data-sources/connections/bot-framework.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { IConnection, ConnectionEditor, IConnectionProps } from './Connection'; 3 | import InfoDrawer from '../../components/common/InfoDrawer'; 4 | import TextField from 'react-md/lib/TextFields'; 5 | import Checkbox from 'react-md/lib/SelectionControls/Checkbox'; 6 | 7 | export default class BotFrameworkConnection implements IConnection { 8 | type = 'bot-framework'; 9 | params = ['directLine', 'conversationsEndpoint', 'webchatEndpoint']; 10 | editor = BotFrameworkEditor; 11 | } 12 | 13 | class BotFrameworkEditor extends ConnectionEditor { 14 | 15 | render() { 16 | let { connection } = this.props; 17 | return ( 18 |
19 |

Bot Framework

20 | 26 |
27 | More about 28 | Bot Framework 29 | and 30 | Direct Line 31 |
32 |

Localhost development

33 |
    34 |
  • 35 |
    Conversations Endpoint
    36 |
    https://********.ngrok.io/api/conversations
    37 |
  • 38 |
  • 39 |
    Webchat (Agent) Endpoint
    40 |
    http://localhost:3978/webchat
    41 |
  • 42 |
43 |
44 |
45 | 54 | 63 | 72 |
73 | ); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /client/src/data-sources/connections/cosmos-db.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { IConnection, ConnectionEditor, IConnectionProps } from './Connection'; 3 | import InfoDrawer from '../../components/common/InfoDrawer'; 4 | import TextField from 'react-md/lib/TextFields'; 5 | import Checkbox from 'react-md/lib/SelectionControls/Checkbox'; 6 | 7 | export default class CosmosDBConnection implements IConnection { 8 | type = 'cosmos-db'; 9 | params = ['host', 'key']; 10 | editor = CosmosDBConnectionEditor; 11 | } 12 | 13 | class CosmosDBConnectionEditor extends ConnectionEditor { 14 | 15 | render() { 16 | let { connection } = this.props; 17 | // connection = connection || {'ssl':true }; 18 | return ( 19 |
20 |

CosmosDB

21 | 27 | 32 | 33 | 42 | 51 |
52 | ); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /client/src/data-sources/connections/graphql.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { IConnection, ConnectionEditor, IConnectionProps } from './Connection'; 3 | import InfoDrawer from '../../components/common/InfoDrawer'; 4 | import TextField from 'react-md/lib/TextFields'; 5 | 6 | export default class GraphQLConnection implements IConnection { 7 | type = 'graphql'; 8 | params = [ 'serviceUrl' ]; 9 | editor = GraphQLConnectionEditor; 10 | } 11 | 12 | class GraphQLConnectionEditor extends ConnectionEditor { 13 | 14 | constructor(props: IConnectionProps) { 15 | super(props); 16 | 17 | this.onParamChange = this.onParamChange.bind(this); 18 | } 19 | 20 | onParamChange(value: string, event: any) { 21 | if (typeof this.props.onParamChange === 'function') { 22 | this.props.onParamChange(event.target.id, value); 23 | } 24 | } 25 | 26 | render() { 27 | 28 | let { connection } = this.props; 29 | connection = connection || {}; 30 | 31 | return ( 32 |
33 |

GraphQL Connection

34 | 40 |
41 | Just enter the URL of the GraphQL service you wish to query below. 42 | Currently only publicly accessible GraphQL endpoints are supported. 43 |
44 |
45 | 54 |
55 | ); 56 | } 57 | } -------------------------------------------------------------------------------- /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 | try { 15 | request( 16 | `${appInsightsUri}/query`, 17 | { 18 | method: 'POST', 19 | json: true, 20 | body: { 21 | apiKey: this.apiKey, 22 | appId: this.appId, 23 | query, 24 | }, 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 | const appInsightsUri = 'http://localhost:3000/applicationinsights'; 2 | 3 | export { 4 | appInsightsUri, 5 | } -------------------------------------------------------------------------------- /client/src/data-sources/plugins/BotFramework/DirectLine.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'xhr-request'; 2 | import { DataSourcePlugin, IOptions } from '../DataSourcePlugin'; 3 | import { DataSourceConnector } from '../../DataSourceConnector'; 4 | import BotFrameworkConnection from '../../connections/bot-framework'; 5 | 6 | let connectionType = new BotFrameworkConnection(); 7 | 8 | const DIRECT_LINE_URL = 'https://directline.botframework.com/v3/directline/conversations'; 9 | 10 | interface IQueryParams { 11 | calculated?: (results: any) => object; 12 | } 13 | 14 | export default class BotFrameworkDirectLine extends DataSourcePlugin { 15 | type = 'BotFramework-DirectLine'; 16 | defaultProperty = 'values'; 17 | connectionType = connectionType.type; 18 | 19 | constructor(options: IOptions, connections: IDict) { 20 | super(options, connections); 21 | this.validateTimespan(this._props); 22 | this.validateParams(this._props.params); 23 | } 24 | 25 | /** 26 | * update - called when dependencies are created 27 | * @param {object} dependencies 28 | * @param {function} callback 29 | */ 30 | dependenciesUpdated(dependencies: any) { 31 | let emptyDependency = false; 32 | Object.keys(this._props.dependencies).forEach((key) => { 33 | if (typeof dependencies[key] === 'undefined') { emptyDependency = true; } 34 | }); 35 | 36 | // If one of the dependencies is not supplied, do not run the query 37 | if (emptyDependency) { 38 | return (dispatch) => { 39 | return dispatch(); 40 | }; 41 | } 42 | 43 | // Validate connection 44 | let connection = this.getConnection() || {}; 45 | let { directLine } = connection; 46 | if (!connection || !directLine) { 47 | return (dispatch) => { 48 | return dispatch(); 49 | }; 50 | } 51 | 52 | const bearerToken = `Bearer ${directLine}`; 53 | 54 | return (dispatch) => { 55 | request(DIRECT_LINE_URL, { 56 | method: 'POST', 57 | json: true, 58 | headers: { 'Authorization' : bearerToken } 59 | }, (error: any, json: any) => { 60 | if (error) { 61 | throw new Error(error); 62 | } 63 | // returns conversationId, token, expires_in, streamUrl, referenceGrammarId 64 | return dispatch(json); 65 | }); 66 | }; 67 | } 68 | 69 | updateSelectedValues(dependencies: IDictionary, selectedValues: any) { 70 | if (Array.isArray(selectedValues)) { 71 | return Object.assign(dependencies, { selectedValues }); 72 | } else { 73 | return Object.assign(dependencies, { ... selectedValues }); 74 | } 75 | } 76 | 77 | private validateTimespan(props: any) { 78 | } 79 | 80 | private validateParams(params: IQueryParams): void { 81 | } 82 | 83 | } -------------------------------------------------------------------------------- /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 | 'BotFrameworkDirectLine': '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 = 'Constant'; 12 | defaultProperty = 'selectedValue'; 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 } = 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 | var result = _.extend(dependencies, args); 33 | 34 | if (typeof callback === 'function') { 35 | return callback(result); 36 | } 37 | 38 | return result; 39 | } 40 | 41 | updateSelectedValues(dependencies: IDictionary, selectedValues: any) { 42 | if (Array.isArray(selectedValues)) { 43 | return _.extend(dependencies, { 'selectedValues': selectedValues }); 44 | } else { 45 | return _.extend(dependencies, { ... selectedValues }); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /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/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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 | import { ApolloClient, ApolloProvider, createNetworkInterface, createBatchingNetworkInterface} from 'react-apollo'; 10 | import { makeExecutableSchema } from 'graphql-tools'; 11 | 12 | interface IDashboardState { 13 | dashboard?: IDashboardConfig; 14 | connections?: IConnections; 15 | connectionsMissing?: boolean; 16 | } 17 | 18 | const batchingNetworkInterface = createBatchingNetworkInterface({ 19 | uri: 'http://localhost:4000/apollo', 20 | batchInterval: 10, 21 | opts: { 22 | // Options to pass along to `fetch` 23 | } 24 | }); 25 | 26 | // const networkInterface = createNetworkInterface({ uri: 'http://localhost:4000/apollo' }); 27 | // const client = new ApolloClient({ networkInterface: networkInterface }); 28 | const client = new ApolloClient({ networkInterface: batchingNetworkInterface }); 29 | 30 | export default class Dashboard extends React.Component { 31 | 32 | state: IDashboardState = { 33 | dashboard: null, 34 | connections: {}, 35 | connectionsMissing: false 36 | }; 37 | 38 | constructor(props: any) { 39 | super(props); 40 | 41 | this.updateConfiguration = this.updateConfiguration.bind(this); 42 | } 43 | 44 | updateConfiguration(newState: IDashboardState) { 45 | this.setState(newState); 46 | } 47 | 48 | componentDidMount() { 49 | 50 | this.setState(ConfigurationsStore.getState()); 51 | ConfigurationsStore.listen(this.updateConfiguration); 52 | } 53 | 54 | componentWillUnmount() { 55 | ConfigurationsStore.unlisten(this.updateConfiguration); 56 | } 57 | 58 | render() { 59 | 60 | var { dashboard, connections, connectionsMissing } = this.state; 61 | 62 | if (!dashboard) { 63 | return null; 64 | } 65 | 66 | if (connectionsMissing) { 67 | return ( 68 | 69 | ); 70 | } 71 | 72 | return ( 73 | 74 | 75 | 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /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, '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 = alt.createStore(ConnectionsStore, 'ConnectionsStore'); 29 | 30 | export default connectionsStore; 31 | -------------------------------------------------------------------------------- /client/src/stores/FilterStore.ts: -------------------------------------------------------------------------------- 1 | import alt, { AbstractStoreModel } from '../alt'; 2 | 3 | import filterActions from '../actions/FilterActions'; 4 | 5 | interface IFilterState { 6 | filterId: string; 7 | values: string[]; 8 | } 9 | 10 | interface IFilterStoreState { 11 | filterState: IFilterState[]; 12 | } 13 | 14 | class FilterStore extends AbstractStoreModel implements IFilterStoreState { 15 | 16 | filterState: IFilterState[]; 17 | 18 | constructor() { 19 | super(); 20 | 21 | this.filterState = []; 22 | 23 | this.bindListeners({ 24 | updateItems: filterActions.filterChanged 25 | }); 26 | } 27 | 28 | updateItems(filter: any) { 29 | var newObj = { filterId: filter.filterId, values: filter.selectedValues }; 30 | for (let i = 0; i < this.filterState.length; i++) { 31 | if (this.filterState[i].filterId === filter.filterId) { 32 | this.filterState[i] = newObj; 33 | return; 34 | } 35 | } 36 | this.filterState.push(newObj); 37 | } 38 | } 39 | 40 | const filterStore = alt.createStore(FilterStore, 'FilterStore'); 41 | 42 | export default filterStore; 43 | -------------------------------------------------------------------------------- /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, '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, '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, 'VisibilityStore'); 29 | 30 | export default visibilityStore; 31 | -------------------------------------------------------------------------------- /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 | dataSources = setupTests(dashboardMock, done); 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": undefined, 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": undefined, 42 | }); 43 | 44 | }); 45 | 46 | }); 47 | -------------------------------------------------------------------------------- /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 | 5 | describe('Data Source: Samples', () => { 6 | 7 | let dataSources: IDataSourceDictionary; 8 | 9 | beforeAll((done) => { 10 | dataSources = setupTests(dashboardMock, done); 11 | }); 12 | 13 | it ('Check basic data == 3 rows', () => { 14 | 15 | expect(dataSources).toHaveProperty('samples'); 16 | expect(dataSources.samples).toHaveProperty('store'); 17 | expect(dataSources.samples).toHaveProperty('action'); 18 | expect(dataSources.samples.store).toHaveProperty('state', { 19 | values: [ 20 | { id: "value1", count: 60 }, 21 | { id: "value2", count: 10 }, 22 | { id: "value3", count: 30 } 23 | ] 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /client/src/tests/data-sources/plugins/Constant/Settings.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import Card from 'react-md/lib/Cards/Card'; 4 | import SelectField from 'react-md/lib/SelectFields'; 5 | import TokenInput from '../../../../components/common/TokenInput'; 6 | import { setupTests } from '../../../utils/setup'; 7 | import * as TestUtils from 'react-addons-test-utils'; 8 | import { DataSourceConnector, IDataSourceDictionary } from '../../../../data-sources'; 9 | import plugins from '../../../../data-sources/plugins'; 10 | import { BaseDataSourceSettings, IBaseSettingsProps, IBaseSettingsState } 11 | from '../../../../components/common/BaseDatasourceSettings'; 12 | 13 | import ConstantDatasourceSettings from '../../../../data-sources/plugins/Constant/Settings' 14 | import dashboardMock, {dashboard2} from '../../../mocks/dashboards/constants'; 15 | 16 | describe('testing data-source settings component', () => { 17 | 18 | it('initializing via editor', () => { 19 | let element = dashboard2.dataSources[0]; 20 | let ReactElementClass = plugins[element.type]; 21 | if (ReactElementClass && ReactElementClass.editor) { 22 | let SettingsEditor: any = ReactElementClass.editor; 23 | let settingsEditor = TestUtils.renderIntoDocument(); 24 | expect(settingsEditor).toBeTruthy(); 25 | } 26 | }); 27 | 28 | it('Selected value displays correctly', () => { 29 | let element = dashboard2.dataSources[0]; 30 | let ReactElementClass = plugins[element.type]; 31 | if (ReactElementClass && ReactElementClass.editor) { 32 | let SettingsEditor = ReactElementClass.editor; 33 | let settingsEditor: any = TestUtils.renderIntoDocument(); 34 | let elements = TestUtils.scryRenderedComponentsWithType(settingsEditor, SelectField); 35 | expect(elements.length).toBeGreaterThan(0); 36 | var s:any = elements[0].state; 37 | expect(s.activeLabel).toEqual(element.params['selectedValue']); 38 | } 39 | }); 40 | 41 | it('Values were passed correctly to token input', () => { 42 | let element = dashboard2.dataSources[0]; 43 | let ReactElementClass = plugins[element.type]; 44 | if (ReactElementClass && ReactElementClass.editor) { 45 | let SettingsEditor = ReactElementClass.editor; 46 | let settingsEditor: any = TestUtils.renderIntoDocument(); 47 | let elements = TestUtils.scryRenderedComponentsWithType(settingsEditor, TokenInput); 48 | expect(elements.length).toEqual(1); 49 | expect(elements[0].props.tokens).toEqual(element.params['values']); 50 | } 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /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-addons-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 | /* 27 | it('First render without content', () => { 28 | dialog = TestUtils.renderIntoDocument(); 29 | let elements = TestUtils.scryRenderedComponentsWithType(dialog, MDDialog); 30 | expect(elements.length).toBe(0); 31 | }); 32 | 33 | it('Opening a dialog', function () { 34 | DialogsActions.openDialog(dialogData.id, { title: 'Title', intent: 'Intent', queryspan: '30D' }); 35 | let elements = TestUtils.scryRenderedComponentsWithType(dialog, MDDialog); 36 | expect(elements.length).toBe(1); 37 | }); 38 | 39 | */ 40 | 41 | it('Closing a dialog', function () { 42 | DialogsActions.closeDialog(); 43 | let elements = TestUtils.scryRenderedComponentsWithType(dialog, MDDialog); 44 | expect(elements.length).toBe(0); 45 | }); 46 | 47 | afterAll(() => { 48 | ReactDOM.unmountComponentAtNode(dialog); 49 | }) 50 | }); -------------------------------------------------------------------------------- /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-addons-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(); 24 | 25 | let progress = TestUtils.scryRenderedComponentsWithType(spinner, CircularProgress); 26 | expect(progress.length).toBe(1); 27 | }); 28 | 29 | it ('Stop page loading', () => { 30 | SpinnerActions.endPageLoading(); 31 | 32 | let progress = TestUtils.scryRenderedComponentsWithType(spinner, CircularProgress); 33 | expect(progress.length).toBe(0); 34 | }); 35 | 36 | it ('Start request loading', () => { 37 | SpinnerActions.startRequestLoading(); 38 | 39 | let progress = TestUtils.scryRenderedComponentsWithType(spinner, CircularProgress); 40 | expect(progress.length).toBe(1); 41 | }); 42 | 43 | it ('Start request loading', () => { 44 | SpinnerActions.endRequestLoading(); 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/SplitPanel.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import * as TestUtils from 'react-addons-test-utils'; 4 | 5 | import { Card } from 'react-md/lib/Cards'; 6 | import { Spinner, SpinnerActions } from '../../components/Spinner'; 7 | import List from 'react-md/lib/Lists/List'; 8 | import SplitPanel from '../../components/generic/SplitPanel'; 9 | import Table from '../../components/generic/Table'; 10 | import { DataTable, TableRow } from 'react-md/lib/DataTables'; 11 | import { DataSourceConnector, IDataSourceDictionary } from '../../data-sources'; 12 | 13 | //import dataSourceMock from '../mocks/dataSource'; 14 | import dashboardMock from '../mocks/dashboards/splitpanel'; 15 | 16 | describe('SplitPanel', () => { 17 | 18 | let dataSources: IDataSourceDictionary = {}; 19 | let splitpanel; 20 | 21 | beforeAll((done) => { 22 | 23 | DataSourceConnector.createDataSources(dashboardMock, dashboardMock.config.connections); 24 | dataSources = DataSourceConnector.getDataSources(); 25 | 26 | let {id, dependencies, actions, props, title, subtitle } = dashboardMock.elements[0]; 27 | let atts = {id, dependencies, actions, props, title, subtitle }; 28 | splitpanel = TestUtils.renderIntoDocument(); 29 | TestUtils.isElementOfType(splitpanel, 'div'); 30 | 31 | setTimeout(done, 10); 32 | }) 33 | 34 | it('Render inside a Card (+ Table inside a Card)', () => { 35 | let card = TestUtils.scryRenderedComponentsWithType(splitpanel, Card); 36 | expect(card).toHaveLength(2); 37 | }); 38 | 39 | it('Render a List entity', () => { 40 | let list = TestUtils.scryRenderedComponentsWithType(splitpanel, List); 41 | expect(list).toHaveLength(1); 42 | }); 43 | 44 | it('Render a Table entity', () => { 45 | let table = TestUtils.scryRenderedComponentsWithType(splitpanel, Table); 46 | expect(table).toHaveLength(1); 47 | }); 48 | 49 | it('Rows == 4', () => { 50 | let rows = TestUtils.scryRenderedComponentsWithType(splitpanel, TableRow); 51 | expect(rows).toHaveLength(4); 52 | }); 53 | 54 | it('Rows == 0', () => { 55 | dataSources['samples'].action.updateDependencies({ 56 | values: [] 57 | }); 58 | let rows = TestUtils.scryRenderedComponentsWithType(splitpanel, Table); 59 | expect(rows).toHaveLength(0); 60 | }); 61 | 62 | afterAll(() => { 63 | ReactDOM.unmountComponentAtNode(splitpanel); 64 | }) 65 | }); -------------------------------------------------------------------------------- /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-addons-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 | let card = TestUtils.scryRenderedComponentsWithType(table, Card); 34 | expect(card.length).toBe(1); 35 | }); 36 | 37 | it('Render a Data Table entity', () => { 38 | let progress = TestUtils.scryRenderedComponentsWithType(table, DataTable); 39 | expect(progress.length).toBe(1); 40 | }); 41 | 42 | it('Rows == 4', () => { 43 | let rows = TestUtils.scryRenderedComponentsWithType(table, TableRow); 44 | expect(rows.length).toBe(4); 45 | }); 46 | 47 | it('Rows == 0', () => { 48 | dataSources['samples'].action.updateDependencies({ 49 | values: [] 50 | }); 51 | let rows = TestUtils.scryRenderedComponentsWithType(table, TableRow); 52 | expect(rows.length).toBe(1); 53 | }); 54 | 55 | afterAll(() => { 56 | ReactDOM.unmountComponentAtNode(table); 57 | }) 58 | }); -------------------------------------------------------------------------------- /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-addons-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.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/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 | selectedValue: 'default' 14 | }, 15 | calculated: (state, dependencies) => { 16 | 17 | someJsonValues.push({ id: 3, count: 10 }); 18 | return { someJsonValues }; 19 | } 20 | }); 21 | 22 | export let dashboard2 = createDashboard(); 23 | dashboard2.dataSources.push( 24 | { 25 | id: "timespan", 26 | type: "Constant", 27 | params: { values: ["24 hours","1 week","1 month","3 months"],selectedValue: "1 month" }, 28 | calculated: (state, dependencies) => { 29 | var queryTimespan = 30 | state.selectedValue === '24 hours' ? 'PT24H' : 31 | state.selectedValue === '1 week' ? 'P7D' : 32 | state.selectedValue === '1 month' ? 'P30D' : 33 | 'P90D'; 34 | var granularity = 35 | state.selectedValue === '24 hours' ? '5m' : 36 | state.selectedValue === '1 week' ? '1d' : '1d'; 37 | 38 | return { queryTimespan, granularity }; 39 | } 40 | } 41 | ); 42 | 43 | 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 | visualDialog: [], 18 | }; -------------------------------------------------------------------------------- /client/src/tests/mocks/dashboards/dialogs.ts: -------------------------------------------------------------------------------- 1 | import { createDashboard } from './utils'; 2 | 3 | let dashboard = createDashboard(); 4 | dashboard.visualDialog.push({ 5 | id: "dialog2", 6 | size:{w:2, h:2}, 7 | visual: [], 8 | }); 9 | 10 | dashboard.dialogs.push({ 11 | id: "conversations", 12 | width: '60%', 13 | params: [ 'title', 'intent', 'queryspan' ], 14 | dataSources: [ 15 | { 16 | id: 'timespan', 17 | type: 'Constant', 18 | params: { 19 | selectedValue: 'default' 20 | }, 21 | calculated: (state, dependencies) => { 22 | 23 | var someJsonValues = [ 24 | { 25 | id: 1, 26 | count: 2, 27 | }, 28 | { 29 | id: 2, 30 | count: 0, 31 | }, 32 | { 33 | id: 3, 34 | count: 10, 35 | } 36 | ]; 37 | 38 | return { someJsonValues }; 39 | } 40 | } 41 | ], 42 | elements: [ 43 | { 44 | id: 'conversations-list', 45 | type: 'Table', 46 | title: 'Conversations', 47 | size: { w: 12, h: 16}, 48 | dependencies: { values: 'timespan:someJsonValues' }, 49 | props: { 50 | cols: [{ 51 | header: 'Conversation Id', 52 | field: 'id' 53 | }, { 54 | header: 'Count', 55 | field: 'count' 56 | }, { 57 | type: 'button', 58 | value: 'chat', 59 | onClick: 'openMessagesDialog' 60 | }] 61 | } 62 | } 63 | ] 64 | }); 65 | 66 | 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/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 | groups: [ 11 | { title: "value1", subtitle: "subvalue1", count: 60 }, 12 | { title: "value2", subtitle: "subvalue2", count: 60 }, 13 | ], 14 | values: [ 15 | { id: "value1", count: 60 }, 16 | { id: "value2", count: 10 }, 17 | { id: "value3", count: 30 } 18 | ] 19 | } 20 | } 21 | } 22 | ); 23 | dashboard.elements.push( 24 | { 25 | id: "splitpanel", 26 | type: "SplitPanel", 27 | title: "Values", 28 | size: { w: 12,h: 16 }, 29 | dependencies: { groups: "samples:groups", values: "samples:values" }, 30 | props: { 31 | group: { field: "title",secondaryField: "subtitle", countField: "count" }, 32 | cols: [ 33 | { header: "Id",field: "id",secondaryHeader: "Repeat Id",secondaryField: "id" }, 34 | { header: "Count",field: "count" }, 35 | ] 36 | }, 37 | actions: { 38 | select: { 39 | action: "samples:updateDependencies", 40 | params: { title: "args:title",type: "args:title" } 41 | } 42 | } 43 | } 44 | ); 45 | 46 | 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: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/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 | selectedValue: 'default' 11 | }, 12 | calculated: (state, dependencies) => { 13 | 14 | var someJsonValues = [ 15 | { 16 | id: 1, 17 | count: 2, 18 | }, 19 | { 20 | id: 2, 21 | count: 0, 22 | }, 23 | { 24 | id: 3, 25 | count: 10, 26 | } 27 | ]; 28 | 29 | return { someJsonValues }; 30 | } 31 | } 32 | ], 33 | elements: [ 34 | { 35 | id: 'conversations-list', 36 | type: 'Table', 37 | title: 'Conversations', 38 | size: { w: 12, h: 16}, 39 | dependencies: { values: 'timespan:someJsonValues' }, 40 | props: { 41 | cols: [{ 42 | header: 'Conversation Id', 43 | field: 'id' 44 | }, { 45 | header: 'Count', 46 | field: 'count' 47 | }, { 48 | type: 'button', 49 | value: 'chat', 50 | onClick: 'openMessagesDialog' 51 | }] 52 | } 53 | } 54 | ] 55 | } -------------------------------------------------------------------------------- /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 | .reply(200, { 8 | account: 'account' 9 | }); 10 | } 11 | export { 12 | mockRequests 13 | }; -------------------------------------------------------------------------------- /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 | (function (window) { 24 | var dashboard = (function () { 25 | return ${JSON.stringify(dashboard)}; 26 | })(); 27 | window.dashboard = dashboard || null; 28 | })(window); 29 | `); 30 | } 31 | export { 32 | mockRequests 33 | }; -------------------------------------------------------------------------------- /client/src/tests/stores/Account.test.ts: -------------------------------------------------------------------------------- 1 | import AccountStore from "../../stores/AccountStore"; 2 | import AccountActions from "../../actions/AccountActions"; 3 | import { mockRequests } from '../mocks/requests/account'; 4 | 5 | describe('Data Source: Samples', () => { 6 | 7 | beforeAll(() => { 8 | mockRequests(); 9 | }) 10 | 11 | it ('Testing AccountActions', (done) => { 12 | 13 | AccountStore.listen((state) => { 14 | expect(state).toHaveProperty('account'); 15 | done(); 16 | }); 17 | AccountActions.updateAccount(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /client/src/tests/stores/Configuration.test.ts: -------------------------------------------------------------------------------- 1 | import ConfigurationsStore from "../../stores/ConfigurationsStore"; 2 | import ConfigurationsActions from "../../actions/ConfigurationsActions"; 3 | import { mockRequests } from '../mocks/requests/configuration'; 4 | 5 | describe('Data Source: ConfigurationsActions', () => { 6 | 7 | beforeAll(() => { 8 | mockRequests(); 9 | }) 10 | 11 | it ('Testing load', (done) => { 12 | 13 | ConfigurationsStore.listen((state) => { 14 | 15 | try { 16 | expect(state).toHaveProperty('dashboards'); 17 | expect(state).toHaveProperty('templates'); 18 | done(); 19 | } catch (e) { 20 | done(e); 21 | throw e; 22 | } 23 | }); 24 | ConfigurationsActions.loadConfiguration(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /client/src/tests/utils/rewire-module.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this method to rewire a module to render children other than 3 | * it's native components. (For shalow testing) 4 | * 5 | * @link http://stackoverflow.com/questions/32057745/using-scryrenderedcomponentswithtype-or-findrenderedcomponentwithtype 6 | * 7 | * @param rewiredModule - The React module to rewire 8 | * @param varValues - The new component to use 9 | */ 10 | function rewireModule (rewiredModule, varValues) { 11 | let rewiredReverts = []; 12 | 13 | beforeEach(() => { 14 | let key, value, revert; 15 | for (key in varValues) { 16 | if (varValues.hasOwnProperty(key)) { 17 | value = varValues[key]; 18 | revert = rewiredModule.__set__(key, value); 19 | rewiredReverts.push(revert); 20 | } 21 | } 22 | }); 23 | 24 | afterEach(() => { 25 | rewiredReverts.forEach(revert => revert()); 26 | }); 27 | 28 | return rewiredModule; 29 | } 30 | 31 | export { rewireModule }; -------------------------------------------------------------------------------- /client/src/tests/utils/setup.ts: -------------------------------------------------------------------------------- 1 | import { DataSourceConnector, IDataSourceDictionary } from '../../data-sources'; 2 | 3 | function setupTests(dashboardMock: IDashboardConfig, done?: () => void): IDataSourceDictionary { 4 | DataSourceConnector.createDataSources(dashboardMock, dashboardMock.config.connections); 5 | let dataSources = DataSourceConnector.getDataSources(); 6 | 7 | // Waiting for all defered functions to complete their execution 8 | done && setTimeout(done, 100); 9 | 10 | return dataSources; 11 | } 12 | 13 | export { 14 | setupTests 15 | }; -------------------------------------------------------------------------------- /client/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import * as moment from 'moment'; 2 | 3 | export default { 4 | kmNumber: (num: number): string => { 5 | if (isNaN(num)) { return ''; } 6 | 7 | return ( 8 | num > 999999 ? 9 | (num / 1000000).toFixed(1) + 'M' : 10 | num > 999 ? 11 | (num / 1000).toFixed(1) + 'K' : 12 | (num % 1 * 10) !== 0 ? 13 | num.toFixed(1).toString() : num.toString()); 14 | }, 15 | 16 | ago: (date: Date): string => { 17 | return moment(date).fromNow(); 18 | } 19 | }; -------------------------------------------------------------------------------- /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/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-react"], 3 | "rules": { 4 | "align": [ 5 | true, 6 | "parameters", 7 | "arguments", 8 | "statements" 9 | ], 10 | "ban": false, 11 | "class-name": true, 12 | "comment-format": [ 13 | true, 14 | "check-space" 15 | ], 16 | "curly": true, 17 | "eofline": false, 18 | "forin": true, 19 | "indent": [ true, "spaces" ], 20 | "interface-name": [true], 21 | "jsdoc-format": true, 22 | "jsx-alignment": false, 23 | "jsx-no-lambda": false, 24 | "jsx-no-multiline-js": false, 25 | "jsx-boolean-value": false, 26 | "label-position": true, 27 | "max-line-length": [ true, 120 ], 28 | "member-ordering": [ 29 | true, 30 | "public-before-private", 31 | "static-before-instance", 32 | "variables-before-functions" 33 | ], 34 | "no-any": false, 35 | "no-arg": true, 36 | "no-bitwise": true, 37 | "no-console": [ 38 | true, 39 | "log", 40 | "error", 41 | "debug", 42 | "info", 43 | "time", 44 | "timeEnd", 45 | "trace" 46 | ], 47 | "no-consecutive-blank-lines": [true], 48 | "no-construct": true, 49 | "no-debugger": true, 50 | "no-duplicate-variable": true, 51 | "no-empty": false, 52 | "no-eval": true, 53 | "no-shadowed-variable": true, 54 | "no-string-literal": false, 55 | "no-switch-case-fall-through": true, 56 | "no-trailing-whitespace": false, 57 | "no-unused-expression": true, 58 | "no-use-before-declare": false, 59 | "one-line": [ 60 | true, 61 | "check-catch", 62 | "check-else", 63 | "check-open-brace", 64 | "check-whitespace" 65 | ], 66 | "quotemark": [true, "single", "jsx-double"], 67 | "radix": true, 68 | "semicolon": [true, "always"], 69 | "switch-default": true, 70 | 71 | "trailing-comma": [false], 72 | 73 | "triple-equals": [ true, "allow-null-check" ], 74 | "typedef": [ 75 | true, 76 | "parameter", 77 | "property-declaration" 78 | ], 79 | "typedef-whitespace": [ 80 | true, 81 | { 82 | "call-signature": "nospace", 83 | "index-signature": "nospace", 84 | "parameter": "nospace", 85 | "property-declaration": "nospace", 86 | "variable-declaration": "nospace" 87 | } 88 | ], 89 | "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore", "allow-pascal-case"], 90 | "whitespace": [ 91 | true, 92 | "check-branch", 93 | "check-decl", 94 | "check-module", 95 | "check-operator", 96 | "check-separator", 97 | "check-type", 98 | "check-typecast" 99 | ] 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /docs/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/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/bot-framedash-msgs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/ibex-dashboard-apollo-graphql/60e0b2174a54d376aea6541aeebb931f397172af/docs/bot-framedash-msgs.png -------------------------------------------------------------------------------- /docs/bot-framedash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/ibex-dashboard-apollo-graphql/60e0b2174a54d376aea6541aeebb931f397172af/docs/bot-framedash.png -------------------------------------------------------------------------------- /docs/bot-framework-apollo-gql-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/ibex-dashboard-apollo-graphql/60e0b2174a54d376aea6541aeebb931f397172af/docs/bot-framework-apollo-gql-preview.png -------------------------------------------------------------------------------- /docs/cosmos-db.md: -------------------------------------------------------------------------------- 1 | # CosmosDB 2 | 3 | CosmosDB can be added to the list of required connections. 4 | 5 | ## Config 6 | To enable [Cosmos DB](https://azure.microsoft.com/en-us/services/cosmos-db/) data sources add 'cosmos-db' to the connections config. The host name and password key are required values. 7 | 8 | ```js 9 | connections: { 10 | 'cosmos-db': { 11 | 'host': "", 12 | 'key': "" 13 | } 14 | } 15 | ``` 16 | 17 | NB. The host name excludes the '.documents.azure.com' suffix. 18 | 19 | ## Data Sources 20 | | Property | Type | Value | Description 21 | | :--------|:-----|:------|:------------ 22 | | `id`| `string` || ID of the element 23 | | `type`| `string` | "CosmosDB/Query" | Data source plugin name 24 | | `dependencies`| `object` || A collection of key values referenced by queries 25 | | `params`| `object` || Contains `databaseId`, `collectionId`, `query` and `parameters` 26 | | `calculated` | `function` || Result contains array of `Documents`, `_rid` and `_count` properties. 27 | 28 | ## Params 29 | | Property | Type | Description 30 | | :--------|:-----|:------------ 31 | | `databaseId`| `string` | Database Id (default is 'admin') 32 | | `collectionId`| `string` | Collection Id 33 | | `query`| `string` | SQL query string 34 | | `parameters`| `object[]` | Parameterized SQL request 35 | 36 | More info about SQL `query` string and `parameters` are available in the [CosmosDB documentation](https://docs.microsoft.com/en-us/rest/api/documentdb/querying-documentdb-resources-using-the-rest-api). You can try out Cosmos DB queries using the *Query Explorer* in the [Azure portal](https://portal.azure.com/), or learn using the [SQL demo](https://www.documentdb.com/sql/demo). 37 | 38 | ## Sample data source 39 | ```js 40 | { 41 | id: "botConversations", 42 | type: "CosmosDB/Query", 43 | dependencies: { timespan: "timespan", queryTimespan: "timespan:queryTimespan" }, 44 | params: { 45 | databaseId: "admin", 46 | collectionId: "conversations", 47 | query: () => `SELECT * FROM conversations WHERE (conversations.state = 0)`, 48 | parameters: [] 49 | }, 50 | calculated: (result) => { 51 | return result; 52 | } 53 | } 54 | ``` 55 | 56 | ## Sample element 57 | 58 | ``` 59 | { 60 | id: "conversations", 61 | type: "Scorecard", 62 | title: "Conversations", 63 | subtitle: "Total conversations", 64 | size: { w: 4, h: 3 }, 65 | dependencies: { 66 | card_conversations_value: "botConversations:_count", 67 | card_conversations_heading: "::Conversations", 68 | card_conversations_icon: "::chat" 69 | } 70 | } 71 | ``` -------------------------------------------------------------------------------- /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/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/filter.md: -------------------------------------------------------------------------------- 1 | # Filter 2 | 3 | There are 2 basic types of filter control: 4 | 1. Single selection 5 | 2. Multi-selection 6 | 7 | ## Single selection: 8 | 9 | The **TextFilter** component is used for single selection. 10 | 11 | | Control | UI Style | Description 12 | | :-------|:---------|:----------- 13 | | `TextFilter` | Select menu | Single selection menu 14 | 15 | ## Multi-selection: 16 | 17 | The **MenuFilter** or **CheckboxFilter** components can be used for multiple selection. 18 | 19 | | Control | UI Style | Description 20 | | :-------|:---------|:----------- 21 | | `MenuFilter` | Select menu | Multi-select menu with checkbox list item controls 22 | | `CheckboxFilter` | Checkboxes | Multi-select checkboxes 23 | 24 | ## Single selection properties 25 | 26 | | Property | Type | Description 27 | | :--------|:-----|:----------- 28 | | `type`| `string` | Use 'TextFilter' 29 | | `dependencies`| `object` | Dependencies required by component 30 | | `actions`| `object` | Contains an `onChange` action defination with a referenced dependency `string` used to update the selected value 31 | | `first`| `boolean` | Declare as primary filter 32 | 33 | ## Single selection dependencies 34 | 35 | Define `dependencies` as follows: 36 | 37 | | Property | Type | Description 38 | | :--------|:-----|:----------- 39 | | `selectedValue`| `string` | Reference to the selected value 40 | | `values`| `string` | Reference to option values 41 | 42 | 43 | #### Single selection sample 44 | 45 | ```js 46 | { 47 | type: "TextFilter", 48 | dependencies: { 49 | selectedValue: "timespan", 50 | values: "timespan:values" 51 | }, 52 | actions: { 53 | onChange: "timespan:updateSelectedValue" 54 | }, 55 | first: true 56 | }, 57 | ``` 58 | 59 | ## Multi-selection properties 60 | 61 | | Property | Type | Description 62 | | :--------|:-----|:----------- 63 | | `type`| `string` | Use either 'MenuFilter', 'CheckboxFilter' 64 | | `dependencies`| `object` | Dependencies required by component 65 | | `actions`| `object` | Contains an `onChange` action defination with a referenced dependency `string` used to update the selected values 66 | | `first`| `boolean` | Declare as primary filter 67 | | `title`| `string` | Primary text label for control 68 | | `subtitle`| `string` | Secondary text label used as selected prompt 69 | 70 | ## Multi-selection dependencies 71 | 72 | Define `dependencies` as follows: 73 | 74 | | Property | Type | Description 75 | | :--------|:-----|:----------- 76 | | `selectedValues`| `string` | Reference to the selected values 77 | | `values`| `string` | Reference to option values 78 | 79 | #### Multi-selection sample (using a forked data source) 80 | 81 | ```js 82 | { 83 | type: "MenuFilter", 84 | title: "Channels", 85 | subtitle: "Select channels", 86 | dependencies: { 87 | selectedValues: "filters:channels-selected", 88 | values: "filters:channels-filters" 89 | }, 90 | actions: { 91 | onChange: "filters:updateSelectedValues:channels-selected" 92 | }, 93 | first: true 94 | }, 95 | ``` -------------------------------------------------------------------------------- /docs/pie-settings.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/ibex-dashboard-apollo-graphql/60e0b2174a54d376aea6541aeebb931f397172af/docs/pie-settings.PNG -------------------------------------------------------------------------------- /docs/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/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/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/template-for-sample.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CatalystCode/ibex-dashboard-apollo-graphql/60e0b2174a54d376aea6541aeebb931f397172af/docs/template-for-sample.PNG -------------------------------------------------------------------------------- /docs/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/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": "0.1.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 | } 17 | -------------------------------------------------------------------------------- /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": "catalystcode/ibex-dashboard" 55 | } 56 | } 57 | ] 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /server/apollo/appInsightsConvertors/ai-to-graph-ds.js: -------------------------------------------------------------------------------- 1 | // Transformation layer: Transforms the heavy AI json results to graphql return types 2 | 3 | const toIdsAndValuesArrays = (appInsightsQueryResult) => { 4 | var jsonObj = JSON.parse(appInsightsQueryResult); 5 | var ids = []; 6 | var values = []; 7 | var res = [] 8 | for (var i = 0; i < jsonObj.Tables[0].Rows.length; i++) { 9 | res.push({ id: jsonObj.Tables[0].Rows[i][1], name: jsonObj.Tables[0].Rows[i][0] }); 10 | } 11 | return res; 12 | }; 13 | 14 | const toLineChart = (appInsightsQueryResult) => { 15 | var countColumn = 3; 16 | var groupColumn = 0; 17 | var channelColumn = 2; 18 | var nameColumn = 1; 19 | var jsonObj = JSON.parse(appInsightsQueryResult); 20 | 21 | var seriesArray = {}; 22 | for (var i = 0; i < jsonObj.Tables[0].Rows.length; i++) { 23 | var channelName = jsonObj.Tables[0].Rows[i][channelColumn]; 24 | if (!seriesArray[channelName]) { 25 | seriesArray[channelName] = {}; 26 | seriesArray[channelName] = { label: channelName, x_values: [], y_values: [] }; 27 | } 28 | 29 | seriesArray[channelName].x_values.push(jsonObj.Tables[0].Rows[i][groupColumn]); 30 | seriesArray[channelName].y_values.push(jsonObj.Tables[0].Rows[i][countColumn]); 31 | 32 | } 33 | 34 | var allSeries = []; 35 | for (var key in seriesArray) { 36 | allSeries.push(seriesArray[key]); 37 | } 38 | 39 | return { seriesData: allSeries }; 40 | }; 41 | 42 | const toSentimentFormat = (appInsightsQueryResult) => { 43 | 44 | var jsonObj = JSON.parse(appInsightsQueryResult); 45 | var sentiments = []; 46 | sentiments.push({ label: 'Positive', value: Math.round(jsonObj.Tables[0].Rows[0] * 100) }); 47 | sentiments.push({ label: 'Negative', value: Math.round((1 - jsonObj.Tables[0].Rows[0]) * 100) }); 48 | 49 | return sentiments; 50 | }; 51 | 52 | const toPieChartData = (appInsightsQueryResult) => { 53 | var jsonObj = JSON.parse(appInsightsQueryResult); 54 | 55 | var labels = []; 56 | var values = []; 57 | for (var i = 0; i < jsonObj.Tables[0].Columns.length; i++) { 58 | 59 | values.push(jsonObj.Tables[0].Rows[0][i]); 60 | labels.push(jsonObj.Tables[0].Columns[i].ColumnName); 61 | 62 | } 63 | 64 | var countColumn = 3; 65 | var timeColumn = 0; 66 | var channelColumn = 2; 67 | var nameColumn = 1; 68 | 69 | return { labels: labels, values: values }; 70 | }; 71 | 72 | module.exports = { 73 | toIdsAndValuesArrays, 74 | toLineChart, 75 | toSentimentFormat, 76 | toPieChartData, 77 | }; -------------------------------------------------------------------------------- /server/apollo/resolvers/index.js: -------------------------------------------------------------------------------- 1 | const { channelsQuery, pieChartQuery, lineChartQuery, barChartQuery } = require('./queries/resolversRedirect'); 2 | 3 | const resolvers = { 4 | Query: { 5 | lineCharts: lineChartQuery, 6 | pieCharts: pieChartQuery, 7 | channels: channelsQuery, 8 | barCharts: barChartQuery, 9 | }, 10 | }; 11 | 12 | module.exports = { 13 | resolvers, 14 | }; 15 | -------------------------------------------------------------------------------- /server/apollo/resolvers/queries/localFile.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const getDataFromFile = (fileName, query, filterKey, filterValues) => { 5 | return new Promise((resolve, reject) => { 6 | var fileContent = JSON.parse(fs.readFileSync(path.join(__dirname, '../../../', 'sampleData', fileName), 'utf8')); 7 | 8 | // if filters required, lets filter by the channels. this is a hack just for the purpose of the sample data 9 | // and it looks for the label in seriesData, so other datasources will not be filtered.. 10 | if (filterValues && filterValues.length > 0) { 11 | if (fileContent[query][0].seriesData) { 12 | var newValues = JSON.parse(JSON.stringify(fileContent[query][0].seriesData)); 13 | for (let i = fileContent[query][0].seriesData.length - 1; i >= 0; i--) { 14 | if (filterValues.indexOf(fileContent[query][0].seriesData[i].label) === -1) { 15 | newValues.splice(i, 1); 16 | } 17 | } 18 | resolve([ {seriesData: newValues }]); 19 | } 20 | } 21 | 22 | resolve(fileContent[query]); 23 | }); 24 | }; 25 | 26 | module.exports = { 27 | getDataFromFile, 28 | }; 29 | -------------------------------------------------------------------------------- /server/apollo/resolvers/queries/resolversRedirect.js: -------------------------------------------------------------------------------- 1 | const localFileResolvers = require('./localFile.js'); 2 | const aiResolvers = require('./ai.js'); 3 | 4 | const runQueryGeneric = (method, source, query, appId, apiKey, filterKey, filterValues) => { 5 | var prefix = 'file::'; 6 | var fileNameIndex = source.indexOf(prefix) + prefix.length; 7 | if (source === 'AI') { 8 | return aiResolvers[method](query, appId, apiKey, filterKey, filterValues); 9 | } else if (fileNameIndex > -1) { 10 | var fileName = source.substr(fileNameIndex); 11 | console.log('Using local file as DB: ' + fileName); 12 | return localFileResolvers.getDataFromFile(fileName, query, filterKey, filterValues); 13 | } 14 | }; 15 | 16 | const channelsQuery = (root, { source, query, appId, apiKey, filterKey, filterValues }) => { 17 | return runQueryGeneric('channelsQuery', source, query, appId, apiKey, filterKey, filterValues); 18 | }; 19 | 20 | const lineChartQuery = (root, { source, query, appId, apiKey, filterKey, filterValues }) => { 21 | return runQueryGeneric('lineChartQuery', source, query, appId, apiKey, filterKey, filterValues); 22 | }; 23 | 24 | const pieChartQuery = (root, { source, query, appId, apiKey, filterKey, filterValues }) => { 25 | return runQueryGeneric('pieChartQuery', source, query, appId, apiKey, filterKey, filterValues); 26 | }; 27 | 28 | const barChartQuery = (root, { source, query, appId, apiKey, filterKey, filterValues }) => { 29 | return runQueryGeneric('barChartQuery', source, query, appId, apiKey, filterKey, filterValues); 30 | }; 31 | 32 | module.exports = { 33 | channelsQuery, 34 | lineChartQuery, 35 | pieChartQuery, 36 | barChartQuery, 37 | }; 38 | -------------------------------------------------------------------------------- /server/apollo/schema.js: -------------------------------------------------------------------------------- 1 | const { makeExecutableSchema } = require('graphql-tools'); 2 | const { resolvers } = require('./resolvers'); 3 | const { typeDefs } = require('./typeDefs'); 4 | 5 | const schema = makeExecutableSchema({ typeDefs, resolvers }); 6 | 7 | module.exports = { 8 | schema, 9 | }; 10 | -------------------------------------------------------------------------------- /server/apollo/typeDefs.js: -------------------------------------------------------------------------------- 1 | // todo: this could be extracted to a module shared between client and server 2 | 3 | const typeDefs = ` 4 | 5 | type Query { 6 | channels(source: String!, query:String!, appId:String!, apiKey:String!): [Channel] 7 | lineCharts(source: String!, query:String!, appId:String!, apiKey:String!, filterKey:String, filterValues:[String]): [LineChart] 8 | pieCharts(source: String!, query:String!, appId:String!, apiKey:String!): [PieChart] 9 | barCharts(source: String!, query:String!, appId:String!, apiKey:String!, filterKey:String, filterValues:[String]): [BarChart] 10 | } 11 | 12 | type Channel { 13 | name: String 14 | id: Int 15 | } 16 | 17 | type LineChart { 18 | id: String 19 | seriesData : [Series] 20 | } 21 | 22 | type PieChart { 23 | id: String 24 | labels: [String] 25 | values: [Int] 26 | } 27 | 28 | type BarChart { 29 | id: String 30 | seriesData : [Series] 31 | } 32 | 33 | type Series { 34 | label: String 35 | x_values: [String] 36 | y_values: [Int] 37 | } 38 | `; 39 | 40 | module.exports = { 41 | typeDefs, 42 | }; 43 | -------------------------------------------------------------------------------- /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 cors = require('cors'); 10 | const { graphqlExpress, graphiqlExpress } = require('graphql-server-express'); 11 | const { schema } = require('./apollo/schema'); 12 | const authRouter = require('./routes/auth'); 13 | const apiRouter = require('./routes/api'); 14 | const graphQLRouter = require('./routes/graphql'); 15 | const cosmosDBRouter = require('./routes/cosmos-db'); 16 | const azureRouter = require('./routes/azure'); 17 | const appInsightsRouter = require('./routes/application-insights'); 18 | 19 | const app = express(); 20 | 21 | 22 | app.use((req, res, next) => { 23 | console.log(`Request URL: ${req.url}`); 24 | return next(); 25 | }); 26 | 27 | app.use(cookieParser()); 28 | app.use(expressSession({ secret: 'keyboard cat', resave: true, saveUninitialized: false })); 29 | app.use(bodyParser.json()); 30 | app.use(bodyParser.urlencoded({ extended: true })); 31 | 32 | // Setup apollo 33 | app.use('*', cors({ origin: '*' })); 34 | app.use('/apollo', bodyParser.json(), graphqlExpress({ schema })); 35 | app.use('/apollo-explorer', graphiqlExpress({ endpointURL: '/apollo' })); 36 | 37 | // Setup logger 38 | app.use(morgan(':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] :response-time ms')); 39 | 40 | app.use(authRouter.authenticationMiddleware('/auth', '/api/setup')); 41 | app.use('/auth', authRouter.router); 42 | app.use('/api', apiRouter.router); 43 | app.use('/cosmosdb', cosmosDBRouter.router); 44 | app.use('/azure', azureRouter.router); 45 | app.use('/graphql', graphQLRouter.router) 46 | app.use('/applicationinsights', appInsightsRouter.router) 47 | 48 | app.use(express.static(path.resolve(__dirname, '..', 'build'))); 49 | 50 | // Always return the main index.html, so react-router render the route in the client 51 | app.get('*', (req, res) => { 52 | res.sendFile(path.resolve(__dirname, '..', 'build', 'index.html')); 53 | }); 54 | 55 | module.exports = app; -------------------------------------------------------------------------------- /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/azure.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: "azure_sample", 7 | name: "Azure Sample", 8 | icon: "dashboard", 9 | url: "azure_sample", 10 | description: "A basic azure ARM sample", 11 | preview: "/images/azure.png", 12 | html: ` 13 |
14 | A basic sample to show how to get resources from 15 | ARM 16 | (Azure) and display them on a dashboard 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: "samples", 33 | type: "Sample", 34 | params: { 35 | samples: { 36 | initialValue: 0 37 | } 38 | } 39 | }, 40 | { 41 | id: "azure", 42 | type: "Azure", 43 | dependencies: { someValue: "samples:initialValue" }, 44 | params: { type: 'resources' }, 45 | calculated: (state, dependencies) => { 46 | console.log(state); 47 | 48 | let resources = state.values || []; 49 | let resourceTypes = _.toPairs(_.groupBy(state.values, 'kind')).map(val => ({ name: val[0], value: val[1].length})); 50 | 51 | return { resources, resourceTypes }; 52 | } 53 | }, 54 | { 55 | id: "azureLocations", 56 | type: "Azure", 57 | dependencies: { someValue: "samples:initialValue" }, 58 | params: { type: 'locations' }, 59 | calculated: (state, dependencies) => { 60 | console.log(state); 61 | 62 | let locations = state.values || []; 63 | let mapData = locations.map(loc => ({ 64 | lat: loc.latitude, 65 | lng: loc.longitude, 66 | tooltip: loc.displayName + ': ' + loc.name 67 | })); 68 | 69 | return { locations: mapData }; 70 | } 71 | } 72 | ], 73 | filters: [], 74 | elements: [ 75 | { 76 | id: "pie_sample1", 77 | type: "PieData", 78 | title: "Pie Sample 1", 79 | subtitle: "Description of pie sample 1", 80 | size: { w: 5,h: 8 }, 81 | dependencies: { values: "azure:resourceTypes" }, 82 | props: { showLegend: true } 83 | }, 84 | { 85 | id: 'locations_map', 86 | type: 'MapData', 87 | title: "Locations Distribution", 88 | subtitle: "Monitor regional activity", 89 | size: { w: 7,h: 12 }, 90 | dependencies: { locations: "azureLocations:locations" }, 91 | props: { mapProps: { zoom: 1,maxZoom: 6 } } 92 | } 93 | ], 94 | dialogs: [] 95 | } -------------------------------------------------------------------------------- /server/dashboards/preconfigured/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: "basic_sample", 7 | name: "Basic Sample", 8 | icon: "extension", 9 | url: "basic_sample", 10 | description: "A basic sample to understand a basic dashboard", 11 | preview: "/images/sample.png", 12 | html: ` 13 |
14 | This is a basic sample dashboard, with JSON based sample data source, to show how data from different data sources 15 | can be manipulated and connected to visual components. 16 |
17 | `, 18 | config: { 19 | connections: {}, 20 | layout: { 21 | isDraggable: true, 22 | isResizable: true, 23 | rowHeight: 30, 24 | verticalCompact: false, 25 | cols: { lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }, 26 | breakpoints: { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 } 27 | } 28 | }, 29 | dataSources: [ 30 | { 31 | id: "samples", 32 | type: "Sample", 33 | params: { 34 | samples: { 35 | data_for_pie: [ 36 | { name: "value1", value: 60 }, 37 | { name: "value2", value: 10 }, 38 | { name: "value3", value: 30 } 39 | ], 40 | scorecard_data_value: 3000000, 41 | scorecard_data_subvalue: 4000 42 | } 43 | } 44 | } 45 | ], 46 | filters: [], 47 | elements: [ 48 | { 49 | id: "pie_sample1", 50 | type: "PieData", 51 | title: "Pie Sample 1", 52 | subtitle: "Description of pie sample 1", 53 | size: { w: 5, h: 8 }, 54 | dependencies: { values: "samples:data_for_pie" }, 55 | props: { showLegend: true } 56 | }, 57 | { 58 | id: "pie_sample2", 59 | type: "PieData", 60 | title: "Pie Sample 2", 61 | subtitle: "Hover on the values to see the difference from sample 1", 62 | size: { w: 5, h: 8 }, 63 | dependencies: { values: "samples:data_for_pie" }, 64 | props: { showLegend: true, compact: true } 65 | }, 66 | { 67 | id: "scorecard_sample1", 68 | type: "Scorecard", 69 | title: "Value", 70 | size: { w: 1, h: 3 }, 71 | dependencies: { 72 | value: "samples:scorecard_data_value", 73 | color: "::#2196F3", 74 | icon: "::av_timer" 75 | } 76 | }, 77 | { 78 | id: "scorecard_sample2", 79 | type: "Scorecard", 80 | title: "Same Value", 81 | size: { w: 1, h: 3 }, 82 | dependencies: { 83 | value: "samples:scorecard_data_value", 84 | color: "::#2196F3", 85 | icon: "::av_timer", 86 | subvalue: "samples:scorecard_data_subvalue" 87 | }, 88 | props: { 89 | subheading: "Value #2" 90 | } 91 | } 92 | ], 93 | dialogs: [] 94 | } 95 | -------------------------------------------------------------------------------- /server/dashboards/sample.basic.private.js: -------------------------------------------------------------------------------- 1 | return { 2 | id: "basic_sample", 3 | name: "Basic Sample", 4 | icon: "extension", 5 | url: "basic_sample", 6 | description: "A basic sample to see how data is connected to graphs", 7 | preview: "/images/bot-framework-preview.png", 8 | config: { 9 | connections: { }, 10 | layout: { 11 | isDraggable: true, 12 | isResizable: true, 13 | rowHeight: 30, 14 | verticalCompact: false, 15 | cols: { lg: 12,md: 10,sm: 6,xs: 4,xxs: 2 }, 16 | breakpoints: { lg: 1200,md: 996,sm: 768,xs: 480,xxs: 0 } 17 | } 18 | }, 19 | dataSources: [ 20 | { 21 | id: "samples", 22 | type: "Sample", 23 | params: { 24 | samples: { 25 | data_for_pie: [ 26 | { name: "value1",value: 60 }, 27 | { name: "value2",value: 10 }, 28 | { name: "value3",value: 30 } 29 | ], 30 | scorecard_data_value: 3000000, 31 | scorecard_data_subvalue: 4000 32 | } 33 | } 34 | } 35 | ], 36 | filters: [], 37 | elements: [ 38 | { 39 | id: "pie_sample1", 40 | type: "PieData", 41 | title: "Pie Sample 1", 42 | subtitle: "Description of pie sample 1", 43 | size: { w: 5,h: 8 }, 44 | dependencies: { values: "samples:data_for_pie" }, 45 | props: { showLegend: true } 46 | }, 47 | { 48 | id: "pie_sample2", 49 | type: "PieData", 50 | title: "Pie Sample 2", 51 | subtitle: "Hover on the values to see the difference from sample 1", 52 | size: { w: 5,h: 8 }, 53 | dependencies: { values: "samples:data_for_pie" }, 54 | props: { showLegend: true, compact: true } 55 | }, 56 | { 57 | id: "scorecard_sample1", 58 | type: "Scorecard", 59 | title: "Value", 60 | size: { w: 1, h: 3}, 61 | dependencies: { 62 | value: "samples:scorecard_data_value", 63 | color: "::#2196F3", 64 | icon: "::av_timer" 65 | } 66 | }, 67 | { 68 | id: "scorecard_sample2", 69 | type: "Scorecard", 70 | title: "Same Value", 71 | size: { w: 1, h: 3}, 72 | dependencies: { 73 | value: "samples:scorecard_data_value", 74 | color: "::#2196F3", 75 | icon: "::av_timer", 76 | subvalue: "samples:scorecard_data_subvalue" 77 | }, 78 | props: { 79 | subheading: "Value #2" 80 | } 81 | } 82 | ], 83 | dialogs: [] 84 | } 85 | -------------------------------------------------------------------------------- /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 | "cors": "^2.8.3", 11 | "express": "^4.15.2", 12 | "express-session": "^1.15.2", 13 | "graphql": "^0.10.1", 14 | "graphql-server-express": "^0.8.4", 15 | "graphql-tools": "^1.0.0", 16 | "lodash": "^4.17.4", 17 | "morgan": "^1.8.1", 18 | "ms-rest-azure": "^2.1.2", 19 | "passport": "^0.3.2", 20 | "passport-azure-ad": "^3.0.5", 21 | "xhr-request": "^1.0.1" 22 | }, 23 | "scripts": { 24 | "start": "node index.js" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/routes/application-insights.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const request = require('xhr-request'); 3 | const router = new express.Router(); 4 | 5 | const host = 'api.applicationinsights.io'; 6 | 7 | router.post('/query', (req, res) => { 8 | const { apiKey, appId, query, queryTimespan } = req.body; 9 | 10 | if (!apiKey || !appId) { 11 | return res.send({ error: 'Invalid request parameters' }); 12 | } 13 | 14 | let url = `https://${host}/beta/apps/${appId}/query`; 15 | if (queryTimespan) url += `?timespan=${queryTimespan}`; 16 | request(url, { 17 | method: 'POST', 18 | headers: { 19 | 'x-api-key': apiKey, 20 | }, 21 | body: { 22 | query, 23 | }, 24 | json: true, 25 | }, (err, result) => { 26 | if (err) { 27 | console.log(err); 28 | return res.send({ error: err }); 29 | } 30 | res.send(result); 31 | }); 32 | 33 | }); 34 | 35 | module.exports = { 36 | router, 37 | }; -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /src/components/Navbar/style.css: -------------------------------------------------------------------------------- 1 | .md-toolbar .md-select-field--toolbar.md-select-field--toolbar { 2 | margin-bottom: 6px; 3 | margin-top: 6px; } 4 | .md-toolbar .md-select-field--toolbar.md-select-field--toolbar .md-floating-label { 5 | padding-left: 6px; } 6 | .md-toolbar .md-select-field--toolbar.md-select-field--toolbar .md-text-field { 7 | padding-bottom: 0; 8 | padding-left: 6px; 9 | padding-right: 16px; 10 | padding-top: 0; 11 | margin: 20px 0 0 0; } 12 | -------------------------------------------------------------------------------- /src/components/generic/generic.css: -------------------------------------------------------------------------------- 1 | .md-card .widgets { 2 | position: absolute; 3 | top: 10px; 4 | right: 10px; } 5 | 6 | .md-card-scorecard .scorecard { 7 | width: 100px; 8 | height: 90px; 9 | white-space: nowrap; 10 | float: left; 11 | margin: 10px 8px 20px; } 12 | .md-card-scorecard .scorecard.clickable-card { 13 | cursor: pointer; } 14 | .md-card-scorecard .scorecard.clickable-card:hover { 15 | background-color: #ECEFF1; } 16 | .md-card-scorecard .scorecard.color-bottom .md-subheading-2 { 17 | margin-bottom: 3px; } 18 | .md-card-scorecard .scorecard.color-bottom .scorecard-subheading { 19 | padding-bottom: 4px; 20 | border-bottom: solid 4px transparent; } 21 | .md-card-scorecard .scorecard.color-left { 22 | padding-left: 5px; 23 | border-left: solid 3px transparent; } 24 | .md-card-scorecard .scorecard .md-icon { 25 | float: left; 26 | padding: 3px 3px 0 0; } 27 | .md-card-scorecard .scorecard .md-headline { 28 | margin-bottom: 0; 29 | font-weight: 500; } 30 | .md-card-scorecard .scorecard .md-subheading-2 { 31 | text-overflow: ellipsis; 32 | overflow-x: hidden; } 33 | .md-card-scorecard .scorecard b { 34 | margin-right: 5px; 35 | margin-bottom: 0; 36 | font-weight: 500; } 37 | 38 | /* MenuFilter */ 39 | @media screen and (min-width: 320px) { 40 | .md-multiselect-menu { 41 | margin-top: 50px; } } 42 | 43 | @media screen and (min-width: 1025px) { 44 | .md-multiselect-menu { 45 | margin-top: 58px; } } 46 | 47 | .md-value { 48 | white-space: nowrap; 49 | overflow: hidden; 50 | text-overflow: ellipsis; } 51 | 52 | /* Table */ 53 | .table > .primary { 54 | display: block; 55 | color: black; } 56 | 57 | .table > .secondary { 58 | color: grey; } 59 | 60 | td.text { 61 | white-space: normal; } 62 | 63 | td.summary { 64 | max-width: 240px; 65 | overflow: hidden; 66 | text-overflow: ellipsis; } 67 | 68 | /* Sentiment */ 69 | .sentiment_very_dissatisfied { 70 | color: red !important; } 71 | 72 | .sentiment_dissatisfied { 73 | color: orange !important; } 74 | 75 | .sentiment_neutral { 76 | color: grey !important; } 77 | 78 | .sentiment_satisfied { 79 | color: yellow !important; } 80 | 81 | .sentiment_very_satisfied { 82 | color: green !important; } 83 | 84 | .error_outline { 85 | color: lightgrey !important; } 86 | 87 | .tooltip { 88 | visibility: visible !important; } 89 | --------------------------------------------------------------------------------