├── .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 |
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 | []
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 | close
58 |
59 | )
60 | }).filter( function(e) {
61 | return e != undefined;
62 | })
63 |
64 | return (
65 |
66 |
67 | {lines}
68 | add
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 |
66 | {buttonIcon}
67 |
68 |
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 | add_circle
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 |
{title}
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 |
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 |
--------------------------------------------------------------------------------