Merge remote-tracking branch 'grafana/master' into table-reducer

* grafana/master: (107 commits)
  another change that didn't come with earlier commit
  change that didn't come with in last commit
  reversed dashboard-padding
  Update CloudWatch metrics/dimension list (#16102)
  brought back dashboard-padding and panel-padding variables, made dashboard-padding more specific
  fix(prometheus): Change aligment of range queries (#16110)
  Minor refactoring of testdata query order PR #16122
  cleaner version
  maintain query order
  Update PLUGIN_DEV.md
  Merge with master, and updated logo and name
  update table data model
  fix(graphite): nonNegativeDerivative argument hidden if 0, fixes #12488
  Correct table names of sql storage for remotecache
  more fixes to snapshot
  more fixes to snapshot
  Fixed gofmt issue in PR #16093
  removed empty space in snapshot
  fix: Update snapshot related to new jest version
  fixed snapshot for test
  ...
pull/15938/head
ryan 6 years ago
commit f8094a6e09
  1. 5
      PLUGIN_DEV.md
  2. 64
      docs/sources/features/datasources/prometheus.md
  3. 79
      package.json
  4. 2
      packages/grafana-ui/package.json
  5. 2
      packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss
  6. 19
      packages/grafana-ui/src/components/FormField/FormField.test.tsx
  7. 11
      packages/grafana-ui/src/components/FormField/FormField.tsx
  8. 20
      packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap
  9. 42
      packages/grafana-ui/src/components/PieChart/PieChart.story.tsx
  10. 147
      packages/grafana-ui/src/components/PieChart/PieChart.tsx
  11. 38
      packages/grafana-ui/src/components/SecretFormFied/SecretFormField.story.tsx
  12. 71
      packages/grafana-ui/src/components/SecretFormFied/SecretFormField.tsx
  13. 2
      packages/grafana-ui/src/components/Table/Table.story.tsx
  14. 16
      packages/grafana-ui/src/components/Table/Table.tsx
  15. 32
      packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap
  16. 2
      packages/grafana-ui/src/components/index.ts
  17. 25
      packages/grafana-ui/src/themes/_variables.scss.tmpl.ts
  18. 15
      packages/grafana-ui/src/themes/default.ts
  19. 35
      packages/grafana-ui/src/types/data.ts
  20. 9
      packages/grafana-ui/src/types/panel.ts
  21. 13
      packages/grafana-ui/src/types/theme.ts
  22. 4
      packages/grafana-ui/src/utils/__snapshots__/processTableData.test.ts.snap
  23. 24
      packages/grafana-ui/src/utils/floatPairs.test.ts
  24. 38
      packages/grafana-ui/src/utils/flotPairs.ts
  25. 2
      packages/grafana-ui/src/utils/index.ts
  26. 40
      packages/grafana-ui/src/utils/processTableData.test.ts
  27. 48
      packages/grafana-ui/src/utils/processTableData.ts
  28. 49
      packages/grafana-ui/src/utils/processTimeSeries.ts
  29. 33
      packages/grafana-ui/src/utils/singlestat.ts
  30. 2
      packages/grafana-ui/src/utils/storybook/UseState.tsx
  31. 2
      pkg/infra/remotecache/database_storage.go
  32. 18
      pkg/infra/remotecache/database_storage_test.go
  33. 4
      pkg/models/org_user.go
  34. 263
      pkg/tsdb/cloudwatch/metric_find_query.go
  35. 9
      public/app/core/angular_wrappers.ts
  36. 6
      public/app/core/components/Animations/FadeIn.tsx
  37. 16
      public/app/core/components/Animations/SlideDown.tsx
  38. 4
      public/app/core/components/CopyToClipboard/CopyToClipboard.tsx
  39. 4
      public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx
  40. 1
      public/app/core/components/JSONFormatter/JSONFormatter.tsx
  41. 35
      public/app/core/components/form_dropdown/form_dropdown.ts
  42. 2
      public/app/core/components/json_explorer/json_explorer.ts
  43. 8
      public/app/core/components/layout_selector/layout_selector.ts
  44. 21
      public/app/core/services/ng_react.ts
  45. 15
      public/app/core/table_model.ts
  46. 2
      public/app/features/api-keys/ApiKeysPage.tsx
  47. 2
      public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx
  48. 28
      public/app/features/dashboard/dashgrid/DataPanel.tsx
  49. 14
      public/app/features/dashboard/dashgrid/PanelChrome.tsx
  50. 12
      public/app/features/dashboard/state/PanelModel.ts
  51. 19
      public/app/features/dashboard/utils/panel.ts
  52. 2
      public/app/features/folders/FolderPermissions.tsx
  53. 4
      public/app/features/playlist/playlist_srv.ts
  54. 2
      public/app/features/plugins/built_in_plugins.ts
  55. 2
      public/app/features/teams/TeamGroupSync.tsx
  56. 2
      public/app/features/teams/TeamMembers.tsx
  57. 5
      public/app/plugins/datasource/graphite/func_editor.ts
  58. 3
      public/app/plugins/datasource/influxdb/influx_series.ts
  59. 14
      public/app/plugins/datasource/mssql/config_ctrl.ts
  60. 18
      public/app/plugins/datasource/mssql/partials/config.html
  61. 13
      public/app/plugins/datasource/postgres/config_ctrl.ts
  62. 17
      public/app/plugins/datasource/postgres/partials/config.html
  63. 14
      public/app/plugins/datasource/prometheus/datasource.ts
  64. 4
      public/app/plugins/datasource/prometheus/result_transformer.ts
  65. 18
      public/app/plugins/datasource/prometheus/specs/datasource.test.ts
  66. 43
      public/app/plugins/datasource/testdata/datasource.ts
  67. 4
      public/app/plugins/panel/bargauge/BarGaugePanel.tsx
  68. 4
      public/app/plugins/panel/gauge/GaugePanel.tsx
  69. 6
      public/app/plugins/panel/graph2/GraphPanel.tsx
  70. 47
      public/app/plugins/panel/piechart/PieChartOptionsBox.tsx
  71. 57
      public/app/plugins/panel/piechart/PieChartPanel.tsx
  72. 27
      public/app/plugins/panel/piechart/PieChartPanelEditor.tsx
  73. 54
      public/app/plugins/panel/piechart/PieChartValueEditor.tsx
  74. 29
      public/app/plugins/panel/piechart/img/icon_piechart.svg
  75. 10
      public/app/plugins/panel/piechart/module.tsx
  76. 19
      public/app/plugins/panel/piechart/plugin.json
  77. 21
      public/app/plugins/panel/piechart/types.ts
  78. 4
      public/app/plugins/panel/singlestat/module.ts
  79. 27
      public/app/plugins/panel/singlestat2/SingleStatPanel.tsx
  80. 3
      public/app/plugins/panel/table/module.ts
  81. 6
      public/app/plugins/panel/table2/TablePanel.tsx
  82. 0
      public/app/types/jquery/jquery.d.ts
  83. 1
      public/sass/_grafana.scss
  84. 9
      public/sass/_variables.generated.scss
  85. 1
      public/sass/base/_type.scss
  86. 4
      public/sass/components/_panel_editor.scss
  87. 3
      public/sass/components/_panel_logs.scss
  88. 40
      public/sass/components/_panel_piechart.scss
  89. 2
      public/sass/components/_tabbed_view.scss
  90. 5
      public/sass/pages/_dashboard.scss
  91. 6
      public/sass/pages/_explore.scss
  92. 3
      scripts/webpack/sass.rule.js
  93. 11
      scripts/webpack/webpack.dev.js
  94. 4
      scripts/webpack/webpack.hot.js
  95. 8
      scripts/webpack/webpack.prod.js
  96. 2
      tsconfig.json
  97. 5352
      yarn.lock

@ -27,3 +27,8 @@ If you think we missed exposing a crucial lib or Grafana component let us know b
The angular directive `<spectrum-picker>` is now deprecated (will still work for a version more) but we recommend plugin authors
to upgrade to new `<color-picker color="ctrl.color" onChange="ctrl.onSparklineColorChange"></color-picker>`
## Changes in v6.0
### DashboardSrv.ts
If you utilize [DashboardSrv](https://github.com/grafana/grafana/commit/8574dca081002f36e482b572517d8f05fd44453f#diff-1ab99561f9f6a10e1fafcddc39bc1d65) in your plugin code, `dash` was renamed to `dashboard`

@ -19,22 +19,22 @@ Grafana includes built-in support for Prometheus.
1. Open the side menu by clicking the Grafana icon in the top header.
2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`.
3. Click the `+ Add data source` button in the top header.
4. Select `Prometheus` from the *Type* dropdown.
4. Select `Prometheus` from the _Type_ dropdown.
> NOTE: If you're not seeing the `Data Sources` link in your side menu it means that your current user does not have the `Admin` role for the current organization.
## Data source options
Name | Description
------------ | -------------
*Name* | The data source name. This is how you refer to the data source in panels & queries.
*Default* | Default data source means that it will be pre-selected for new panels.
*Url* | The http protocol, ip and port of you Prometheus server (default port is usually 9090)
*Access* | Server (default) = URL needs to be accessible from the Grafana backend/server, Browser = URL needs to be accessible from the browser.
*Basic Auth* | Enable basic authentication to the Prometheus data source.
*User* | Name of your Prometheus user
*Password* | Database user's password
*Scrape interval* | This will be used as a lower limit for the Prometheus step query parameter. Default value is 15s.
| Name | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| _Name_ | The data source name. This is how you refer to the data source in panels & queries. |
| _Default_ | Default data source means that it will be pre-selected for new panels. |
| _Url_ | The http protocol, ip and port of you Prometheus server (default port is usually 9090) |
| _Access_ | Server (default) = URL needs to be accessible from the Grafana backend/server, Browser = URL needs to be accessible from the browser. |
| _Basic Auth_ | Enable basic authentication to the Prometheus data source. |
| _User_ | Name of your Prometheus user |
| _Password_ | Database user's password |
| _Scrape interval_ | This will be used as a lower limit for the Prometheus step query parameter. Default value is 15s. |
## Query editor
@ -43,14 +43,17 @@ Open a graph in edit mode by click the title > Edit (or by pressing `e` key whil
{{< docs-imagebox img="/img/docs/v45/prometheus_query_editor_still.png"
animated-gif="/img/docs/v45/prometheus_query_editor.gif" >}}
Name | Description
------- | --------
*Query expression* | Prometheus query expression, check out the [Prometheus documentation](http://prometheus.io/docs/querying/basics/).
*Legend format* | Controls the name of the time series, using name or pattern. For example `{{hostname}}` will be replaced with label value for the label `hostname`.
*Min step* | Set a lower limit for the Prometheus step option. Step controls how big the jumps are when the Prometheus query engine performs range queries. Sadly there is no official prometheus documentation to link to for this very important option.
*Resolution* | Controls the step option. Small steps create high-resolution graphs but can be slow over larger time ranges, lowering the resolution can speed things up. `1/2` will try to set step option to generate 1 data point for every other pixel. A value of `1/10` will try to set step option so there is a data point every 10 pixels.
*Metric lookup* | Search for metric names in this input field.
*Format as* | Switch between Table, Time series or Heatmap. Table format will only work in the Table panel. Heatmap format is suitable for displaying metrics having histogram type on Heatmap panel. Under the hood, it converts cumulative histogram to regular and sorts series by the bucket bound.
| Name | Description |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| _Query expression_ | Prometheus query expression, check out the [Prometheus documentation](http://prometheus.io/docs/querying/basics/). |
| _Legend format_ | Controls the name of the time series, using name or pattern. For example `{{hostname}}` will be replaced with label value for the label `hostname`. |
| _Min step_ | Set a lower limit for the Prometheus step option. Step controls how big the jumps are when the Prometheus query engine performs range queries. Sadly there is no official prometheus documentation to link to for this very important option. |
| _Resolution_ | Controls the step option. Small steps create high-resolution graphs but can be slow over larger time ranges, lowering the resolution can speed things up. `1/2` will try to set step option to generate 1 data point for every other pixel. A value of `1/10` will try to set step option so there is a data point every 10 pixels. |
| _Metric lookup_ | Search for metric names in this input field. |
| _Format as_ | Switch between Table, Time series or Heatmap. Table format will only work in the Table panel. Heatmap format is suitable for displaying metrics having histogram type on Heatmap panel. Under the hood, it converts cumulative histogram to regular and sorts series by the bucket bound. |
> NOTE: Grafana slightly modifies the request dates for queries to align them with the dynamically calculated step.
> This ensures consistent display of metrics data but can result in a small gap of data at the right edge of a graph.
## Templating
@ -63,19 +66,18 @@ types of template variables.
### Query variable
Variable of the type *Query* allows you to query Prometheus for a list of metrics, labels or label values. The Prometheus data source plugin
Variable of the type _Query_ allows you to query Prometheus for a list of metrics, labels or label values. The Prometheus data source plugin
provides the following functions you can use in the `Query` input field.
Name | Description
---- | --------
*label_names()* | Returns a list of label names.
*label_values(label)* | Returns a list of label values for the `label` in every metric.
*label_values(metric, label)* | Returns a list of label values for the `label` in the specified metric.
*metrics(metric)* | Returns a list of metrics matching the specified `metric` regex.
*query_result(query)* | Returns a list of Prometheus query result for the `query`.
For details of *metric names*, *label names* and *label values* are please refer to the [Prometheus documentation](http://prometheus.io/docs/concepts/data_model/#metric-names-and-labels).
| Name | Description |
| ----------------------------- | ----------------------------------------------------------------------- |
| _label_names()_ | Returns a list of label names. |
| _label_values(label)_ | Returns a list of label values for the `label` in every metric. |
| _label_values(metric, label)_ | Returns a list of label values for the `label` in the specified metric. |
| _metrics(metric)_ | Returns a list of metrics matching the specified `metric` regex. |
| _query_result(query)_ | Returns a list of Prometheus query result for the `query`. |
For details of _metric names_, _label names_ and _label values_ are please refer to the [Prometheus documentation](http://prometheus.io/docs/concepts/data_model/#metric-names-and-labels).
#### Using interval and range variables
@ -106,10 +108,10 @@ Regex:
There are two syntaxes:
- `$<varname>` Example: rate(http_requests_total{job=~"$job"}[5m])
- `$<varname>` Example: rate(http_requests_total{job=~"\$job"}[5m])
- `[[varname]]` Example: rate(http_requests_total{job=~"[[job]]"}[5m])
Why two ways? The first syntax is easier to read and write but does not allow you to use a variable in the middle of a word. When the *Multi-value* or *Include all value*
Why two ways? The first syntax is easier to read and write but does not allow you to use a variable in the middle of a word. When the _Multi-value_ or _Include all value_
options are enabled, Grafana converts the labels from plain text to a regex compatible string. Which means you have to use `=~` instead of `=`.
## Annotations

@ -11,11 +11,11 @@
"url": "http://github.com/grafana/grafana.git"
},
"devDependencies": {
"@babel/core": "^7.1.2",
"@babel/core": "^7.3.4",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/preset-env": "^7.1.0",
"@babel/preset-env": "^7.3.4",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.1.0",
"@babel/preset-typescript": "^7.3.3",
"@rtsao/plugin-proposal-class-properties": "^7.0.1-patch.1",
"@types/angular": "^1.6.6",
"@types/chalk": "^2.2.0",
@ -24,26 +24,28 @@
"@types/d3": "^4.10.1",
"@types/enzyme": "^3.1.13",
"@types/inquirer": "^0.0.43",
"@types/jest": "^23.3.2",
"@types/jest": "^24.0.11",
"@types/jquery": "^1.10.35",
"@types/node": "^8.0.31",
"@types/papaparse": "^4.5.9",
"@types/react": "^16.8.8",
"@types/react-dom": "^16.8.2",
"@types/react-grid-layout": "^0.16.6",
"@types/react-select": "^2.0.4",
"@types/react-transition-group": "^2.0.15",
"@types/react-virtualized": "^9.18.12",
"@types/clipboard": "^2.0.1",
"angular-mocks": "1.6.6",
"autoprefixer": "^6.4.0",
"axios": "^0.17.1",
"autoprefixer": "^9.4.10",
"axios": "^0.18.0",
"babel-core": "^7.0.0-bridge",
"babel-jest": "^23.6.0",
"babel-jest": "^24.5.0",
"babel-loader": "^8.0.4",
"babel-plugin-angularjs-annotate": "^0.9.0",
"babel-plugin-angularjs-annotate": "^0.10.0",
"chalk": "^2.4.2",
"clean-webpack-plugin": "^0.1.19",
"clean-webpack-plugin": "^2.0.0",
"concurrently": "^4.1.0",
"css-loader": "^0.28.7",
"css-loader": "^2.1.1",
"enzyme": "^3.6.0",
"enzyme-adapter-react-16": "^1.5.0",
"enzyme-to-json": "^3.3.4",
@ -51,11 +53,11 @@
"es6-shim": "^0.35.3",
"execa": "^1.0.0",
"expect.js": "~0.2.0",
"expose-loader": "^0.7.3",
"file-loader": "^1.1.11",
"fork-ts-checker-webpack-plugin": "^0.4.9",
"expose-loader": "0.7.5",
"file-loader": "^3.0.1",
"fork-ts-checker-webpack-plugin": "^1.0.0",
"gaze": "^1.1.2",
"glob": "~7.0.0",
"glob": "~7.1.3",
"grunt": "1.0.1",
"grunt-angular-templates": "^1.1.0",
"grunt-cli": "~1.2.0",
@ -69,29 +71,29 @@
"grunt-sass-lint": "^0.2.4",
"grunt-usemin": "3.1.1",
"grunt-webpack": "^3.0.2",
"html-loader": "^0.5.1",
"html-webpack-harddisk-plugin": "^0.2.0",
"html-loader": "0.5.5",
"html-webpack-harddisk-plugin": "^1.0.1",
"html-webpack-plugin": "^3.2.0",
"husky": "^1.3.1",
"inquirer": "^6.2.2",
"jest": "^23.6.0",
"jest": "^24.5.0",
"jest-date-mock": "^1.0.6",
"lint-staged": "^8.1.3",
"load-grunt-tasks": "3.5.2",
"mini-css-extract-plugin": "^0.4.0",
"mini-css-extract-plugin": "^0.5.0",
"mocha": "^4.0.1",
"monaco-editor": "^0.15.6",
"ng-annotate-loader": "^0.6.1",
"ng-annotate-webpack-plugin": "^0.3.0",
"ngtemplate-loader": "^2.0.1",
"node-sass": "^4.11.0",
"npm": "^5.4.2",
"optimize-css-assets-webpack-plugin": "^4.0.2",
"ora": "^3.1.0",
"npm": "^6.9.0",
"optimize-css-assets-webpack-plugin": "^5.0.1",
"ora": "^3.2.0",
"phantomjs-prebuilt": "^2.1.15",
"postcss-browser-reporter": "^0.5.0",
"postcss-loader": "^2.0.6",
"postcss-reporter": "^5.0.0",
"postcss-loader": "^3.0.0",
"postcss-reporter": "^6.0.1",
"prettier": "1.16.4",
"react-hot-loader": "^4.3.6",
"react-test-renderer": "^16.5.0",
@ -99,27 +101,27 @@
"regexp-replace-loader": "^1.0.1",
"rimraf": "^2.6.3",
"sass-lint": "^1.10.2",
"sass-loader": "^7.0.1",
"sass-loader": "7.1.0",
"semver": "^5.6.0",
"sinon": "1.17.6",
"style-loader": "^0.21.0",
"style-loader": "0.23.1",
"systemjs": "0.20.19",
"systemjs-plugin-css": "^0.1.36",
"ts-jest": "^23.10.4",
"ts-loader": "^5.1.0",
"ts-node": "^8.0.2",
"tslib": "^1.9.3",
"tslint": "^5.8.0",
"terser-webpack-plugin": "^1.2.3",
"ts-jest": "^24.0.0",
"ts-loader": "5.3.3",
"ts-node": "8.0.2",
"tslib": "1.9.3",
"tslint": "5.14.0",
"tslint-loader": "^3.5.3",
"tslint-react": "^3.6.0",
"typescript": "^3.0.3",
"uglifyjs-webpack-plugin": "^1.2.7",
"webpack": "4.19.1",
"webpack-bundle-analyzer": "^2.9.0",
"typescript": "3.3.3333",
"webpack": "4.29.6",
"webpack-bundle-analyzer": "3.1.0",
"webpack-cleanup-plugin": "^0.5.1",
"webpack-cli": "^2.1.4",
"webpack-dev-server": "^3.1.0",
"webpack-merge": "^4.1.0",
"webpack-cli": "3.2.3",
"webpack-dev-server": "3.2.1",
"webpack-merge": "4.2.1",
"zone.js": "^0.7.2"
},
"scripts": {
@ -176,7 +178,7 @@
"baron": "^3.0.3",
"brace": "^0.10.0",
"classnames": "^2.2.6",
"clipboard": "^1.7.1",
"clipboard": "^2.0.4",
"d3": "^4.11.0",
"d3-scale-chromatic": "^1.3.0",
"eventemitter3": "^2.0.3",
@ -188,6 +190,7 @@
"mousetrap": "^1.6.0",
"mousetrap-global-bind": "^1.1.0",
"nodemon": "^1.18.10",
"papaparse": "^4.6.3",
"prismjs": "^1.6.0",
"prop-types": "^15.6.2",
"rc-cascader": "^0.14.0",

@ -21,6 +21,7 @@
"@torkelo/react-select": "2.1.1",
"@types/react-color": "^2.14.0",
"classnames": "^2.2.5",
"d3": "^5.7.0",
"jquery": "^3.2.1",
"lodash": "^4.17.10",
"moment": "^2.22.2",
@ -43,6 +44,7 @@
"@storybook/addon-knobs": "^4.1.7",
"@storybook/react": "^4.1.4",
"@types/classnames": "^2.2.6",
"@types/d3": "^5.7.0",
"@types/jest": "^23.3.2",
"@types/jquery": "^1.10.35",
"@types/lodash": "^4.14.119",

@ -45,7 +45,7 @@ $arrowSize: 15px;
border-right-color: transparent;
border-top-color: transparent;
top: 0;
left: calc(100% -$arrowSize);
left: calc(100%-#{$arrowSize});
}
&[data-placement^='right'] {

@ -2,7 +2,7 @@ import React from 'react';
import { shallow } from 'enzyme';
import { FormField, Props } from './FormField';
const setup = (propOverrides?: object) => {
const setup = (propOverrides?: Partial<Props>) => {
const props: Props = {
label: 'Test',
labelWidth: 11,
@ -15,10 +15,23 @@ const setup = (propOverrides?: object) => {
return shallow(<FormField {...props} />);
};
describe('Render', () => {
it('should render component', () => {
describe('FormField', () => {
it('should render component with default inputEl', () => {
const wrapper = setup();
expect(wrapper).toMatchSnapshot();
});
it('should render component with custom inputEl', () => {
const wrapper = setup({
inputEl: (
<>
<span>Input</span>
<button>Ok</button>
</>
),
});
expect(wrapper).toMatchSnapshot();
});
});

@ -5,6 +5,7 @@ export interface Props extends InputHTMLAttributes<HTMLInputElement> {
label: string;
labelWidth?: number;
inputWidth?: number;
inputEl?: React.ReactNode;
}
const defaultProps = {
@ -12,14 +13,18 @@ const defaultProps = {
inputWidth: 12,
};
const FormField: FunctionComponent<Props> = ({ label, labelWidth, inputWidth, ...inputProps }) => {
/**
* Default form field including label used in Grafana UI. Default input element is simple <input />. You can also pass
* custom inputEl if required in which case inputWidth and inputProps are ignored.
*/
export const FormField: FunctionComponent<Props> = ({ label, labelWidth, inputWidth, inputEl, ...inputProps }) => {
return (
<div className="form-field">
<FormLabel width={labelWidth}>{label}</FormLabel>
<input type="text" className={`gf-form-input width-${inputWidth}`} {...inputProps} />
{inputEl || <input type="text" className={`gf-form-input width-${inputWidth}`} {...inputProps} />}
</div>
);
};
FormField.displayName = 'FormField';
FormField.defaultProps = defaultProps;
export { FormField };

@ -1,6 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
exports[`FormField should render component with custom inputEl 1`] = `
<div
className="form-field"
>
<Component
width={11}
>
Test
</Component>
<span>
Input
</span>
<button>
Ok
</button>
</div>
`;
exports[`FormField should render component with default inputEl 1`] = `
<div
className="form-field"
>

@ -0,0 +1,42 @@
import { storiesOf } from '@storybook/react';
import { number, text, object } from '@storybook/addon-knobs';
import { PieChart, PieChartType } from './PieChart';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
const getKnobs = () => {
return {
datapoints: object('datapoints', [
{
value: 100,
name: '100',
color: '#7EB26D',
},
{
value: 200,
name: '200',
color: '#6ED0E0',
},
]),
pieType: text('pieType', PieChartType.PIE),
strokeWidth: number('strokeWidth', 1),
unit: text('unit', 'ms'),
};
};
const PieChartStories = storiesOf('UI/PieChart/PieChart', module);
PieChartStories.addDecorator(withCenteredStory);
PieChartStories.add('Pie type: pie', () => {
const { datapoints, pieType, strokeWidth, unit } = getKnobs();
return renderComponentWithTheme(PieChart, {
width: 200,
height: 400,
datapoints,
pieType,
strokeWidth,
unit,
});
});

@ -0,0 +1,147 @@
import React, { PureComponent } from 'react';
import { select, pie, arc, event } from 'd3';
import { sum } from 'lodash';
import { GrafanaThemeType } from '../../types';
import { Themeable } from '../../index';
export enum PieChartType {
PIE = 'pie',
DONUT = 'donut',
}
export interface PieChartDataPoint {
value: number;
name: string;
color: string;
}
export interface Props extends Themeable {
height: number;
width: number;
datapoints: PieChartDataPoint[];
unit: string;
pieType: PieChartType;
strokeWidth: number;
}
export class PieChart extends PureComponent<Props> {
containerElement: any;
svgElement: any;
tooltipElement: any;
tooltipValueElement: any;
static defaultProps = {
pieType: 'pie',
format: 'short',
stat: 'current',
strokeWidth: 1,
theme: GrafanaThemeType.Dark,
};
componentDidMount() {
this.draw();
}
componentDidUpdate() {
this.draw();
}
draw() {
const { datapoints, pieType, strokeWidth } = this.props;
if (datapoints.length === 0) {
return;
}
const data = datapoints.map(datapoint => datapoint.value);
const names = datapoints.map(datapoint => datapoint.name);
const colors = datapoints.map(datapoint => datapoint.color);
const total = sum(data) || 1;
const percents = data.map((item: number) => (item / total) * 100);
const width = this.containerElement.offsetWidth;
const height = this.containerElement.offsetHeight;
const radius = Math.min(width, height) / 2;
const outerRadius = radius - radius / 10;
const innerRadius = pieType === PieChartType.PIE ? 0 : radius - radius / 3;
const svg = select(this.svgElement)
.html('')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', `translate(${width / 2},${height / 2})`);
const pieChart = pie();
const customArc = arc()
.outerRadius(outerRadius)
.innerRadius(innerRadius)
.padAngle(0);
svg
.selectAll('path')
.data(pieChart(data))
.enter()
.append('path')
.attr('d', customArc as any)
.attr('fill', (d: any, idx: number) => colors[idx])
.style('fill-opacity', 0.15)
.style('stroke', (d: any, idx: number) => colors[idx])
.style('stroke-width', `${strokeWidth}px`)
.on('mouseover', (d: any, idx: any) => {
select(this.tooltipElement).style('opacity', 1);
select(this.tooltipValueElement).text(`${names[idx]} (${percents[idx].toFixed(2)}%)`);
})
.on('mousemove', () => {
select(this.tooltipElement)
.style('top', `${event.pageY - height / 2}px`)
.style('left', `${event.pageX}px`);
})
.on('mouseout', () => {
select(this.tooltipElement).style('opacity', 0);
});
}
render() {
const { height, width, datapoints } = this.props;
if (datapoints.length > 0) {
return (
<div className="piechart-panel">
<div
ref={element => (this.containerElement = element)}
className="piechart-container"
style={{
height: `${height * 0.9}px`,
width: `${Math.min(width, height * 1.3)}px`,
}}
>
<svg ref={element => (this.svgElement = element)} />
</div>
<div className="piechart-tooltip" ref={element => (this.tooltipElement = element)}>
<div className="piechart-tooltip-time">
<div
id="tooltip-value"
className="piechart-tooltip-value"
ref={element => (this.tooltipValueElement = element)}
/>
</div>
</div>
</div>
);
} else {
return (
<div className="piechart-panel">
<div className="datapoints-warning">
<span className="small">No data points</span>
</div>
</div>
);
}
}
}

@ -0,0 +1,38 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { boolean } from '@storybook/addon-knobs';
import { SecretFormField } from './SecretFormField';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { UseState } from '../../utils/storybook/UseState';
const SecretFormFieldStories = storiesOf('UI/SecretFormField/SecretFormField', module);
SecretFormFieldStories.addDecorator(withCenteredStory);
const getSecretFormFieldKnobs = () => {
return {
isConfigured: boolean('Set configured state', false),
};
};
SecretFormFieldStories.add('default', () => {
const knobs = getSecretFormFieldKnobs();
return (
<UseState initialState="Input value">
{(value, setValue) => (
<SecretFormField
label={'Secret field'}
labelWidth={10}
value={value}
isConfigured={knobs.isConfigured}
onChange={e => setValue(e.currentTarget.value)}
onReset={() => {
action('Value was reset')('');
setValue('');
}}
/>
)}
</UseState>
);
});

@ -0,0 +1,71 @@
import { omit } from 'lodash';
import React, { InputHTMLAttributes, FunctionComponent } from 'react';
import { FormField } from '..';
interface Props extends InputHTMLAttributes<HTMLInputElement> {
// Function to use when reset is clicked. Means you have to reset the input value yourself as this is uncontrolled
// component (or do something else if required).
onReset: () => void;
isConfigured: boolean;
label?: string;
labelWidth?: number;
inputWidth?: number;
// Placeholder of the input field when in non configured state.
placeholder?: string;
}
const defaultProps = {
inputWidth: 12,
placeholder: 'Password',
label: 'Password',
};
/**
* Form field that has 2 states configured and not configured. If configured it will not show its contents and adds
* a reset button that will clear the input and makes it accessible. In non configured state it behaves like normal
* form field. This is used for passwords or anything that is encrypted on the server and is later returned encrypted
* to the user (like datasource passwords).
*/
export const SecretFormField: FunctionComponent<Props> = ({
label,
labelWidth,
inputWidth,
onReset,
isConfigured,
placeholder,
...inputProps
}: Props) => {
return (
<FormField
label={label!}
labelWidth={labelWidth}
inputEl={
isConfigured ? (
<>
<input
type="text"
className={`gf-form-input width-${inputWidth! - 2}`}
disabled={true}
value="configured"
{...omit(inputProps, 'value')}
/>
<button className="btn btn-secondary gf-form-btn" onClick={onReset}>
reset
</button>
</>
) : (
<input
type="password"
className={`gf-form-input width-${inputWidth}`}
placeholder={placeholder}
{...inputProps}
/>
)
}
/>
);
};
SecretFormField.defaultProps = defaultProps;
SecretFormField.displayName = 'SecretFormField';

@ -40,8 +40,6 @@ export function makeDummyTable(columnCount: number, rowCount: number): TableData
const suffix = (rowId + 1).toString();
return Array.from(new Array(columnCount), (x, colId) => columnIndexToLeter(colId) + suffix);
}),
type: 'table',
columnMap: {},
};
}

@ -8,6 +8,7 @@ import {
CellMeasurerCache,
CellMeasurer,
GridCellProps,
Index,
} from 'react-virtualized';
import { Themeable } from '../../types/theme';
@ -26,6 +27,7 @@ import { stringToJsRegex } from '../../utils/index';
export interface Props extends Themeable {
data: TableData;
minColumnWidth: number;
showHeader: boolean;
fixedHeader: boolean;
fixedColumns: number;
@ -46,6 +48,7 @@ interface State {
interface ColumnRenderInfo {
header: string;
width: number;
builder: TableCellBuilder;
}
@ -64,6 +67,7 @@ export class Table extends Component<Props, State> {
fixedHeader: true,
fixedColumns: 0,
rotate: false,
minColumnWidth: 150,
};
constructor(props: Props) {
@ -76,7 +80,7 @@ export class Table extends Component<Props, State> {
this.renderer = this.initColumns(props);
this.measurer = new CellMeasurerCache({
defaultHeight: 30,
defaultWidth: 150,
fixedWidth: true,
});
}
@ -110,7 +114,8 @@ export class Table extends Component<Props, State> {
/** Given the configuration, setup how each column gets rendered */
initColumns(props: Props): ColumnRenderInfo[] {
const { styles, data } = props;
const { styles, data, width, minColumnWidth } = props;
const columnWidth = Math.max(width / data.columns.length, minColumnWidth);
return data.columns.map((col, index) => {
let title = col.text;
@ -131,6 +136,7 @@ export class Table extends Component<Props, State> {
return {
header: title,
width: columnWidth,
builder: getCellBuilder(col, style, this.props),
};
});
@ -228,6 +234,10 @@ export class Table extends Component<Props, State> {
);
};
getColumnWidth = (col: Index): number => {
return this.renderer[col.index].width;
};
render() {
const { showHeader, fixedHeader, fixedColumns, rotate, width, height } = this.props;
const { data } = this.state;
@ -269,7 +279,7 @@ export class Table extends Component<Props, State> {
rowCount={rowCount}
overscanColumnCount={8}
overscanRowCount={8}
columnWidth={this.measurer.columnWidth}
columnWidth={this.getColumnWidth}
deferredMeasurementCache={this.measurer}
cellRenderer={this.cellRenderer}
rowHeight={this.measurer.rowHeight}

@ -17,7 +17,7 @@ exports[`Render should render with base threshold 1`] = `
],
"results": Array [
Object {
"isThrow": false,
"type": "return",
"value": undefined,
},
],
@ -195,7 +195,7 @@ exports[`Render should render with base threshold 1`] = `
"typography": Object {
"fontFamily": Object {
"monospace": "Menlo, Monaco, Consolas, 'Courier New', monospace",
"sansSerif": "'Roboto', Helvetica, Arial, sans-serif",
"sansSerif": "'Roboto', 'Helvetica Neue', Arial, sans-serif",
},
"heading": Object {
"h1": "28px",
@ -211,6 +211,10 @@ exports[`Render should render with base threshold 1`] = `
"sm": 1.1,
"xs": 1,
},
"link": Object {
"decoration": "none",
"hoverDecoration": "none",
},
"size": Object {
"base": "13px",
"lg": "18px",
@ -225,6 +229,15 @@ exports[`Render should render with base threshold 1`] = `
"semibold": 500,
},
},
"zIndex": Object {
"dropdown": "1000",
"modal": "1050",
"modalBackdrop": "1040",
"navbarFixed": "1020",
"sidemenu": "1025",
"tooltip": "1030",
"typeahead": "1060",
},
}
}
>
@ -339,7 +352,7 @@ exports[`Render should render with base threshold 1`] = `
"typography": Object {
"fontFamily": Object {
"monospace": "Menlo, Monaco, Consolas, 'Courier New', monospace",
"sansSerif": "'Roboto', Helvetica, Arial, sans-serif",
"sansSerif": "'Roboto', 'Helvetica Neue', Arial, sans-serif",
},
"heading": Object {
"h1": "28px",
@ -355,6 +368,10 @@ exports[`Render should render with base threshold 1`] = `
"sm": 1.1,
"xs": 1,
},
"link": Object {
"decoration": "none",
"hoverDecoration": "none",
},
"size": Object {
"base": "13px",
"lg": "18px",
@ -369,6 +386,15 @@ exports[`Render should render with base threshold 1`] = `
"semibold": 500,
},
},
"zIndex": Object {
"dropdown": "1000",
"modal": "1050",
"modalBackdrop": "1040",
"navbarFixed": "1020",
"sidemenu": "1025",
"tooltip": "1030",
"typeahead": "1060",
},
}
}
/>

@ -14,6 +14,7 @@ export { default as resetSelectStyles } from './Select/resetSelectStyles';
// Forms
export { FormLabel } from './FormLabel/FormLabel';
export { FormField } from './FormField/FormField';
export { SecretFormField } from './SecretFormFied/SecretFormField';
export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder';
export { ColorPicker, SeriesColorPicker } from './ColorPicker/ColorPicker';
@ -24,6 +25,7 @@ export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid';
export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor';
export { Switch } from './Switch/Switch';
export { EmptySearchResult } from './EmptySearchResult/EmptySearchResult';
export { PieChart, PieChartDataPoint, PieChartType } from './PieChart/PieChart';
export { UnitPicker } from './UnitPicker/UnitPicker';
export { Input, InputStatus } from './Input/Input';

@ -110,7 +110,6 @@ $font-size-h4: ${theme.typography.heading.h4} !default;
$font-size-h5: ${theme.typography.heading.h5} !default;
$font-size-h6: ${theme.typography.heading.h6} !default;
$headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
$headings-line-height: ${theme.typography.lineHeight.sm} !default;
// Components
@ -130,8 +129,8 @@ $page-sidebar-margin: 56px;
// Links
// -------------------------
$link-decoration: none !default;
$link-hover-decoration: none !default;
$link-decoration: ${theme.typography.link.decoration} !default;
$link-hover-decoration: ${theme.typography.link.hoverDecoration} !default;
// Tables
//
@ -166,13 +165,13 @@ $form-icon-danger: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www
// -------------------------
// Used for a bird's eye view of components dependent on the z-axis
// Try to avoid customizing these :)
$zindex-dropdown: 1000;
$zindex-navbar-fixed: 1020;
$zindex-sidemenu: 1025;
$zindex-tooltip: 1030;
$zindex-modal-backdrop: 1040;
$zindex-modal: 1050;
$zindex-typeahead: 1060;
$zindex-dropdown: ${theme.zIndex.dropdown};
$zindex-navbar-fixed: ${theme.zIndex.navbarFixed};
$zindex-sidemenu: ${theme.zIndex.sidemenu};
$zindex-tooltip: ${theme.zIndex.tooltip};
$zindex-modal-backdrop: ${theme.zIndex.modalBackdrop};
$zindex-modal: ${theme.zIndex.modal};
$zindex-typeahead: ${theme.zIndex.typeahead};
// Buttons
//
@ -197,10 +196,8 @@ $btn-semi-transparent: rgba(0, 0, 0, 0.2) !default;
$side-menu-width: 60px;
// dashboard
$dashboard-padding: 10px * 2;
$panel-horizontal-padding: 10;
$panel-vertical-padding: 5;
$panel-padding: 0px $panel-horizontal-padding + 0px $panel-vertical-padding + 0px $panel-horizontal-padding + 0px;
$dashboard-padding: $space-md;
$panel-padding: 0 $space-md $space-sm $space-md;
// tabs
$tabs-padding: 10px 15px 9px;

@ -4,7 +4,7 @@ const theme: GrafanaThemeCommons = {
name: 'Grafana Default',
typography: {
fontFamily: {
sansSerif: "'Roboto', Helvetica, Arial, sans-serif",
sansSerif: "'Roboto', 'Helvetica Neue', Arial, sans-serif",
monospace: "Menlo, Monaco, Consolas, 'Courier New', monospace",
},
size: {
@ -34,6 +34,10 @@ const theme: GrafanaThemeCommons = {
md: 4 / 3,
lg: 1.5,
},
link: {
decoration: 'none',
hoverDecoration: 'none',
},
},
breakpoints: {
xs: '0',
@ -66,6 +70,15 @@ const theme: GrafanaThemeCommons = {
horizontal: 10,
vertical: 5,
},
zIndex: {
dropdown: '1000',
navbarFixed: '1020',
sidemenu: '1025',
tooltip: '1030',
modalBackdrop: '1040',
modal: '1050',
typeahead: '1060',
},
};
export default theme;

@ -50,28 +50,29 @@ export enum NullValueMode {
/** View model projection of many time series */
export type TimeSeriesVMs = TimeSeriesVM[];
export enum ColumnType {
time = 'time', // or date
number = 'number',
string = 'string',
boolean = 'boolean',
other = 'other', // Object, Array, etc
}
export interface Column {
text: string;
title?: string;
type?: string;
sort?: boolean;
desc?: boolean;
text: string; // The column name
type?: ColumnType;
filterable?: boolean;
unit?: string;
dateFormat?: string; // Source data format
}
export interface TableData {
columns: Column[];
rows: any[];
type: string;
columnMap: any;
export interface Tags {
[key: string]: string;
}
export type SingleStatValue = number | string | null;
/*
* So we can add meta info like tags & series name
*/
export interface SingleStatValueInfo {
value: SingleStatValue;
export interface TableData {
name?: string;
columns: Column[];
rows: any[][];
tags?: Tags;
}

@ -1,12 +1,12 @@
import { ComponentClass } from 'react';
import { TimeSeries, LoadingState, TableData } from './data';
import { LoadingState, TableData } from './data';
import { TimeRange } from './time';
import { ScopedVars } from './datasource';
export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string;
export interface PanelProps<T = any> {
panelData: PanelData;
data?: TableData[];
timeRange: TimeRange;
loading: LoadingState;
options: T;
@ -16,11 +16,6 @@ export interface PanelProps<T = any> {
replaceVariables: InterpolateFunction;
}
export interface PanelData {
timeSeries?: TimeSeries[];
tableData?: TableData;
}
export interface PanelEditorProps<T = any> {
options: T;
onOptionsChange: (options: T) => void;

@ -46,6 +46,10 @@ export interface GrafanaThemeCommons {
h5: string;
h6: string;
};
link: {
decoration: string;
hoverDecoration: string;
};
};
spacing: {
d: string;
@ -71,6 +75,15 @@ export interface GrafanaThemeCommons {
horizontal: number;
vertical: number;
};
zIndex: {
dropdown: string;
navbarFixed: string;
sidemenu: string;
tooltip: string;
modalBackdrop: string;
modal: string;
typeahead: string;
};
}
export interface GrafanaTheme extends GrafanaThemeCommons {

@ -2,7 +2,6 @@
exports[`processTableData basic processing should generate a header and fix widths 1`] = `
Object {
"columnMap": Object {},
"columns": Array [
Object {
"text": "Column 1",
@ -31,13 +30,11 @@ Object {
null,
],
],
"type": "table",
}
`;
exports[`processTableData basic processing should read header and two rows 1`] = `
Object {
"columnMap": Object {},
"columns": Array [
Object {
"text": "a",
@ -61,6 +58,5 @@ Object {
6,
],
],
"type": "table",
}
`;

@ -0,0 +1,24 @@
import { getFlotPairs } from './flotPairs';
describe('getFlotPairs', () => {
const table = {
rows: [[1, 100, 'a'], [2, 200, 'b'], [3, 300, 'c']],
};
it('should get X and y', () => {
const pairs = getFlotPairs({ rows: table.rows, xIndex: 0, yIndex: 1 });
expect(pairs.length).toEqual(3);
expect(pairs[0].length).toEqual(2);
expect(pairs[0][0]).toEqual(1);
expect(pairs[0][1]).toEqual(100);
});
it('should work with strings', () => {
const pairs = getFlotPairs({ rows: table.rows, xIndex: 0, yIndex: 2 });
expect(pairs.length).toEqual(3);
expect(pairs[0].length).toEqual(2);
expect(pairs[0][0]).toEqual(1);
expect(pairs[0][1]).toEqual('a');
});
});

@ -0,0 +1,38 @@
// Types
import { NullValueMode } from '../types/index';
export interface FloatPairsOptions {
rows: any[][];
xIndex: number;
yIndex: number;
nullValueMode?: NullValueMode;
}
export function getFlotPairs({ rows, xIndex, yIndex, nullValueMode }: FloatPairsOptions): any[][] {
const ignoreNulls = nullValueMode === NullValueMode.Ignore;
const nullAsZero = nullValueMode === NullValueMode.AsZero;
const pairs: any[][] = [];
for (let i = 0; i < rows.length; i++) {
const x = rows[i][xIndex];
let y = rows[i][yIndex];
if (y === null) {
if (ignoreNulls) {
continue;
}
if (nullAsZero) {
y = 0;
}
}
// X must be a value
if (x === null) {
continue;
}
pairs.push([x, y]);
}
return pairs;
}

@ -1,5 +1,5 @@
export * from './processTimeSeries';
export * from './singlestat';
export * from './processTableData';
export * from './valueFormats/valueFormats';
export * from './colors';
export * from './namedColorsPalette';

@ -1,4 +1,4 @@
import { parseCSV } from './processTableData';
import { parseCSV, toTableData } from './processTableData';
describe('processTableData', () => {
describe('basic processing', () => {
@ -18,3 +18,41 @@ describe('processTableData', () => {
});
});
});
describe('toTableData', () => {
it('converts timeseries to table skipping nulls', () => {
const input1 = {
target: 'Field Name',
datapoints: [[100, 1], [200, 2]],
};
const input2 = {
// without target
target: '',
datapoints: [[100, 1], [200, 2]],
};
const data = toTableData([null, input1, input2, null, null]);
expect(data.length).toBe(2);
expect(data[0].columns[0].text).toBe(input1.target);
expect(data[0].rows).toBe(input1.datapoints);
// Default name
expect(data[1].columns[0].text).toEqual('Value');
});
it('keeps tableData unchanged', () => {
const input = {
columns: [{ text: 'A' }, { text: 'B' }, { text: 'C' }],
rows: [[100, 'A', 1], [200, 'B', 2], [300, 'C', 3]],
};
const data = toTableData([null, input, null, null]);
expect(data.length).toBe(1);
expect(data[0]).toBe(input);
});
it('supports null values OK', () => {
expect(toTableData([null, null, null, null])).toEqual([]);
expect(toTableData(undefined)).toEqual([]);
expect(toTableData((null as unknown) as any[])).toEqual([]);
expect(toTableData([])).toEqual([]);
});
});

@ -3,7 +3,7 @@ import isNumber from 'lodash/isNumber';
import Papa, { ParseError, ParseMeta } from 'papaparse';
// Types
import { TableData, Column } from '../types';
import { TableData, Column, TimeSeries, ColumnType } from '../types';
// Subset of all parse options
export interface TableParseOptions {
@ -70,8 +70,6 @@ export function matchRowSizes(table: TableData): TableData {
return {
columns,
rows: fixedRows,
type: table.type,
columnMap: table.columnMap,
};
}
@ -118,8 +116,6 @@ export function parseCSV(text: string, options?: TableParseOptions, details?: Ta
return {
columns: [],
rows: [],
type: 'table',
columnMap: {},
};
}
@ -130,11 +126,49 @@ export function parseCSV(text: string, options?: TableParseOptions, details?: Ta
return matchRowSizes({
columns: makeColumns(header),
rows: results.data,
type: 'table',
columnMap: {},
});
}
function convertTimeSeriesToTableData(timeSeries: TimeSeries): TableData {
return {
name: timeSeries.target,
columns: [
{
text: timeSeries.target || 'Value',
unit: timeSeries.unit,
},
{
text: 'Time',
type: ColumnType.time,
unit: 'dateTimeAsIso',
},
],
rows: timeSeries.datapoints,
};
}
export const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns');
export const toTableData = (results?: any[]): TableData[] => {
if (!results) {
return [];
}
return results
.filter(d => !!d)
.map(data => {
if (data.hasOwnProperty('columns')) {
return data as TableData;
}
if (data.hasOwnProperty('datapoints')) {
return convertTimeSeriesToTableData(data);
}
// TODO, try to convert JSON to table?
console.warn('Can not convert', data);
throw new Error('Unsupported data format');
});
};
export function sortTableData(data: TableData, sortIndex?: number, reverse = false): TableData {
if (isNumber(sortIndex)) {
const copy = {

@ -4,18 +4,45 @@ import isNumber from 'lodash/isNumber';
import { colors } from './colors';
// Types
import { TimeSeries, TimeSeriesVMs, NullValueMode, TimeSeriesValue } from '../types';
import { getFlotPairs } from './flotPairs';
import { TimeSeriesVMs, NullValueMode, TimeSeriesValue, TableData } from '../types';
interface Options {
timeSeries: TimeSeries[];
data: TableData[];
xColumn?: number; // Time (or null to guess)
yColumn?: number; // Value (or null to guess)
nullValueMode: NullValueMode;
}
export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeSeriesVMs {
const vmSeries = timeSeries.map((item, index) => {
// NOTE: this should move to processTableData.ts
// I left it as is so the merge changes are more clear.
export function processTimeSeries({ data, xColumn, yColumn, nullValueMode }: Options): TimeSeriesVMs {
const vmSeries = data.map((item, index) => {
if (!isNumber(xColumn)) {
xColumn = 1; // Default timeseries colum. TODO, find first time field!
}
if (!isNumber(yColumn)) {
yColumn = 0; // TODO, find first non-time field
}
// TODO? either % or throw error?
if (xColumn >= item.columns.length) {
throw new Error('invalid colum: ' + xColumn);
}
if (yColumn >= item.columns.length) {
throw new Error('invalid colum: ' + yColumn);
}
const colorIndex = index % colors.length;
const label = item.target;
const result = [];
const label = item.columns[yColumn].text;
// Use external calculator just to make sure it works :)
const result = getFlotPairs({
rows: item.rows,
xIndex: xColumn,
yIndex: yColumn,
nullValueMode,
});
// stat defaults
let total = 0;
@ -42,9 +69,9 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS
let previousValue = 0;
let previousDeltaUp = true;
for (let i = 0; i < item.datapoints.length; i++) {
currentValue = item.datapoints[i][0];
currentTime = item.datapoints[i][1];
for (let i = 0; i < item.rows.length; i++) {
currentValue = item.rows[i][yColumn];
currentTime = item.rows[i][xColumn];
if (typeof currentTime !== 'number') {
continue;
@ -95,7 +122,7 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS
if (previousValue > currentValue) {
// counter reset
previousDeltaUp = false;
if (i === item.datapoints.length - 1) {
if (i === item.rows.length - 1) {
// reset on last
delta += currentValue;
}
@ -118,8 +145,6 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS
allIsZero = false;
}
}
result.push([currentTime, currentValue]);
}
if (max === -Number.MAX_VALUE) {

@ -1,33 +0,0 @@
import { PanelData, NullValueMode, SingleStatValueInfo } from '../types';
import { processTimeSeries } from './processTimeSeries';
export interface SingleStatProcessingOptions {
panelData: PanelData;
stat: string;
}
//
// This is a temporary thing, waiting for a better data model and maybe unification between time series & table data
//
export function processSingleStatPanelData(options: SingleStatProcessingOptions): SingleStatValueInfo[] {
const { panelData, stat } = options;
if (panelData.timeSeries) {
const timeSeries = processTimeSeries({
timeSeries: panelData.timeSeries,
nullValueMode: NullValueMode.Null,
});
return timeSeries.map((series, index) => {
const value = stat !== 'name' ? series.stats[stat] : series.label;
return {
value: value,
};
});
} else if (panelData.tableData) {
throw { message: 'Panel data not supported' };
}
return [];
}

@ -2,7 +2,7 @@ import React from 'react';
interface StateHolderProps<T> {
initialState: T;
children: (currentState: T, updateState: (nextState: T) => void) => JSX.Element;
children: (currentState: T, updateState: (nextState: T) => void) => React.ReactNode;
}
export class UseState<T> extends React.Component<StateHolderProps<T>, { value: T; initialState: T }> {

@ -101,7 +101,7 @@ func (dc *databaseCache) Set(key string, value interface{}, expire time.Duration
// insert or update depending on if item already exist
if has {
sql := `UPDATE cache_data SET data=?, created=?, expire=? WHERE cache_key='?'`
sql := `UPDATE cache_data SET data=?, created_at=?, expires=? WHERE cache_key=?`
_, err = session.Exec(sql, data, getTime().Unix(), expiresInSeconds, key)
} else {
sql := `INSERT INTO cache_data (cache_key,data,created_at,expires) VALUES(?,?,?,?)`

@ -5,7 +5,6 @@ import (
"time"
"github.com/bmizerany/assert"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/services/sqlstore"
)
@ -54,3 +53,20 @@ func TestDatabaseStorageGarbageCollection(t *testing.T) {
_, err = db.Get("key5")
assert.Equal(t, err, nil)
}
func TestSecondSet(t *testing.T) {
var err error
sqlstore := sqlstore.InitTestDB(t)
db := &databaseCache{
SQLStore: sqlstore,
log: log.New("remotecache.database"),
}
obj := &CacheableStruct{String: "hey!"}
err = db.Set("killa-gorilla", obj, 0)
err = db.Set("killa-gorilla", obj, 0)
assert.Equal(t, err, nil)
}

@ -36,6 +36,10 @@ func (r RoleType) Includes(other RoleType) bool {
return other != ROLE_ADMIN
}
if r == ROLE_VIEWER {
return other == ROLE_VIEWER
}
return false
}

@ -40,137 +40,142 @@ var regionCache sync.Map
func init() {
metricsMap = map[string][]string{
"AWS/AmazonMQ": {"CpuUtilization", "HeapUsage", "NetworkIn", "NetworkOut", "TotalMessageCount", "ConsumerCount", "EnqueueCount", "EnqueueTime", "ExpiredCount", "InflightCount", "DispatchCount", "DequeueCount", "MemoryUsage", "ProducerCount", "QueueSize"},
"AWS/ApiGateway": {"4XXError", "5XXError", "CacheHitCount", "CacheMissCount", "Count", "IntegrationLatency", "Latency"},
"AWS/ApplicationELB": {"ActiveConnectionCount", "ClientTLSNegotiationErrorCount", "HealthyHostCount", "HTTPCode_ELB_4XX_Count", "HTTPCode_ELB_5XX_Count", "HTTPCode_Target_2XX_Count", "HTTPCode_Target_3XX_Count", "HTTPCode_Target_4XX_Count", "HTTPCode_Target_5XX_Count", "IPv6ProcessedBytes", "IPv6RequestCount", "NewConnectionCount", "ProcessedBytes", "RejectedConnectionCount", "RequestCount", "RequestCountPerTarget", "TargetConnectionErrorCount", "TargetResponseTime", "TargetTLSNegotiationErrorCount", "UnHealthyHostCount"},
"AWS/AutoScaling": {"GroupMinSize", "GroupMaxSize", "GroupDesiredCapacity", "GroupInServiceInstances", "GroupPendingInstances", "GroupStandbyInstances", "GroupTerminatingInstances", "GroupTotalInstances"},
"AWS/Billing": {"EstimatedCharges"},
"AWS/CloudFront": {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"},
"AWS/CloudSearch": {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"},
"AWS/CloudHSM": {"HsmUnhealthy", "HsmTemperature", "HsmKeysSessionOccupied", "HsmKeysTokenOccupied", "HsmSslCtxsOccupied", "HsmSessionCount", "HsmUsersAvailable", "HsmUsersMax", "InterfaceEth2OctetsInput", "InterfaceEth2OctetsOutput"},
"AWS/CodeBuild": {"BuildDuration", "Builds", "DownloadSourceDuration", "Duration", "FailedBuilds", "FinalizingDuration", "InstallDuration", "PostBuildDuration", "PreBuildDuration", "ProvisioningDuration", "QueuedDuration", "SubmittedDuration", "SucceededBuilds", "UploadArtifactsDuration"},
"AWS/Connect": {"CallsBreachingConcurrencyQuota", "CallBackNotDialableNumber", "CallRecordingUploadError", "CallsPerInterval", "ConcurrentCalls", "ConcurrentCallsPercentage", "ContactFlowErrors", "ContactFlowFatalErrors", "LongestQueueWaitTime", "MissedCalls", "MisconfiguredPhoneNumbers", "PublicSigningKeyUsage", "QueueCapacityExceededError", "QueueSize", "ThrottledCalls", "ToInstancePacketLossRate"},
"AWS/DMS": {"FreeableMemory", "WriteIOPS", "ReadIOPS", "WriteThroughput", "ReadThroughput", "WriteLatency", "ReadLatency", "SwapUsage", "NetworkTransmitThroughput", "NetworkReceiveThroughput", "FullLoadThroughputBandwidthSource", "FullLoadThroughputBandwidthTarget", "FullLoadThroughputRowsSource", "FullLoadThroughputRowsTarget", "CDCIncomingChanges", "CDCChangesMemorySource", "CDCChangesMemoryTarget", "CDCChangesDiskSource", "CDCChangesDiskTarget", "CDCThroughputBandwidthTarget", "CDCThroughputRowsSource", "CDCThroughputRowsTarget", "CDCLatencySource", "CDCLatencyTarget"},
"AWS/DX": {"ConnectionState", "ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionCRCErrorCount", "ConnectionLightLevelTx", "ConnectionLightLevelRx"},
"AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReturnedBytes", "ReturnedItemCount", "ReturnedRecordsCount", "SuccessfulRequestLatency", "SystemErrors", "TimeToLiveDeletedItemCount", "ThrottledRequests", "UserErrors", "WriteThrottleEvents"},
"AWS/EBS": {"VolumeReadBytes", "VolumeWriteBytes", "VolumeReadOps", "VolumeWriteOps", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeIdleTime", "VolumeQueueLength", "VolumeThroughputPercentage", "VolumeConsumedReadWriteOps", "BurstBalance"},
"AWS/EC2": {"CPUCreditUsage", "CPUCreditBalance", "CPUUtilization", "DiskReadOps", "DiskWriteOps", "DiskReadBytes", "DiskWriteBytes", "NetworkIn", "NetworkOut", "NetworkPacketsIn", "NetworkPacketsOut", "StatusCheckFailed", "StatusCheckFailed_Instance", "StatusCheckFailed_System"},
"AWS/EC2/API": {"ClientErrors", "RequestLimitExceeded", "ServerErrors", "SuccessfulCalls"},
"AWS/EC2Spot": {"AvailableInstancePoolsCount", "BidsSubmittedForCapacity", "EligibleInstancePoolCount", "FulfilledCapacity", "MaxPercentCapacityAllocation", "PendingCapacity", "PercentCapacityAllocation", "TargetCapacity", "TerminatingCapacity"},
"AWS/ECS": {"CPUReservation", "MemoryReservation", "CPUUtilization", "MemoryUtilization"},
"AWS/EFS": {"BurstCreditBalance", "ClientConnections", "DataReadIOBytes", "DataWriteIOBytes", "MetadataIOBytes", "TotalIOBytes", "PermittedThroughput", "PercentIOLimit"},
"AWS/ELB": {"HealthyHostCount", "UnHealthyHostCount", "RequestCount", "Latency", "HTTPCode_ELB_4XX", "HTTPCode_ELB_5XX", "HTTPCode_Backend_2XX", "HTTPCode_Backend_3XX", "HTTPCode_Backend_4XX", "HTTPCode_Backend_5XX", "BackendConnectionErrors", "SurgeQueueLength", "SpilloverCount", "EstimatedALBActiveConnectionCount", "EstimatedALBConsumedLCUs", "EstimatedALBNewConnectionCount", "EstimatedProcessedBytes"},
"AWS/ElastiCache": {
"CPUUtilization", "FreeableMemory", "NetworkBytesIn", "NetworkBytesOut", "SwapUsage",
"BytesUsedForCacheItems", "BytesReadIntoMemcached", "BytesWrittenOutFromMemcached", "CasBadval", "CasHits", "CasMisses", "CmdFlush", "CmdGet", "CmdSet", "CurrConnections", "CurrItems", "DecrHits", "DecrMisses", "DeleteHits", "DeleteMisses", "Evictions", "GetHits", "GetMisses", "IncrHits", "IncrMisses", "Reclaimed",
"BytesUsedForHash", "CmdConfigGet", "CmdConfigSet", "CmdTouch", "CurrConfig", "EvictedUnfetched", "ExpiredUnfetched", "SlabsMoved", "TouchHits", "TouchMisses",
"NewConnections", "NewItems", "UnusedMemory",
"BytesUsedForCache", "CacheHits", "CacheMisses", "CurrConnections", "Evictions", "HyperLogLogBasedCmds", "NewConnections", "Reclaimed", "ReplicationBytes", "ReplicationLag", "SaveInProgress",
"CurrItems", "GetTypeCmds", "HashBasedCmds", "KeyBasedCmds", "ListBasedCmds", "SetBasedCmds", "SetTypeCmds", "SortedSetBasedCmds", "StringBasedCmds",
},
"AWS/ElasticBeanstalk": {
"EnvironmentHealth",
"ApplicationLatencyP10", "ApplicationLatencyP50", "ApplicationLatencyP75", "ApplicationLatencyP85", "ApplicationLatencyP90", "ApplicationLatencyP95", "ApplicationLatencyP99", "ApplicationLatencyP99.9",
"ApplicationRequests2xx", "ApplicationRequests3xx", "ApplicationRequests4xx", "ApplicationRequests5xx", "ApplicationRequestsTotal",
"CPUIdle", "CPUIowait", "CPUIrq", "CPUNice", "CPUSoftirq", "CPUSystem", "CPUUser",
"InstanceHealth", "InstancesDegraded", "InstancesInfo", "InstancesNoData", "InstancesOk", "InstancesPending", "InstancesSevere", "InstancesUnknown", "InstancesWarning",
"LoadAverage1min", "LoadAverage5min",
"RootFilesystemUtil",
},
"AWS/ElasticMapReduce": {"IsIdle", "JobsRunning", "JobsFailed",
"MapTasksRunning", "MapTasksRemaining", "MapSlotsOpen", "RemainingMapTasksPerSlot", "ReduceTasksRunning", "ReduceTasksRemaining", "ReduceSlotsOpen",
"CoreNodesRunning", "CoreNodesPending", "LiveDataNodes", "TaskNodesRunning", "TaskNodesPending", "LiveTaskTrackers",
"S3BytesWritten", "S3BytesRead", "HDFSUtilization", "HDFSBytesRead", "HDFSBytesWritten", "MissingBlocks", "TotalLoad",
"BackupFailed", "MostRecentBackupDuration", "TimeSinceLastSuccessfulBackup",
"IsIdle", "ContainerAllocated", "ContainerReserved", "ContainerPending", "AppsCompleted", "AppsFailed", "AppsKilled", "AppsPending", "AppsRunning", "AppsSubmitted",
"CoreNodesRunning", "CoreNodesPending", "LiveDataNodes", "MRTotalNodes", "MRActiveNodes", "MRLostNodes", "MRUnhealthyNodes", "MRDecommissionedNodes", "MRRebootedNodes",
"S3BytesWritten", "S3BytesRead", "HDFSUtilization", "HDFSBytesRead", "HDFSBytesWritten", "MissingBlocks", "CorruptBlocks", "TotalLoad", "MemoryTotalMB", "MemoryReservedMB", "MemoryAvailableMB", "MemoryAllocatedMB", "PendingDeletionBlocks", "UnderReplicatedBlocks", "DfsPendingReplicationBlocks", "CapacityRemainingGB",
"HbaseBackupFailed", "MostRecentBackupDuration", "TimeSinceLastSuccessfulBackup"},
"AWS/ES": {"ClusterStatus.green", "ClusterStatus.yellow", "ClusterStatus.red", "ClusterUsedSpace", "Nodes", "SearchableDocuments", "DeletedDocuments", "CPUCreditBalance", "CPUUtilization", "FreeStorageSpace", "JVMMemoryPressure", "AutomatedSnapshotFailure", "MasterCPUCreditBalance", "MasterCPUUtilization", "MasterFreeStorageSpace", "MasterJVMMemoryPressure", "ReadLatency", "WriteLatency", "ReadThroughput", "WriteThroughput", "DiskQueueDepth", "ReadIOPS", "WriteIOPS"},
"AWS/Events": {"Invocations", "FailedInvocations", "TriggeredRules", "MatchedEvents", "ThrottledRules"},
"AWS/Firehose": {"DeliveryToElasticsearch.Bytes", "DeliveryToElasticsearch.Records", "DeliveryToElasticsearch.Success", "DeliveryToRedshift.Bytes", "DeliveryToRedshift.Records", "DeliveryToRedshift.Success", "DeliveryToS3.Bytes", "DeliveryToS3.DataFreshness", "DeliveryToS3.Records", "DeliveryToS3.Success", "IncomingBytes", "IncomingRecords", "DescribeDeliveryStream.Latency", "DescribeDeliveryStream.Requests", "ListDeliveryStreams.Latency", "ListDeliveryStreams.Requests", "PutRecord.Bytes", "PutRecord.Latency", "PutRecord.Requests", "PutRecordBatch.Bytes", "PutRecordBatch.Latency", "PutRecordBatch.Records", "PutRecordBatch.Requests", "UpdateDeliveryStream.Latency", "UpdateDeliveryStream.Requests"},
"AWS/IoT": {"PublishIn.Success", "PublishOut.Success", "Subscribe.Success", "Ping.Success", "Connect.Success", "GetThingShadow.Accepted"},
"AWS/Kinesis": {"GetRecords.Bytes", "GetRecords.IteratorAge", "GetRecords.IteratorAgeMilliseconds", "GetRecords.Latency", "GetRecords.Records", "GetRecords.Success", "IncomingBytes", "IncomingRecords", "PutRecord.Bytes", "PutRecord.Latency", "PutRecord.Success", "PutRecords.Bytes", "PutRecords.Latency", "PutRecords.Records", "PutRecords.Success", "ReadProvisionedThroughputExceeded", "WriteProvisionedThroughputExceeded", "IteratorAgeMilliseconds", "OutgoingBytes", "OutgoingRecords"},
"AWS/KinesisAnalytics": {"Bytes", "MillisBehindLatest", "Records", "Success"},
"AWS/Lambda": {"Invocations", "Errors", "Duration", "Throttles", "IteratorAge"},
"AWS/AppSync": {"Latency", "4XXError", "5XXError"},
"AWS/Logs": {"IncomingBytes", "IncomingLogEvents", "ForwardedBytes", "ForwardedLogEvents", "DeliveryErrors", "DeliveryThrottling"},
"AWS/ML": {"PredictCount", "PredictFailureCount"},
"AWS/NATGateway": {"PacketsOutToDestination", "PacketsOutToSource", "PacketsInFromSource", "PacketsInFromDestination", "BytesOutToDestination", "BytesOutToSource", "BytesInFromSource", "BytesInFromDestination", "ErrorPortAllocation", "ActiveConnectionCount", "ConnectionAttemptCount", "ConnectionEstablishedCount", "IdleTimeoutCount", "PacketsDropCount"},
"AWS/Neptune": {"CPUUtilization", "ClusterReplicaLag", "ClusterReplicaLagMaximum", "ClusterReplicaLagMinimum", "EngineUptime", "FreeableMemory", "FreeLocalStorage", "GremlinHttp1xx", "GremlinHttp2xx", "GremlinHttp4xx", "GremlinHttp5xx", "GremlinErrors", "GremlinRequests", "GremlinRequestsPerSec", "GremlinWebSocketSuccess", "GremlinWebSocketClientErrors", "GremlinWebSocketServerErrors", "GremlinWebSocketAvailableConnections", "Http1xx", "Http2xx", "Http4xx", "Http5xx", "Http100", "Http101", "Http200", "Http400", "Http403", "Http405", "Http413", "Http429", "Http500", "Http501", "LoaderErrors", "LoaderRequests", "NetworkReceiveThroughput", "NetworkThroughput", "NetworkTransmitThroughput", "SparqlHttp1xx", "SparqlHttp2xx", "SparqlHttp4xx", "SparqlHttp5xx", "SparqlErrors", "SparqlRequests", "SparqlRequestsPerSec", "StatusErrors", "StatusRequests", "VolumeBytesUsed", "VolumeReadIOPs", "VolumeWriteIOPs"},
"AWS/NetworkELB": {"ActiveFlowCount", "ConsumedLCUs", "HealthyHostCount", "NewFlowCount", "ProcessedBytes", "TCP_Client_Reset_Count", "TCP_ELB_Reset_Count", "TCP_Target_Reset_Count", "UnHealthyHostCount"},
"AWS/OpsWorks": {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"},
"AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "QueriesCompletedPerSecond", "QueryDuration", "QueryRuntimeBreakdown", "ReadIOPS", "ReadLatency", "ReadThroughput", "WLMQueriesCompletedPerSecond", "WLMQueryDuration", "WLMQueueLength", "WriteIOPS", "WriteLatency", "WriteThroughput"},
"AWS/RDS": {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "BurstBalance", "CommitLatency", "CommitThroughput", "BinLogDiskUsage", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "EngineUptime", "FailedSqlStatements", "FreeableMemory", "FreeLocalStorage", "FreeStorageSpace", "InsertLatency", "InsertThroughput", "LoginFailures", "MaximumUsedTransactionIDs", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "NetworkThroughput", "Queries", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "ServerlessDatabaseCapacity", "SwapUsage", "TotalConnections", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"},
"AWS/Route53": {"ChildHealthCheckHealthyCount", "HealthCheckStatus", "HealthCheckPercentageHealthy", "ConnectionTime", "SSLHandshakeTime", "TimeToFirstByte"},
"AWS/S3": {"BucketSizeBytes", "NumberOfObjects", "AllRequests", "GetRequests", "PutRequests", "DeleteRequests", "HeadRequests", "PostRequests", "ListRequests", "BytesDownloaded", "BytesUploaded", "4xxErrors", "5xxErrors", "FirstByteLatency", "TotalRequestLatency"},
"AWS/SES": {"Bounce", "Complaint", "Delivery", "Reject", "Send", "Reputation.BounceRate", "Reputation.ComplaintRate"},
"AWS/SNS": {"NumberOfMessagesPublished", "PublishSize", "NumberOfNotificationsDelivered", "NumberOfNotificationsFailed"},
"AWS/SQS": {"NumberOfMessagesSent", "SentMessageSize", "NumberOfMessagesReceived", "NumberOfEmptyReceives", "NumberOfMessagesDeleted", "ApproximateAgeOfOldestMessage", "ApproximateNumberOfMessagesDelayed", "ApproximateNumberOfMessagesVisible", "ApproximateNumberOfMessagesNotVisible"},
"AWS/States": {"ExecutionTime", "ExecutionThrottled", "ExecutionsAborted", "ExecutionsFailed", "ExecutionsStarted", "ExecutionsSucceeded", "ExecutionsTimedOut", "ActivityRunTime", "ActivityScheduleTime", "ActivityTime", "ActivitiesFailed", "ActivitiesHeartbeatTimedOut", "ActivitiesScheduled", "ActivitiesScheduled", "ActivitiesSucceeded", "ActivitiesTimedOut", "LambdaFunctionRunTime", "LambdaFunctionScheduleTime", "LambdaFunctionTime", "LambdaFunctionsFailed", "LambdaFunctionsHeartbeatTimedOut", "LambdaFunctionsScheduled", "LambdaFunctionsStarted", "LambdaFunctionsSucceeded", "LambdaFunctionsTimedOut"},
"AWS/StorageGateway": {"CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "CloudBytesDownloaded", "CloudDownloadLatency", "CloudBytesUploaded", "UploadBufferFree", "UploadBufferPercentUsed", "UploadBufferUsed", "QueuedWrites", "ReadBytes", "ReadTime", "TotalCacheSize", "WriteBytes", "WriteTime", "TimeSinceLastRecoveryPoint", "WorkingStorageFree", "WorkingStoragePercentUsed", "WorkingStorageUsed",
"CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "ReadBytes", "ReadTime", "WriteBytes", "WriteTime", "QueuedWrites"},
"AWS/SWF": {"DecisionTaskScheduleToStartTime", "DecisionTaskStartToCloseTime", "DecisionTasksCompleted", "StartedDecisionTasksTimedOutOnClose", "WorkflowStartToCloseTime", "WorkflowsCanceled", "WorkflowsCompleted", "WorkflowsContinuedAsNew", "WorkflowsFailed", "WorkflowsTerminated", "WorkflowsTimedOut",
"ActivityTaskScheduleToCloseTime", "ActivityTaskScheduleToStartTime", "ActivityTaskStartToCloseTime", "ActivityTasksCanceled", "ActivityTasksCompleted", "ActivityTasksFailed", "ScheduledActivityTasksTimedOutOnClose", "ScheduledActivityTasksTimedOutOnStart", "StartedActivityTasksTimedOutOnClose", "StartedActivityTasksTimedOutOnHeartbeat"},
"AWS/VPN": {"TunnelState", "TunnelDataIn", "TunnelDataOut"},
"Rekognition": {"SuccessfulRequestCount", "ThrottledCount", "ResponseTime", "DetectedFaceCount", "DetectedLabelCount", "ServerErrorCount", "UserErrorCount"},
"WAF": {"AllowedRequests", "BlockedRequests", "CountedRequests"},
"AWS/WorkSpaces": {"Available", "Unhealthy", "ConnectionAttempt", "ConnectionSuccess", "ConnectionFailure", "SessionLaunchTime", "InSessionLatency", "SessionDisconnect"},
"KMS": {"SecondsUntilKeyMaterialExpiration"},
"AWS/AmazonMQ": {"ConsumerCount", "CpuCreditBalance", "CpuUtilization", "CurrentConnectionsCount", "DequeueCount", "DispatchCount", "EnqueueCount", "EnqueueTime", "ExpiredCount", "HeapUsage", "InflightCount", "JournalFilesForFastRecovery", "JournalFilesForFullRecovery", "MemoryUsage", "NetworkIn", "NetworkOut", "OpenTransactionsCount", "ProducerCount", "QueueSize", "StorePercentUsage", "TotalConsumerCount", "TotalMessageCount", "TotalProducerCount"},
"AWS/ApiGateway": {"4XXError", "5XXError", "CacheHitCount", "CacheMissCount", "Count", "IntegrationLatency", "Latency"},
"AWS/AppStream": {"ActualCapacity", "AvailableCapacity", "CapacityUtilization", "DesiredCapacity", "InUseCapacity", "InsufficientCapacityError", "PendingCapacity", "RunningCapacity"},
"AWS/AppSync": {"4XXError", "5XXError", "Latency"},
"AWS/ApplicationELB": {"ActiveConnectionCount", "ClientTLSNegotiationErrorCount", "ConsumedLCUs", "ELBAuthError", "ELBAuthFailure", "ELBAuthLatency", "ELBAuthRefreshTokenSuccess", "ELBAuthSuccess", "ELBAuthUserClaimsSizeExceeded", "HTTPCode_ELB_3XX_Count", "HTTPCode_ELB_4XX_Count", "HTTPCode_ELB_5XX_Count", "HTTPCode_Target_2XX_Count", "HTTPCode_Target_3XX_Count", "HTTPCode_Target_4XX_Count", "HTTPCode_Target_5XX_Count", "HTTP_Fixed_Response_Count", "HTTP_Redirect_Count", "HTTP_Redirect_Url_Limit_Exceeded_Count", "HealthyHostCount", "IPv6ProcessedBytes", "IPv6RequestCount", "LambdaInternalError", "LambdaTargetProcessedBytes", "LambdaUserError", "NewConnectionCount", "NonStickyRequestCount", "ProcessedBytes", "RejectedConnectionCount", "RequestCount", "RequestCountPerTarget", "RuleEvaluations", "StandardProcessedBytes", "TargetConnectionErrorCount", "TargetResponseTime", "TargetTLSNegotiationErrorCount", "UnHealthyHostCount"},
"AWS/AutoScaling": {"GroupDesiredCapacity", "GroupInServiceInstances", "GroupMaxSize", "GroupMinSize", "GroupPendingInstances", "GroupStandbyInstances", "GroupTerminatingInstances", "GroupTotalInstances"},
"AWS/Billing": {"EstimatedCharges"},
"AWS/CloudFront": {"4xxErrorRate", "5xxErrorRate", "BytesDownloaded", "BytesUploaded", "Requests", "TotalErrorRate"},
"AWS/CloudHSM": {"HsmKeysSessionOccupied", "HsmKeysTokenOccupied", "HsmSessionCount", "HsmSslCtxsOccupied", "HsmTemperature", "HsmUnhealthy", "HsmUsersAvailable", "HsmUsersMax", "InterfaceEth2OctetsInput", "InterfaceEth2OctetsOutput"},
"AWS/CloudSearch": {"IndexUtilization", "Partitions", "SearchableDocuments", "SuccessfulRequests"},
"AWS/CodeBuild": {"BuildDuration", "Builds", "DownloadSourceDuration", "Duration", "FailedBuilds", "FinalizingDuration", "InstallDuration", "PostBuildDuration", "PreBuildDuration", "ProvisioningDuration", "QueuedDuration", "SubmittedDuration", "SucceededBuilds", "UploadArtifactsDuration"},
"AWS/Connect": {"CallBackNotDialableNumber", "CallRecordingUploadError", "CallsBreachingConcurrencyQuota", "CallsPerInterval", "ConcurrentCalls", "ConcurrentCallsPercentage", "ContactFlowErrors", "ContactFlowFatalErrors", "LongestQueueWaitTime", "MisconfiguredPhoneNumbers", "MissedCalls", "PublicSigningKeyUsage", "QueueCapacityExceededError", "QueueSize", "ThrottledCalls", "ToInstancePacketLossRate"},
"AWS/DDoSProtection": {"AllowedRequests", "BlockedRequests", "CountedRequests", "DDoSAttackBitsPerSecond", "DDoSAttackPacketsPerSecond", "DDoSAttackRequestsPerSecond", "DDoSDetected", "PassedRequests"},
"AWS/DMS": {"CDCChangesDiskSource", "CDCChangesDiskTarget", "CDCChangesMemorySource", "CDCChangesMemoryTarget", "CDCIncomingChanges", "CDCLatencySource", "CDCLatencyTarget", "CDCThroughputBandwidthTarget", "CDCThroughputRowsSource", "CDCThroughputRowsTarget", "FreeableMemory", "FullLoadThroughputBandwidthSource", "FullLoadThroughputBandwidthTarget", "FullLoadThroughputRowsSource", "FullLoadThroughputRowsTarget", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "ReadIOPS", "ReadLatency", "ReadThroughput", "SwapUsage", "WriteIOPS", "WriteLatency", "WriteThroughput"},
"AWS/DX": {"ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionCRCErrorCount", "ConnectionLightLevelRx", "ConnectionLightLevelTx", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionState"},
"AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "PendingReplicationCount", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReplicationLatency", "ReturnedBytes", "ReturnedItemCount", "ReturnedRecordsCount", "SuccessfulRequestLatency", "SystemErrors", "ThrottledRequests", "TimeToLiveDeletedItemCount", "UserErrors", "WriteThrottleEvents"},
"AWS/EBS": {"BurstBalance", "VolumeConsumedReadWriteOps", "VolumeIdleTime", "VolumeQueueLength", "VolumeReadBytes", "VolumeReadOps", "VolumeThroughputPercentage", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeWriteBytes", "VolumeWriteOps"},
"AWS/EC2": {"CPUCreditBalance", "CPUCreditUsage", "CPUSurplusCreditBalance", "CPUSurplusCreditsCharged", "CPUUtilization", "DiskReadBytes", "DiskReadOps", "DiskWriteBytes", "DiskWriteOps", "EBSByteBalance%", "EBSIOBalance%", "EBSReadBytes", "EBSReadOps", "EBSWriteBytes", "EBSWriteOps", "NetworkIn", "NetworkOut", "NetworkPacketsIn", "NetworkPacketsOut", "StatusCheckFailed", "StatusCheckFailed_Instance", "StatusCheckFailed_System"},
"AWS/EC2/API": {"ClientErrors", "RequestLimitExceeded", "ServerErrors", "SuccessfulCalls"},
"AWS/EC2Spot": {"AvailableInstancePoolsCount", "BidsSubmittedForCapacity", "EligibleInstancePoolCount", "FulfilledCapacity", "MaxPercentCapacityAllocation", "PendingCapacity", "PercentCapacityAllocation", "TargetCapacity", "TerminatingCapacity"},
"AWS/ECS": {"CPUReservation", "CPUUtilization", "GPUReservation", "MemoryReservation", "MemoryUtilization"},
"AWS/EFS": {"BurstCreditBalance", "ClientConnections", "DataReadIOBytes", "DataWriteIOBytes", "MetadataIOBytes", "PercentIOLimit", "PermittedThroughput", "TotalIOBytes"},
"AWS/ELB": {"BackendConnectionErrors", "EstimatedALBActiveConnectionCount", "EstimatedALBConsumedLCUs", "EstimatedALBNewConnectionCount", "EstimatedProcessedBytes", "HTTPCode_Backend_2XX", "HTTPCode_Backend_3XX", "HTTPCode_Backend_4XX", "HTTPCode_Backend_5XX", "HTTPCode_ELB_4XX", "HTTPCode_ELB_5XX", "HealthyHostCount", "Latency", "RequestCount", "SpilloverCount", "SurgeQueueLength", "UnHealthyHostCount"},
"AWS/ES": {"AutomatedSnapshotFailure", "CPUCreditBalance", "CPUUtilization", "ClusterIndexWritesBlocked", "ClusterStatus.green", "ClusterStatus.red", "ClusterStatus.yellow", "ClusterUsedSpace", "DeletedDocuments", "DiskQueueDepth", "ElasticsearchRequests", "FreeStorageSpace", "IndexingLatency", "IndexingRate", "InvalidHostHeaderRequests", "JVMGCOldCollectionCount", "JVMGCOldCollectionTime", "JVMGCYoungCollectionCount", "JVMGCYoungCollectionTime", "JVMMemoryPressure", "KMSKeyError", "KMSKeyInaccessible", "KibanaHealthyNodes", "MasterCPUCreditBalance", "MasterCPUUtilization", "MasterFreeStorageSpace", "MasterJVMMemoryPressure", "MasterReachableFromNode", "Nodes", "ReadIOPS", "ReadLatency", "ReadThroughput", "RequestCount", "SearchLatency", "SearchRate", "SearchableDocuments", "SysMemoryUtilization", "ThreadpoolBulkQueue", "ThreadpoolBulkRejected", "ThreadpoolBulkThreads", "ThreadpoolForce_mergeQueue", "ThreadpoolForce_mergeRejected", "ThreadpoolForce_mergeThreads", "ThreadpoolIndexQueue", "ThreadpoolIndexRejected", "ThreadpoolIndexThreads", "ThreadpoolSearchQueue", "ThreadpoolSearchRejected", "ThreadpoolSearchThreads", "WriteIOPS", "WriteLatency", "WriteThroughput"},
"AWS/ElastiCache": {"ActiveDefragHits", "BytesReadIntoMemcached", "BytesUsedForCache", "BytesUsedForCacheItems", "BytesUsedForHash", "BytesWrittenOutFromMemcached", "CPUUtilization", "CacheHits", "CacheMisses", "CasBadval", "CasHits", "CasMisses", "CmdConfigGet", "CmdConfigSet", "CmdFlush", "CmdGet", "CmdSet", "CmdTouch", "CurrConfig", "CurrConnections", "CurrItems", "DecrHits", "DecrMisses", "DeleteHits", "DeleteMisses", "EngineCPUUtilization", "EvictedUnfetched", "Evictions", "ExpiredUnfetched", "FreeableMemory", "GetHits", "GetMisses", "GetTypeCmds", "HashBasedCmds", "HyperLogLogBasedCmds", "IncrHits", "IncrMisses", "KeyBasedCmds", "ListBasedCmds", "NetworkBytesIn", "NetworkBytesOut", "NewConnections", "NewItems", "Reclaimed", "ReplicationBytes", "ReplicationLag", "SaveInProgress", "SetBasedCmds", "SetTypeCmds", "SlabsMoved", "SortedSetBasedCmds", "StringBasedCmds", "SwapUsage", "TouchHits", "TouchMisses", "UnusedMemory"},
"AWS/ElasticBeanstalk": {"ApplicationLatencyP10", "ApplicationLatencyP50", "ApplicationLatencyP75", "ApplicationLatencyP85", "ApplicationLatencyP90", "ApplicationLatencyP95", "ApplicationLatencyP99", "ApplicationLatencyP99.9", "ApplicationRequests2xx", "ApplicationRequests3xx", "ApplicationRequests4xx", "ApplicationRequests5xx", "ApplicationRequestsTotal", "CPUIdle", "CPUIowait", "CPUIrq", "CPUNice", "CPUSoftirq", "CPUSystem", "CPUUser", "EnvironmentHealth", "InstanceHealth", "InstancesDegraded", "InstancesInfo", "InstancesNoData", "InstancesOk", "InstancesPending", "InstancesSevere", "InstancesUnknown", "InstancesWarning", "LoadAverage1min", "LoadAverage5min", "RootFilesystemUtil"},
"AWS/ElasticMapReduce": {"AppsCompleted", "AppsFailed", "AppsKilled", "AppsPending", "AppsRunning", "AppsSubmitted", "BackupFailed", "CapacityRemainingGB", "Cluster Status", "ContainerAllocated", "ContainerPending", "ContainerPendingRatio", "ContainerReserved", "CoreNodesPending", "CoreNodesRunning", "CorruptBlocks", "DfsPendingReplicationBlocks", "HBase", "HDFSBytesRead", "HDFSBytesWritten", "HDFSUtilization", "HbaseBackupFailed", "IO", "IsIdle", "JobsFailed", "JobsRunning", "LiveDataNodes", "LiveTaskTrackers", "MRActiveNodes", "MRDecommissionedNodes", "MRLostNodes", "MRRebootedNodes", "MRTotalNodes", "MRUnhealthyNodes", "Map/Reduce", "MapSlotsOpen", "MapTasksRemaining", "MapTasksRunning", "MemoryAllocatedMB", "MemoryAvailableMB", "MemoryReservedMB", "MemoryTotalMB", "MissingBlocks", "MostRecentBackupDuration", "Node Status", "PendingDeletionBlocks", "ReduceSlotsOpen", "ReduceTasksRemaining", "ReduceTasksRunning", "RemainingMapTasksPerSlot", "S3BytesRead", "S3BytesWritten", "TaskNodesPending", "TaskNodesRunning", "TimeSinceLastSuccessfulBackup", "TotalLoad", "UnderReplicatedBlocks", "YARNMemoryAvailablePercentage"},
"AWS/ElasticTranscoder": {"Billed Audio Output", "Billed HD Output", "Billed SD Output", "Errors", "Jobs Completed", "Jobs Errored", "Outputs per Job", "Standby Time", "Throttles"},
"AWS/Events": {"DeadLetterInvocations", "FailedInvocations", "Invocations", "MatchedEvents", "ThrottledRules", "TriggeredRules"},
"AWS/FSx": {"DataReadBytes", "DataReadOperations", "DataWriteBytes", "DataWriteOperations", "FreeDataStorageCapacity", "MetadataOperations"},
"AWS/Firehose": {"BackupToS3.Bytes", "BackupToS3.DataFreshness", "BackupToS3.Records", "BackupToS3.Success", "DataReadFromKinesisStream.Bytes", "DataReadFromKinesisStream.Records", "DeliveryToElasticsearch.Bytes", "DeliveryToElasticsearch.Records", "DeliveryToElasticsearch.Success", "DeliveryToRedshift.Bytes", "DeliveryToRedshift.Records", "DeliveryToRedshift.Success", "DeliveryToS3.Bytes", "DeliveryToS3.DataFreshness", "DeliveryToS3.Records", "DeliveryToS3.Success", "DeliveryToSplunk.Bytes", "DeliveryToSplunk.DataFreshness", "DeliveryToSplunk.Records", "DeliveryToSplunk.Success", "DescribeDeliveryStream.Latency", "DescribeDeliveryStream.Requests", "ExecuteProcessing.Duration", "ExecuteProcessing.Success", "FailedConversion.Bytes", "FailedConversion.Records", "IncomingBytes", "IncomingRecords", "KinesisMillisBehindLatest", "ListDeliveryStreams.Latency", "ListDeliveryStreams.Requests", "PutRecord.Bytes", "PutRecord.Latency", "PutRecord.Requests", "PutRecordBatch.Bytes", "PutRecordBatch.Latency", "PutRecordBatch.Records", "PutRecordBatch.Requests", "SucceedConversion.Bytes", "SucceedConversion.Records", "SucceedProcessing.Bytes", "SucceedProcessing.Records", "ThrottledDescribeStream", "ThrottledGetRecords", "ThrottledGetShardIterator", "UpdateDeliveryStream.Latency", "UpdateDeliveryStream.Requests"},
"AWS/Glue": {"glue.driver.BlockManager.disk.diskSpaceUsed_MB", "glue.driver.ExecutorAllocationManager.executors.numberAllExecutors", "glue.driver.ExecutorAllocationManager.executors.numberMaxNeededExecutors", "glue.driver.aggregate.bytesRead", "glue.driver.aggregate.elapsedTime", "glue.driver.aggregate.numCompletedStages", "glue.driver.aggregate.numCompletedTasks", "glue.driver.aggregate.numFailedTasks", "glue.driver.aggregate.numKilledTasks", "glue.driver.aggregate.recordsRead", "glue.driver.aggregate.shuffleBytesWritten", "glue.driver.aggregate.shuffleLocalBytesRead", "glue.driver.jvm.heap.usage glue.executorId.jvm.heap.usage glue.ALL.jvm.heap.usage", "glue.driver.jvm.heap.used glue.executorId.jvm.heap.used glue.ALL.jvm.heap.used", "glue.driver.s3.filesystem.read_bytes glue.executorId.s3.filesystem.read_bytes glue.ALL.s3.filesystem.read_bytes", "glue.driver.s3.filesystem.write_bytes glue.executorId.s3.filesystem.write_bytes glue.ALL.s3.filesystem.write_bytes", "glue.driver.system.cpuSystemLoad glue.executorId.system.cpuSystemLoad glue.ALL.system.cpuSystemLoad"},
"AWS/Inspector": {"TotalAssessmentRunFindings", "TotalAssessmentRuns", "TotalHealthyAgents", "TotalMatchingAgents"},
"AWS/IoT": {"Connect.Success", "GetThingShadow.Accepted", "Ping.Success", "PublishIn.Success", "PublishOut.Success", "Subscribe.Success"},
"AWS/KMS": {"SecondsUntilKeyMaterialExpiration"},
"AWS/Kinesis": {"GetRecords.Bytes", "GetRecords.IteratorAge", "GetRecords.IteratorAgeMilliseconds", "GetRecords.Latency", "GetRecords.Records", "GetRecords.Success", "IncomingBytes", "IncomingRecords", "IteratorAgeMilliseconds", "OutgoingBytes", "OutgoingRecords", "PutRecord.Bytes", "PutRecord.Latency", "PutRecord.Success", "PutRecords.Bytes", "PutRecords.Latency", "PutRecords.Records", "PutRecords.Success", "ReadProvisionedThroughputExceeded", "SubscribeToShard.RateExceeded", "SubscribeToShard.Success", "SubscribeToShardEvent.Bytes", "SubscribeToShardEvent.MillisBehindLatest", "SubscribeToShardEvent.Records", "SubscribeToShardEvent.Success", "WriteProvisionedThroughputExceeded"},
"AWS/KinesisAnalytics": {"Bytes", "InputProcessing.DroppedRecords", "InputProcessing.Duration", "InputProcessing.OkBytes", "InputProcessing.OkRecords", "InputProcessing.ProcessingFailedRecords", "InputProcessing.Success", "KPUs", "LambdaDelivery.DeliveryFailedRecords", "LambdaDelivery.Duration", "LambdaDelivery.OkRecords", "MillisBehindLatest", "Records", "Success"},
"AWS/KinesisVideo": {"GetHLSMasterPlaylist.Latency", "GetHLSMasterPlaylist.Requests", "GetHLSMasterPlaylist.Success", "GetHLSMediaPlaylist.Latency", "GetHLSMediaPlaylist.Requests", "GetHLSMediaPlaylist.Success", "GetHLSStreamingSessionURL.Latency", "GetHLSStreamingSessionURL.Requests", "GetHLSStreamingSessionURL.Success", "GetMP4InitFragment.Latency", "GetMP4InitFragment.Requests", "GetMP4InitFragment.Success", "GetMP4MediaFragment.Latency", "GetMP4MediaFragment.OutgoingBytes", "GetMP4MediaFragment.Requests", "GetMP4MediaFragment.Success", "GetMedia.ConnectionErrors", "GetMedia.MillisBehindNow", "GetMedia.OutgoingBytes", "GetMedia.OutgoingFragments", "GetMedia.OutgoingFrames", "GetMedia.Requests", "GetMedia.Success", "GetMediaForFragmentList.OutgoingBytes", "GetMediaForFragmentList.OutgoingFragments", "GetMediaForFragmentList.OutgoingFrames", "GetMediaForFragmentList.Requests", "GetMediaForFragmentList.Success", "GetTSFragment.Latency", "GetTSFragment.OutgoingBytes", "GetTSFragment.Requests", "GetTSFragment.Success", "ListFragments.Latency", "PutMedia.ActiveConnections", "PutMedia.BufferingAckLatency", "PutMedia.ConnectionErrors", "PutMedia.ErrorAckCount", "PutMedia.FragmentIngestionLatency", "PutMedia.FragmentPersistLatency", "PutMedia.IncomingBytes", "PutMedia.IncomingFragments", "PutMedia.IncomingFrames", "PutMedia.Latency", "PutMedia.PersistedAckLatency", "PutMedia.ReceivedAckLatency", "PutMedia.Requests", "PutMedia.Success"},
"AWS/Lambda": {"ConcurrentExecutions", "DeadLetterErrors", "Duration", "Errors", "Invocations", "IteratorAge", "Throttles", "UnreservedConcurrentExecutions"},
"AWS/Lex": {"BotChannelAuthErrors", "BotChannelConfigurationErrors", "BotChannelInboundThrottledEvents", "BotChannelOutboundThrottledEvents", "BotChannelRequestCount", "BotChannelResponseCardErrors", "BotChannelSystemErrors", "MissedUtteranceCount", "RuntimeInvalidLambdaResponses", "RuntimeLambdaErrors", "RuntimePollyErrors", "RuntimeRequestCount", "RuntimeSucessfulRequestLatency", "RuntimeSystemErrors", "RuntimeThrottledEvents", "RuntimeUserErrors"},
"AWS/Logs": {"DeliveryErrors", "DeliveryThrottling", "ForwardedBytes", "ForwardedLogEvents", "IncomingBytes", "IncomingLogEvents"},
"AWS/ML": {"PredictCount", "PredictFailureCount"},
"AWS/MediaConvert": {"AudioOutputSeconds", "Errors", "HDOutputSeconds", "JobsCompletedCount", "JobsErroredCount", "SDOutputSeconds", "StandbyTime", "TranscodingTime", "UHDOutputSeconds"},
"AWS/MediaPackage": {"ActiveInput", "EgressBytes", "EgressRequestCount", "EgressResponseTime", "IngressBytes", "IngressResponseTime"},
"AWS/MediaTailor": {"AdDecisionServer.Ads", "AdDecisionServer.Duration", "AdDecisionServer.Errors", "AdDecisionServer.FillRate", "AdDecisionServer.Timeouts", "AdNotReady", "Avails.Duration", "Avails.FillRate", "Avails.FilledDuration", "GetManifest.Errors", "Origin.Errors", "Origin.Timeouts"},
"AWS/NATGateway": {"ActiveConnectionCount", "BytesInFromDestination", "BytesInFromSource", "BytesOutToDestination", "BytesOutToSource", "ConnectionAttemptCount", "ConnectionEstablishedCount", "ErrorPortAllocation", "IdleTimeoutCount", "PacketsDropCount", "PacketsInFromDestination", "PacketsInFromSource", "PacketsOutToDestination", "PacketsOutToSource"},
"AWS/Neptune": {"CPUUtilization", "ClusterReplicaLag", "ClusterReplicaLagMaximum", "ClusterReplicaLagMinimum", "EngineUptime", "FreeLocalStorage", "FreeableMemory", "GremlinErrors", "GremlinHttp1xx", "GremlinHttp2xx", "GremlinHttp4xx", "GremlinHttp5xx", "GremlinRequests", "GremlinRequestsPerSec", "GremlinWebSocketAvailableConnections", "GremlinWebSocketClientErrors", "GremlinWebSocketServerErrors", "GremlinWebSocketSuccess", "Http100", "Http101", "Http1xx", "Http200", "Http2xx", "Http400", "Http403", "Http405", "Http413", "Http429", "Http4xx", "Http500", "Http501", "Http5xx", "LoaderErrors", "LoaderRequests", "NetworkReceiveThroughput", "NetworkThroughput", "NetworkTransmitThroughput", "SparqlErrors", "SparqlHttp1xx", "SparqlHttp2xx", "SparqlHttp4xx", "SparqlHttp5xx", "SparqlRequests", "SparqlRequestsPerSec", "StatusErrors", "StatusRequests", "VolumeBytesUsed", "VolumeReadIOPs", "VolumeWriteIOPs"},
"AWS/NetworkELB": {"ActiveFlowCount", "ActiveFlowCount_TLS", "ClientTLSNegotiationErrorCount", "ConsumedLCUs", "HealthyHostCount", "NewFlowCount", "NewFlowCount_TLS", "ProcessedBytes", "ProcessedBytes_TLS", "TCP_Client_Reset_Count", "TCP_ELB_Reset_Count", "TCP_Target_Reset_Count", "TargetTLSNegotiationErrorCount", "UnHealthyHostCount"},
"AWS/OpsWorks": {"cpu_idle", "cpu_nice", "cpu_steal", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_15", "load_5", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"},
"AWS/Polly": {"2XXCount", "4XXCount", "5XXCount", "RequestCharacters", "ResponseLatency"},
"AWS/RDS": {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "BurstBalance", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "CommitLatency", "CommitThroughput", "DDLLatency", "DDLThroughput", "DMLLatency", "DMLThroughput", "DatabaseConnections", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "EngineUptime", "FailedSQLServerAgentJobsCount", "FailedSqlStatements", "FreeLocalStorage", "FreeStorageSpace", "FreeableMemory", "InsertLatency", "InsertThroughput", "LoginFailures", "MaximumUsedTransactionIDs", "NetworkReceiveThroughput", "NetworkThroughput", "NetworkTransmitThroughput", "OldestReplicationSlotLag", "Queries", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ReplicationSlotDiskUsage", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "ServerlessDatabaseCapacity", "SwapUsage", "TotalConnections", "TransactionLogsDiskUsage", "TransactionLogsGeneration", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"},
"AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "QueriesCompletedPerSecond", "QueryDuration", "QueryRuntimeBreakdown", "ReadIOPS", "ReadLatency", "ReadThroughput", "TotalTableCount", "WLMQueriesCompletedPerSecond", "WLMQueryDuration", "WLMQueueLength", "WriteIOPS", "WriteLatency", "WriteThroughput"},
"AWS/Route53": {"ChildHealthCheckHealthyCount", "ConnectionTime", "HealthCheckPercentageHealthy", "HealthCheckStatus", "SSLHandshakeTime", "TimeToFirstByte"},
"AWS/S3": {"4xxErrors", "5xxErrors", "AllRequests", "BucketSizeBytes", "BytesDownloaded", "BytesUploaded", "DeleteRequests", "FirstByteLatency", "GetRequests", "HeadRequests", "ListRequests", "NumberOfObjects", "PostRequests", "PutRequests", "SelectRequests", "SelectReturnedBytes", "SelectScannedBytes", "TotalRequestLatency"},
"AWS/SES": {"Bounce", "Complaint", "Delivery", "Reject", "Reputation.BounceRate", "Reputation.ComplaintRate", "Send"},
"AWS/SNS": {"NumberOfMessagesPublished", "NumberOfNotificationsDelivered", "NumberOfNotificationsFailed", "PublishSize"},
"AWS/SQS": {"ApproximateAgeOfOldestMessage", "ApproximateNumberOfMessagesDelayed", "ApproximateNumberOfMessagesNotVisible", "ApproximateNumberOfMessagesVisible", "NumberOfEmptyReceives", "NumberOfMessagesDeleted", "NumberOfMessagesReceived", "NumberOfMessagesSent", "SentMessageSize"},
"AWS/SWF": {"ActivityTaskScheduleToCloseTime", "ActivityTaskScheduleToStartTime", "ActivityTaskStartToCloseTime", "ActivityTasksCanceled", "ActivityTasksCompleted", "ActivityTasksFailed", "DecisionTaskScheduleToStartTime", "DecisionTaskStartToCloseTime", "DecisionTasksCompleted", "ScheduledActivityTasksTimedOutOnClose", "ScheduledActivityTasksTimedOutOnStart", "StartedActivityTasksTimedOutOnClose", "StartedActivityTasksTimedOutOnHeartbeat", "StartedDecisionTasksTimedOutOnClose", "WorkflowStartToCloseTime", "WorkflowsCanceled", "WorkflowsCompleted", "WorkflowsContinuedAsNew", "WorkflowsFailed", "WorkflowsTerminated", "WorkflowsTimedOut"},
"AWS/SageMaker": {"CPUUtilization", "DatasetObjectsAutoAnnotated", "DatasetObjectsHumanAnnotated", "DatasetObjectsLabelingFailed", "DiskUtilization", "GPUMemoryUtilization", "GPUUtilization", "Invocation4XXErrors", "Invocation5XXErrors", "Invocations", "InvocationsPerInstance", "JobsFailed", "JobsStopped", "JobsSucceeded", "MemoryUtilization", "ModelLatency", "OverheadLatency", "TotalDatasetObjectsLabeled"},
"AWS/States": {"ActivitiesFailed", "ActivitiesHeartbeatTimedOut", "ActivitiesScheduled", "ActivitiesStarted", "ActivitiesSucceeded", "ActivitiesTimedOut", "ActivityRunTime", "ActivityScheduleTime", "ActivityTime", "ConsumedCapacity", "ExecutionThrottled", "ExecutionTime", "ExecutionsAborted", "ExecutionsFailed", "ExecutionsStarted", "ExecutionsSucceeded", "ExecutionsTimedOut", "LambdaFunctionRunTime", "LambdaFunctionScheduleTime", "LambdaFunctionTime", "LambdaFunctionsFailed", "LambdaFunctionsHeartbeatTimedOut", "LambdaFunctionsScheduled", "LambdaFunctionsStarted", "LambdaFunctionsSucceeded", "LambdaFunctionsTimedOut", "ProvisionedBucketSize", "ProvisionedRefillRate", "ThrottledEvents"},
"AWS/StorageGateway": {"CacheFree", "CacheHitPercent", "CachePercentDirty", "CachePercentUsed", "CacheUsed", "CloudBytesDownloaded", "CloudBytesUploaded", "CloudDownloadLatency", "QueuedWrites", "ReadBytes", "ReadTime", "TimeSinceLastRecoveryPoint", "TotalCacheSize", "UploadBufferFree", "UploadBufferPercentUsed", "UploadBufferUsed", "WorkingStorageFree", "WorkingStoragePercentUsed", "WorkingStorageUsed", "WriteBytes", "WriteTime"},
"AWS/TransitGateway": {"BytesIn", "BytesOut", "PacketDropCountBlackhole", "PacketDropCountNoRoute", "PacketsIn", "PacketsOut"},
"AWS/Translate": {"CharacterCount", "ResponseTime", "ServerErrorCount", "SuccessfulRequestCount", "ThrottledCount", "UserErrorCount"},
"AWS/VPN": {"TunnelDataIn", "TunnelDataOut", "TunnelState"},
"AWS/WorkSpaces": {"Available", "ConnectionAttempt", "ConnectionFailure", "ConnectionSuccess", "InSessionLatency", "Maintenance", "SessionDisconnect", "SessionLaunchTime", "Stopped", "Unhealthy", "UserConnected"},
"Rekognition": {"DetectedFaceCount", "DetectedLabelCount", "ResponseTime", "ServerErrorCount", "SuccessfulRequestCount", "ThrottledCount", "UserErrorCount"},
"WAF": {"AllowedRequests", "BlockedRequests", "CountedRequests", "DDoSAttackBitsPerSecond", "DDoSAttackPacketsPerSecond", "DDoSAttackRequestsPerSecond", "DDoSDetected", "PassedRequests"},
}
dimensionsMap = map[string][]string{
"AWS/AmazonMQ": {"Broker", "Topic", "Queue"},
"AWS/ApiGateway": {"ApiName", "Method", "Resource", "Stage"},
"AWS/ApplicationELB": {"LoadBalancer", "TargetGroup", "AvailabilityZone"},
"AWS/AutoScaling": {"AutoScalingGroupName"},
"AWS/Billing": {"ServiceName", "LinkedAccount", "Currency"},
"AWS/CloudFront": {"DistributionId", "Region"},
"AWS/CloudSearch": {},
"AWS/CloudHSM": {"Region", "ClusterId", "HsmId"},
"AWS/CodeBuild": {"ProjectName"},
"AWS/Connect": {"InstanceId", "MetricGroup", "Participant", "QueueName", "Stream Type", "Type of Connection"},
"AWS/DMS": {"ReplicationInstanceIdentifier", "ReplicationTaskIdentifier"},
"AWS/DX": {"ConnectionId"},
"AWS/DynamoDB": {"TableName", "GlobalSecondaryIndexName", "Operation", "StreamLabel"},
"AWS/EBS": {"VolumeId"},
"AWS/EC2": {"AutoScalingGroupName", "ImageId", "InstanceId", "InstanceType"},
"AWS/EC2/API": {},
"AWS/EC2Spot": {"AvailabilityZone", "FleetRequestId", "InstanceType"},
"AWS/ECS": {"ClusterName", "ServiceName"},
"AWS/EFS": {"FileSystemId"},
"AWS/ELB": {"LoadBalancerName", "AvailabilityZone"},
"AWS/ElastiCache": {"CacheClusterId", "CacheNodeId"},
"AWS/ElasticBeanstalk": {"EnvironmentName", "InstanceId"},
"AWS/ElasticMapReduce": {"ClusterId", "JobFlowId", "JobId"},
"AWS/ES": {"ClientId", "DomainName"},
"AWS/Events": {"RuleName"},
"AWS/Firehose": {"DeliveryStreamName"},
"AWS/IoT": {"Protocol"},
"AWS/Kinesis": {"StreamName", "ShardId"},
"AWS/KinesisAnalytics": {"Flow", "Id", "Application"},
"AWS/Lambda": {"FunctionName", "Resource", "Version", "Alias"},
"AWS/AppSync": {"GraphQLAPIId"},
"AWS/Logs": {"LogGroupName", "DestinationType", "FilterName"},
"AWS/ML": {"MLModelId", "RequestMode"},
"AWS/NATGateway": {"NatGatewayId"},
"AWS/Neptune": {"DBClusterIdentifier", "Role", "DatabaseClass", "EngineName"},
"AWS/NetworkELB": {"LoadBalancer", "TargetGroup", "AvailabilityZone"},
"AWS/OpsWorks": {"StackId", "LayerId", "InstanceId"},
"AWS/Redshift": {"NodeID", "ClusterIdentifier", "latency", "service class", "wmlid"},
"AWS/RDS": {"DBInstanceIdentifier", "DBClusterIdentifier", "DbClusterIdentifier", "DatabaseClass", "EngineName", "Role"},
"AWS/Route53": {"HealthCheckId", "Region"},
"AWS/S3": {"BucketName", "StorageType", "FilterId"},
"AWS/SES": {},
"AWS/SNS": {"Application", "Platform", "TopicName"},
"AWS/SQS": {"QueueName"},
"AWS/States": {"StateMachineArn", "ActivityArn", "LambdaFunctionArn"},
"AWS/StorageGateway": {"GatewayId", "GatewayName", "VolumeId"},
"AWS/SWF": {"Domain", "WorkflowTypeName", "WorkflowTypeVersion", "ActivityTypeName", "ActivityTypeVersion"},
"AWS/VPN": {"VpnId", "TunnelIpAddress"},
"Rekognition": {},
"WAF": {"Rule", "WebACL"},
"AWS/WorkSpaces": {"DirectoryId", "WorkspaceId"},
"KMS": {"KeyId"},
"AWS/AmazonMQ": {"Broker", "Queue", "Topic"},
"AWS/ApiGateway": {"ApiName", "Method", "Resource", "Stage"},
"AWS/AppStream": {"Fleet"},
"AWS/AppSync": {"GraphQLAPIId"},
"AWS/ApplicationELB": {"AvailabilityZone", "LoadBalancer", "TargetGroup"},
"AWS/AutoScaling": {"AutoScalingGroupName"},
"AWS/Billing": {"Currency", "LinkedAccount", "ServiceName"},
"AWS/CloudFront": {"DistributionId", "Region"},
"AWS/CloudHSM": {"ClusterId", "HsmId", "Region"},
"AWS/CloudSearch": {"ClientId", "DomainName"},
"AWS/CodeBuild": {"ProjectName"},
"AWS/Connect": {"InstanceId", "MetricGroup", "Participant", "QueueName", "Stream Type", "Type of Connection"},
"AWS/DDoSProtection": {"Region", "Rule", "RuleGroup", "WebACL"},
"AWS/DMS": {"ReplicationInstanceIdentifier", "ReplicationTaskIdentifier"},
"AWS/DX": {"ConnectionId"},
"AWS/DynamoDB": {"GlobalSecondaryIndexName", "Operation", "ReceivingRegion", "StreamLabel", "TableName"},
"AWS/EBS": {"VolumeId"},
"AWS/EC2": {"AutoScalingGroupName", "ImageId", "InstanceId", "InstanceType"},
"AWS/EC2/API": {},
"AWS/EC2Spot": {"AvailabilityZone", "FleetRequestId", "InstanceType"},
"AWS/ECS": {"ClusterName", "ServiceName"},
"AWS/EFS": {"FileSystemId"},
"AWS/ELB": {"AvailabilityZone", "LoadBalancerName"},
"AWS/ES": {"ClientId", "DomainName"},
"AWS/ElastiCache": {"CacheClusterId", "CacheNodeId"},
"AWS/ElasticBeanstalk": {"EnvironmentName", "InstanceId"},
"AWS/ElasticMapReduce": {"ClusterId", "JobFlowId", "JobId"},
"AWS/ElasticTranscoder": {"Operation", "PipelineId"},
"AWS/Events": {"RuleName"},
"AWS/FSx": {},
"AWS/Firehose": {"DeliveryStreamName"},
"AWS/Glue": {"JobName", "JobRunId", "Type"},
"AWS/Inspector": {},
"AWS/IoT": {"Protocol"},
"AWS/KMS": {"KeyId"},
"AWS/Kinesis": {"ShardId", "StreamName"},
"AWS/KinesisAnalytics": {"Application", "Flow", "Id"},
"AWS/KinesisVideo": {},
"AWS/Lambda": {"Alias", "ExecutedVersion", "FunctionName", "Resource"},
"AWS/Lex": {"BotAlias", "BotChannelName", "BotName", "BotVersion", "InputMode", "Operation", "Source"},
"AWS/Logs": {"DestinationType", "FilterName", "LogGroupName"},
"AWS/ML": {"MLModelId", "RequestMode"},
"AWS/MediaConvert": {"Job", "Operation", "Queue"},
"AWS/MediaPackage": {"Channel", "No Dimension", "OriginEndpoint", "StatusCodeRange"},
"AWS/MediaTailor": {"Configuration Name"},
"AWS/NATGateway": {"NatGatewayId"},
"AWS/Neptune": {"DBClusterIdentifier", "DatabaseClass", "EngineName", "Role"},
"AWS/NetworkELB": {"AvailabilityZone", "LoadBalancer", "TargetGroup"},
"AWS/OpsWorks": {"InstanceId", "LayerId", "StackId"},
"AWS/Polly": {"Operation"},
"AWS/RDS": {"DBClusterIdentifier", "DBInstanceIdentifier", "DatabaseClass", "DbClusterIdentifier", "EngineName", "Role", "SourceRegion"},
"AWS/Redshift": {"ClusterIdentifier", "NodeID", "Service class", "Stage", "latency", "wmlid"},
"AWS/Route53": {"HealthCheckId", "Region"},
"AWS/S3": {"BucketName", "FilterId", "StorageType"},
"AWS/SES": {},
"AWS/SNS": {"Application", "Platform", "TopicName"},
"AWS/SQS": {"QueueName"},
"AWS/SWF": {"ActivityTypeName", "ActivityTypeVersion", "Domain", "WorkflowTypeName", "WorkflowTypeVersion"},
"AWS/SageMaker": {"EndpointName", "Host", "LabelingJobName", "VariantName"},
"AWS/States": {"APIName", "ActivityArn", "LambdaFunctionArn", "StateMachineArn", "StateTransition"},
"AWS/StorageGateway": {"GatewayId", "GatewayName", "VolumeId"},
"AWS/TransitGateway": {"TransitGateway"},
"AWS/Translate": {"LanguagePair", "Operation"},
"AWS/VPN": {"TunnelIpAddress", "VpnId"},
"AWS/WorkSpaces": {"DirectoryId", "WorkspaceId"},
"Rekognition": {},
"WAF": {"Region", "Rule", "RuleGroup", "WebACL"},
}
customMetricsMetricsMap = make(map[string]map[string]map[string]*CustomMetricsCache)

@ -9,7 +9,7 @@ import { TagFilter } from './components/TagFilter/TagFilter';
import { SideMenu } from './components/sidemenu/SideMenu';
import { MetricSelect } from './components/Select/MetricSelect';
import AppNotificationList from './components/AppNotifications/AppNotificationList';
import { ColorPicker, SeriesColorPickerPopoverWithTheme } from '@grafana/ui';
import { ColorPicker, SeriesColorPickerPopoverWithTheme, SecretFormField } from '@grafana/ui';
import { FunctionEditor } from 'app/plugins/datasource/graphite/FunctionEditor';
export function registerAngularDirectives() {
@ -59,4 +59,11 @@ export function registerAngularDirectives() {
['datasource', { watchDepth: 'reference' }],
['templateSrv', { watchDepth: 'reference' }],
]);
react2AngularDirective('secretFormField', SecretFormField, [
'value',
'isConfigured',
'inputWidth',
['onReset', { watchDepth: 'reference', wrapApply: true }],
['onChange', { watchDepth: 'reference', wrapApply: true }],
]);
}

@ -1,4 +1,4 @@
import React, { FC } from 'react';
import React, { FC, CSSProperties } from 'react';
import Transition, { ExitHandler } from 'react-transition-group/Transition';
interface Props {
@ -10,12 +10,12 @@ interface Props {
}
export const FadeIn: FC<Props> = props => {
const defaultStyle = {
const defaultStyle: CSSProperties = {
transition: `opacity ${props.duration}ms linear`,
opacity: 0,
};
const transitionStyles = {
const transitionStyles: { [str: string]: CSSProperties } = {
exited: { opacity: 0, display: 'none' },
entering: { opacity: 0 },
entered: { opacity: 1 },

@ -1,4 +1,4 @@
import React from 'react';
import React, { CSSProperties, FC } from 'react';
import Transition from 'react-transition-group/Transition';
interface Style {
@ -16,11 +16,18 @@ export const defaultStyle: Style = {
overflow: 'hidden',
};
export default ({ children, in: inProp, maxHeight = defaultMaxHeight, style = defaultStyle }) => {
export interface Props {
children: React.ReactNode;
in: boolean;
maxHeight?: number;
style?: CSSProperties;
}
export const SlideDown: FC<Props> = ({ children, in: inProp, maxHeight = defaultMaxHeight, style = defaultStyle }) => {
// There are 4 main states a Transition can be in:
// ENTERING, ENTERED, EXITING, EXITED
// https://reactcommunity.org/react-transition-group/
const transitionStyles = {
// https://reactcommunity.or[g/react-transition-group/
const transitionStyles: { [str: string]: CSSProperties } = {
exited: { maxHeight: 0 },
entering: { maxHeight: maxHeight },
entered: { maxHeight: 'unset', overflow: 'visible' },
@ -34,6 +41,7 @@ export default ({ children, in: inProp, maxHeight = defaultMaxHeight, style = de
style={{
...style,
...transitionStyles[state],
inProp,
}}
>
{children}

@ -11,10 +11,10 @@ interface Props {
}
export class CopyToClipboard extends PureComponent<Props> {
clipboardjs: any;
clipboardjs: ClipboardJS;
myRef: any;
constructor(props) {
constructor(props: Props) {
super(props);
this.myRef = React.createRef();
}

@ -1,5 +1,5 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { shallow } from 'enzyme';
import EmptyListCTA from './EmptyListCTA';
const model = {
@ -16,7 +16,7 @@ const model = {
describe('EmptyListCTA', () => {
it('renders correctly', () => {
const tree = renderer.create(<EmptyListCTA model={model} />).toJSON();
const tree = shallow(<EmptyListCTA model={model} />);
expect(tree).toMatchSnapshot();
});
});

@ -1,5 +1,4 @@
import React, { PureComponent, createRef } from 'react';
// import JSONFormatterJS, { JSONFormatterConfiguration } from 'json-formatter-js';
import { JsonExplorer } from 'app/core/core'; // We have made some monkey-patching of json-formatter-js so we can't switch right now
interface Props {

@ -1,7 +1,8 @@
import _ from 'lodash';
import coreModule from '../../core_module';
import { ISCEService, IQService } from 'angular';
function typeaheadMatcher(this: any, item) {
function typeaheadMatcher(this: any, item: string) {
let str = this.query;
if (str === '') {
return true;
@ -16,8 +17,8 @@ function typeaheadMatcher(this: any, item) {
}
export class FormDropdownCtrl {
inputElement: any;
linkElement: any;
inputElement: JQLite;
linkElement: JQLite;
model: any;
display: any;
text: any;
@ -37,7 +38,13 @@ export class FormDropdownCtrl {
debounce: number;
/** @ngInject */
constructor(private $scope, $element, private $sce, private templateSrv, private $q) {
constructor(
private $scope: any,
$element: JQLite,
private $sce: ISCEService,
private templateSrv: any,
private $q: IQService
) {
this.inputElement = $element.find('input').first();
this.linkElement = $element.find('a').first();
this.linkMode = true;
@ -99,7 +106,7 @@ export class FormDropdownCtrl {
}
}
getOptionsInternal(query) {
getOptionsInternal(query: string) {
const result = this.getOptions({ $query: query });
if (this.isPromiseLike(result)) {
return result;
@ -107,7 +114,7 @@ export class FormDropdownCtrl {
return this.$q.when(result);
}
isPromiseLike(obj) {
isPromiseLike(obj: any) {
return obj && typeof obj.then === 'function';
}
@ -117,7 +124,7 @@ export class FormDropdownCtrl {
} else {
// if we have text use it
if (this.lookupText) {
this.getOptionsInternal('').then(options => {
this.getOptionsInternal('').then((options: any) => {
const item = _.find(options, { value: this.model });
this.updateDisplay(item ? item.text : this.model);
});
@ -127,12 +134,12 @@ export class FormDropdownCtrl {
}
}
typeaheadSource(query, callback) {
this.getOptionsInternal(query).then(options => {
typeaheadSource(query: string, callback: (res: any) => void) {
this.getOptionsInternal(query).then((options: any) => {
this.optionCache = options;
// extract texts
const optionTexts = _.map(options, op => {
const optionTexts = _.map(options, (op: any) => {
return _.escape(op.text);
});
@ -147,7 +154,7 @@ export class FormDropdownCtrl {
});
}
typeaheadUpdater(text) {
typeaheadUpdater(text: string) {
if (text === this.text) {
clearTimeout(this.cancelBlur);
this.inputElement.focus();
@ -159,7 +166,7 @@ export class FormDropdownCtrl {
return text;
}
switchToLink(fromClick) {
switchToLink(fromClick: boolean) {
if (this.linkMode && !fromClick) {
return;
}
@ -178,7 +185,7 @@ export class FormDropdownCtrl {
this.cancelBlur = setTimeout(this.switchToLink.bind(this), 200);
}
updateValue(text) {
updateValue(text: string) {
text = _.unescape(text);
if (text === '' || this.text === text) {
@ -214,7 +221,7 @@ export class FormDropdownCtrl {
});
}
updateDisplay(text) {
updateDisplay(text: string) {
this.text = text;
this.display = this.$sce.trustAsHtml(this.templateSrv.highlightVariablesAsHtml(text));
}

@ -232,7 +232,7 @@ export class JsonExplorer {
// some pretty handling of number arrays
if (this.isNumberArray()) {
this.json.forEach((val, index) => {
this.json.forEach((val: any, index: number) => {
if (index > 0) {
arrayWrapperSpan.appendChild(createElement('span', 'array-comma', ','));
}

@ -16,7 +16,7 @@ export class LayoutSelectorCtrl {
mode: string;
/** @ngInject */
constructor(private $rootScope) {
constructor(private $rootScope: any) {
this.mode = store.get('grafana.list.layout.mode') || 'grid';
}
@ -46,18 +46,18 @@ export function layoutSelector() {
}
/** @ngInject */
export function layoutMode($rootScope) {
export function layoutMode($rootScope: any) {
return {
restrict: 'A',
scope: {},
link: (scope, elem) => {
link: (scope: any, elem: any) => {
const layout = store.get('grafana.list.layout.mode') || 'grid';
let className = 'card-list-layout-' + layout;
elem.addClass(className);
$rootScope.onAppEvent(
'layout-mode-changed',
(evt, newLayout) => {
(evt: any, newLayout: any) => {
elem.removeClass(className);
className = 'card-list-layout-' + newLayout;
elem.addClass(className);

@ -9,6 +9,7 @@
// - reactComponent (generic directive for delegating off to React Components)
// - reactDirective (factory for creating specific directives that correspond to reactComponent directives)
import { kebabCase } from 'lodash';
import React from 'react';
import ReactDOM from 'react-dom';
import angular from 'angular';
@ -155,11 +156,17 @@ function getPropExpression(prop) {
return Array.isArray(prop) ? prop[0] : prop;
}
// find the normalized attribute knowing that React props accept any type of capitalization
function findAttribute(attrs, propName) {
const index = Object.keys(attrs).filter(attr => {
return attr.toLowerCase() === propName.toLowerCase();
})[0];
/**
* Finds the normalized attribute knowing that React props accept any type of capitalization and it also handles
* kabab case attributes which can be used in case the attribute would also be a standard html attribute and would be
* evaluated by the browser as such.
* @param attrs All attributes of the component.
* @param propName Name of the prop that react component expects.
*/
function findAttribute(attrs: string, propName: string): string {
const index = Object.keys(attrs).find(attr => {
return attr.toLowerCase() === propName.toLowerCase() || attr.toLowerCase() === kebabCase(propName);
});
return attrs[index];
}
@ -274,7 +281,9 @@ const reactDirective = $injector => {
// watch each property name and trigger an update whenever something changes,
// to update scope.props with new values
const propExpressions = props.map(prop => {
return Array.isArray(prop) ? [attrs[getPropName(prop)], getPropConfig(prop)] : attrs[prop];
return Array.isArray(prop)
? [findAttribute(attrs, prop[0]), getPropConfig(prop)]
: findAttribute(attrs, prop);
});
// If we don't have any props, then our watch statement won't fire.

@ -1,17 +1,18 @@
import _ from 'lodash';
import { Column, TableData } from '@grafana/ui';
interface Column {
text: string;
/**
* Extends the standard Column class with variables that get
* mutated in the angular table panel.
*/
interface MutableColumn extends Column {
title?: string;
type?: string;
sort?: boolean;
desc?: boolean;
filterable?: boolean;
unit?: string;
}
export default class TableModel {
columns: Column[];
export default class TableModel implements TableData {
columns: MutableColumn[];
rows: any[];
type: string;
columnMap: any;

@ -7,7 +7,7 @@ import { getNavModel } from 'app/core/selectors/navModel';
import { getApiKeys, getApiKeysCount } from './state/selectors';
import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions';
import Page from 'app/core/components/Page/Page';
import SlideDown from 'app/core/components/Animations/SlideDown';
import { SlideDown } from 'app/core/components/Animations/SlideDown';
import ApiKeysAddedModal from './ApiKeysAddedModal';
import config from 'app/core/config';
import appEvents from 'app/core/app_events';

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react';
import { Tooltip } from '@grafana/ui';
import SlideDown from 'app/core/components/Animations/SlideDown';
import { SlideDown } from 'app/core/components/Animations/SlideDown';
import { StoreState, FolderInfo } from 'app/types';
import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl';
import {

@ -11,16 +11,15 @@ import {
DataQueryResponse,
DataQueryError,
LoadingState,
PanelData,
TableData,
TimeRange,
TimeSeries,
ScopedVars,
toTableData,
} from '@grafana/ui';
interface RenderProps {
loading: LoadingState;
panelData: PanelData;
data: TableData[];
}
export interface Props {
@ -44,7 +43,7 @@ export interface State {
isFirstLoad: boolean;
loading: LoadingState;
response: DataQueryResponse;
panelData: PanelData;
data?: TableData[];
}
export class DataPanel extends Component<Props, State> {
@ -64,7 +63,6 @@ export class DataPanel extends Component<Props, State> {
response: {
data: [],
},
panelData: {},
isFirstLoad: true,
};
}
@ -149,7 +147,7 @@ export class DataPanel extends Component<Props, State> {
this.setState({
loading: LoadingState.Done,
response: resp,
panelData: this.getPanelData(resp),
data: toTableData(resp.data),
isFirstLoad: false,
});
} catch (err) {
@ -172,23 +170,9 @@ export class DataPanel extends Component<Props, State> {
}
};
getPanelData(response: DataQueryResponse) {
if (response.data.length > 0 && (response.data[0] as TableData).type === 'table') {
return {
tableData: response.data[0] as TableData,
timeSeries: null,
};
}
return {
timeSeries: response.data as TimeSeries[],
tableData: null,
};
}
render() {
const { queries } = this.props;
const { loading, isFirstLoad, panelData } = this.state;
const { loading, isFirstLoad, data } = this.state;
// do not render component until we have first data
if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) {
@ -206,7 +190,7 @@ export class DataPanel extends Component<Props, State> {
return (
<>
{loading === LoadingState.Loading && this.renderLoadingState()}
{this.props.children({ loading, panelData })}
{this.props.children({ loading, data })}
</>
);
}

@ -11,7 +11,7 @@ import { DataPanel } from './DataPanel';
import ErrorBoundary from '../../../core/components/ErrorBoundary/ErrorBoundary';
// Utils
import { applyPanelTimeOverrides, snapshotDataToPanelData } from 'app/features/dashboard/utils/panel';
import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
import { PANEL_HEADER_HEIGHT } from 'app/core/constants';
import { profiler } from 'app/core/profiler';
import config from 'app/core/config';
@ -19,7 +19,7 @@ import config from 'app/core/config';
// Types
import { DashboardModel, PanelModel } from '../state';
import { PanelPlugin } from 'app/types';
import { DataQueryResponse, TimeRange, LoadingState, PanelData, DataQueryError } from '@grafana/ui';
import { DataQueryResponse, TimeRange, LoadingState, TableData, DataQueryError, toTableData } from '@grafana/ui';
import { ScopedVars } from '@grafana/ui';
import templateSrv from 'app/features/templating/template_srv';
@ -139,10 +139,10 @@ export class PanelChrome extends PureComponent<Props, State> {
}
get getDataForPanel() {
return this.hasPanelSnapshot ? snapshotDataToPanelData(this.props.panel) : null;
return this.hasPanelSnapshot ? toTableData(this.props.panel.snapshotData) : null;
}
renderPanelPlugin(loading: LoadingState, panelData: PanelData, width: number, height: number): JSX.Element {
renderPanelPlugin(loading: LoadingState, data: TableData[], width: number, height: number): JSX.Element {
const { panel, plugin } = this.props;
const { timeRange, renderCounter } = this.state;
const PanelComponent = plugin.exports.reactPanel.panel;
@ -157,7 +157,7 @@ export class PanelChrome extends PureComponent<Props, State> {
<div className="panel-content">
<PanelComponent
loading={loading}
panelData={panelData}
data={data}
timeRange={timeRange}
options={panel.getOptions(plugin.exports.reactPanel.defaults)}
width={width - 2 * config.theme.panelPadding.horizontal}
@ -188,8 +188,8 @@ export class PanelChrome extends PureComponent<Props, State> {
onDataResponse={this.onDataResponse}
onError={this.onDataError}
>
{({ loading, panelData }) => {
return this.renderPanelPlugin(loading, panelData, width, height);
{({ loading, data }) => {
return this.renderPanelPlugin(loading, data, width, height);
}}
</DataPanel>
) : (

@ -111,12 +111,12 @@ export class PanelModel {
cachedPluginOptions?: any;
legend?: { show: boolean };
constructor(model) {
constructor(model: any) {
this.events = new Emitter();
// copy properties from persisted model
for (const property in model) {
this[property] = model[property];
(this as any)[property] = model[property];
}
// defaults
@ -150,7 +150,7 @@ export class PanelModel {
}
}
getOptions(panelDefaults) {
getOptions(panelDefaults: any) {
return _.defaultsDeep(this.options || {}, panelDefaults);
}
@ -227,7 +227,7 @@ export class PanelModel {
}
return {
...acc,
[property]: this[property],
[property]: (this as any)[property],
};
}, {});
}
@ -236,7 +236,7 @@ export class PanelModel {
const prevOptions = this.cachedPluginOptions[pluginId] || {};
Object.keys(prevOptions).map(property => {
this[property] = prevOptions[property];
(this as any)[property] = prevOptions[property];
});
}
@ -252,7 +252,7 @@ export class PanelModel {
continue;
}
delete this[key];
delete (this as any)[key];
}
this.cachedPluginOptions[oldPluginId] = oldOptions;

@ -4,8 +4,7 @@ import store from 'app/core/store';
// Models
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { PanelData, TimeRange, TimeSeries } from '@grafana/ui';
import { TableData } from '@grafana/ui/src';
import { TimeRange } from '@grafana/ui';
// Utils
import { isString as _isString } from 'lodash';
@ -170,19 +169,3 @@ export function getResolution(panel: PanelModel): number {
return panel.maxDataPoints ? panel.maxDataPoints : Math.ceil(width * (panel.gridPos.w / 24));
}
const isTimeSeries = (data: any): data is TimeSeries => data && data.hasOwnProperty('datapoints');
const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns');
export const snapshotDataToPanelData = (panel: PanelModel): PanelData => {
const snapshotData = panel.snapshotData;
if (isTimeSeries(snapshotData[0])) {
return {
timeSeries: snapshotData,
} as PanelData;
} else if (isTableData(snapshotData[0])) {
return {
tableData: snapshotData[0],
} as PanelData;
}
throw new Error('snapshotData is invalid:' + snapshotData.toString());
};

@ -3,7 +3,7 @@ import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import Page from 'app/core/components/Page/Page';
import { Tooltip } from '@grafana/ui';
import SlideDown from 'app/core/components/Animations/SlideDown';
import { SlideDown } from 'app/core/components/Animations/SlideDown';
import { getNavModel } from 'app/core/selectors/navModel';
import { NavModel, StoreState, FolderState } from 'app/types';
import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl';

@ -41,7 +41,9 @@ export class PlaylistSrv {
const dash = this.dashboards[this.index];
const queryParams = this.$location.search();
const filteredParams = _.pickBy(queryParams, value => value !== null);
const filteredParams = _.pickBy(queryParams, key => {
return key === 'kiosk' || key === 'autofitpanels' || key === 'orgId';
});
const nextDashboardUrl = locationUtil.stripBaseFromUrl(dash.url);
// this is done inside timeout to make sure digest happens after

@ -28,6 +28,7 @@ import * as singlestatPanel from 'app/plugins/panel/singlestat/module';
import * as singlestatPanel2 from 'app/plugins/panel/singlestat2/module';
import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module';
import * as gaugePanel from 'app/plugins/panel/gauge/module';
import * as pieChartPanel from 'app/plugins/panel/piechart/module';
import * as barGaugePanel from 'app/plugins/panel/bargauge/module';
const builtInPlugins = {
@ -61,6 +62,7 @@ const builtInPlugins = {
'app/plugins/panel/singlestat2/module': singlestatPanel2,
'app/plugins/panel/gettingstarted/module': gettingStartedPanel,
'app/plugins/panel/gauge/module': gaugePanel,
'app/plugins/panel/piechart/module': pieChartPanel,
'app/plugins/panel/bargauge/module': barGaugePanel,
};

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import SlideDown from 'app/core/components/Animations/SlideDown';
import { SlideDown } from 'app/core/components/Animations/SlideDown';
import { Tooltip } from '@grafana/ui';
import { TeamGroup } from '../../types';
import { addTeamGroup, loadTeamGroups, removeTeamGroup } from './state/actions';

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import SlideDown from 'app/core/components/Animations/SlideDown';
import { SlideDown } from 'app/core/components/Animations/SlideDown';
import { UserPicker } from 'app/core/components/Select/UserPicker';
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
import { TeamMember, User } from 'app/types';

@ -183,8 +183,9 @@ export function graphiteFuncEditor($compile, templateSrv, popoverSrv) {
}
let paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]);
const hasValue = paramValue !== null && paramValue !== undefined;
const last = index >= func.params.length - 1 && param.optional && !paramValue;
const last = index >= func.params.length - 1 && param.optional && !hasValue;
if (last && param.multiple) {
paramValue = '+';
}
@ -197,7 +198,7 @@ export function graphiteFuncEditor($compile, templateSrv, popoverSrv) {
'<a ng-click="" class="graphite-func-param-link' +
(last ? ' query-part__last' : '') +
'">' +
(paramValue || '&nbsp;') +
(hasValue ? paramValue : '&nbsp;') +
'</a>'
);
const $input = $(paramTemplate);

@ -1,5 +1,6 @@
import _ from 'lodash';
import TableModel from 'app/core/table_model';
import { ColumnType } from '@grafana/ui';
export default class InfluxSeries {
series: any;
@ -156,7 +157,7 @@ export default class InfluxSeries {
// Check that the first column is indeed 'time'
if (series.columns[0] === 'time') {
// Push this now before the tags and with the right type
table.columns.push({ text: 'Time', type: 'time' });
table.columns.push({ text: 'Time', type: ColumnType.time });
j++;
}
_.each(_.keys(series.tags), key => {

@ -1,3 +1,5 @@
import { SyntheticEvent } from 'react';
export class MssqlConfigCtrl {
static templateUrl = 'partials/config.html';
@ -7,4 +9,16 @@ export class MssqlConfigCtrl {
constructor($scope) {
this.current.jsonData.encrypt = this.current.jsonData.encrypt || 'false';
}
onPasswordReset = (event: SyntheticEvent<HTMLInputElement>) => {
event.preventDefault();
this.current.secureJsonFields.password = false;
this.current.secureJsonData = this.current.secureJsonData || {};
this.current.secureJsonData.password = '';
};
onPasswordChange = (event: SyntheticEvent<HTMLInputElement>) => {
this.current.secureJsonData = this.current.secureJsonData || {};
this.current.secureJsonData.password = event.currentTarget.value;
};
}

@ -17,15 +17,15 @@
<span class="gf-form-label width-7">User</span>
<input type="text" class="gf-form-input" ng-model='ctrl.current.user' placeholder="user"></input>
</div>
<div class="gf-form max-width-15" ng-if="!ctrl.current.secureJsonFields.password">
<span class="gf-form-label width-7">Password</span>
<input type="password" class="gf-form-input" ng-model='ctrl.current.secureJsonData.password' placeholder="password"></input>
</div>
<div class="gf-form max-width-19" ng-if="ctrl.current.secureJsonFields.password">
<span class="gf-form-label width-7">Password</span>
<input type="text" class="gf-form-input" disabled="disabled" value="configured">
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.current.secureJsonFields.password = false">reset</a>
</div>
<div class="gf-form">
<secret-form-field
isConfigured="ctrl.current.secureJsonFields.password"
value="ctrl.current.secureJsonData.password"
on-reset="ctrl.onPasswordReset"
on-change="ctrl.onPasswordChange"
inputWidth="9"
/>
</div>
</div>
<div class="gf-form">

@ -1,4 +1,5 @@
import _ from 'lodash';
import { SyntheticEvent } from 'react';
export class PostgresConfigCtrl {
static templateUrl = 'partials/config.html';
@ -52,6 +53,18 @@ export class PostgresConfigCtrl {
this.showTimescaleDBHelp = !this.showTimescaleDBHelp;
}
onPasswordReset = (event: SyntheticEvent<HTMLInputElement>) => {
event.preventDefault();
this.current.secureJsonFields.password = false;
this.current.secureJsonData = this.current.secureJsonData || {};
this.current.secureJsonData.password = '';
};
onPasswordChange = (event: SyntheticEvent<HTMLInputElement>) => {
this.current.secureJsonData = this.current.secureJsonData || {};
this.current.secureJsonData.password = event.currentTarget.value;
};
// the value portion is derived from postgres server_version_num/100
postgresVersions = [
{ name: '9.3', value: 903 },

@ -17,16 +17,17 @@
<span class="gf-form-label width-7">User</span>
<input type="text" class="gf-form-input" ng-model='ctrl.current.user' placeholder="user"></input>
</div>
<div class="gf-form max-width-15" ng-if="!ctrl.current.secureJsonFields.password">
<span class="gf-form-label width-7">Password</span>
<input type="password" class="gf-form-input" ng-model='ctrl.current.secureJsonData.password' placeholder="password"></input>
</div>
<div class="gf-form max-width-19" ng-if="ctrl.current.secureJsonFields.password">
<span class="gf-form-label width-7">Password</span>
<input type="text" class="gf-form-input" disabled="disabled" value="configured">
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.current.secureJsonFields.password = false">reset</a>
<div class="gf-form">
<secret-form-field
isConfigured="ctrl.current.secureJsonFields.password"
value="ctrl.current.secureJsonData.password"
on-reset="ctrl.onPasswordReset"
on-change="ctrl.onPasswordChange"
inputWidth="9"
/>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label width-7">SSL Mode</label>
<div class="gf-form-select-wrapper max-width-15 gf-form-select-wrapper--has-help-icon">

@ -224,7 +224,8 @@ export class PrometheusDatasource implements DataSourceApi<PromQuery> {
query.expr = this.templateSrv.replace(expr, scopedVars, this.interpolateQueryExpr);
query.requestId = options.panelId + target.refId;
// Align query interval with step
// Align query interval with step to allow query caching and to ensure
// that about-same-time query results look the same.
const adjusted = alignRange(start, end, query.step);
query.start = adjusted.start;
query.end = adjusted.end;
@ -497,8 +498,15 @@ export class PrometheusDatasource implements DataSourceApi<PromQuery> {
}
}
export function alignRange(start, end, step) {
const alignedEnd = Math.ceil(end / step) * step;
/**
* Align query range to step.
* Rounds start and end down to a multiple of step.
* @param start Timestamp marking the beginning of the range.
* @param end Timestamp marking the end of the range.
* @param step Interval to align start and end with.
*/
export function alignRange(start: number, end: number, step: number): { end: number; start: number } {
const alignedEnd = Math.floor(end / step) * step;
const alignedStart = Math.floor(start / step) * step;
return {
end: alignedEnd,

@ -1,6 +1,6 @@
import _ from 'lodash';
import TableModel from 'app/core/table_model';
import { TimeSeries } from '@grafana/ui';
import { TimeSeries, ColumnType } from '@grafana/ui';
export class ResultTransformer {
constructor(private templateSrv) {}
@ -98,7 +98,7 @@ export class ResultTransformer {
// Sort metric labels, create columns for them and record their index
const sortedLabels = _.keys(metricLabels).sort();
table.columns.push({ text: 'Time', type: 'time' });
table.columns.push({ text: 'Time', type: ColumnType.time });
_.each(sortedLabels, (label, labelIndex) => {
metricLabels[label] = labelIndex + 1;
table.columns.push({ text: label, filterable: true });

@ -206,12 +206,12 @@ describe('PrometheusDatasource', () => {
it('does align intervals that are a multiple of steps', () => {
const range = alignRange(1, 4, 3);
expect(range.start).toEqual(0);
expect(range.end).toEqual(6);
expect(range.end).toEqual(3);
});
it('does align intervals that are not a multiple of steps', () => {
const range = alignRange(1, 5, 3);
expect(range.start).toEqual(0);
expect(range.end).toEqual(6);
expect(range.end).toEqual(3);
});
});
@ -351,7 +351,7 @@ const timeSrv = {
};
describe('PrometheusDatasource', () => {
describe('When querying prometheus with one target using query editor target spec', async () => {
describe('When querying prometheus with one target using query editor target spec', () => {
let results;
const query = {
range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) },
@ -360,7 +360,7 @@ describe('PrometheusDatasource', () => {
};
// Interval alignment with step
const urlExpected =
'proxied/api/v1/query_range?query=' + encodeURIComponent('test{job="testjob"}') + '&start=60&end=240&step=60';
'proxied/api/v1/query_range?query=' + encodeURIComponent('test{job="testjob"}') + '&start=60&end=180&step=60';
beforeEach(async () => {
const response = {
@ -788,7 +788,7 @@ describe('PrometheusDatasource', () => {
interval: '5s',
};
// times get rounded up to interval
const urlExpected = 'proxied/api/v1/query_range?query=test&start=50&end=450&step=50';
const urlExpected = 'proxied/api/v1/query_range?query=test&start=50&end=400&step=50';
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
await ctx.ds.query(query);
@ -831,7 +831,7 @@ describe('PrometheusDatasource', () => {
interval: '10s',
};
// times get aligned to interval
const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=0&end=500&step=100';
const urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=0&end=400&step=100';
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
await ctx.ds.query(query);
@ -996,7 +996,7 @@ describe('PrometheusDatasource', () => {
const urlExpected =
'proxied/api/v1/query_range?query=' +
encodeURIComponent('rate(test[$__interval])') +
'&start=0&end=500&step=100';
'&start=0&end=400&step=100';
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
templateSrv.replace = jest.fn(str => str);
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
@ -1041,7 +1041,7 @@ describe('PrometheusDatasource', () => {
const urlExpected =
'proxied/api/v1/query_range?query=' +
encodeURIComponent('rate(test[$__interval])') +
'&start=50&end=450&step=50';
'&start=50&end=400&step=50';
templateSrv.replace = jest.fn(str => str);
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
@ -1166,7 +1166,7 @@ describe('PrometheusDatasource for POST', () => {
const dataExpected = {
query: 'test{job="testjob"}',
start: 1 * 60,
end: 3 * 60,
end: 2 * 60,
step: 60,
};
const query = {

@ -1,8 +1,13 @@
import _ from 'lodash';
import TableModel from 'app/core/table_model';
import { DataSourceApi, DataQueryOptions } from '@grafana/ui';
import { DataSourceApi, DataQueryOptions, TableData, TimeSeries } from '@grafana/ui';
import { TestDataQuery, Scenario } from './types';
type TestData = TimeSeries | TableData;
export interface TestDataRegistry {
[key: string]: TestData[];
}
export class TestDataDatasource implements DataSourceApi<TestDataQuery> {
id: number;
@ -42,26 +47,24 @@ export class TestDataDatasource implements DataSourceApi<TestDataQuery> {
},
})
.then(res => {
const data = [];
const data: TestData[] = [];
// Returns data in the order it was asked for.
// if the response has data with different refId, it is ignored
for (const query of queries) {
const results = res.data.results[query.refId];
if (!results) {
console.warn('No Results for:', query);
continue;
}
if (res.data.results) {
_.forEach(res.data.results, queryRes => {
if (queryRes.tables) {
for (const table of queryRes.tables) {
const model = new TableModel();
model.rows = table.rows;
model.columns = table.columns;
for (const table of results.tables || []) {
data.push(table as TableData);
}
data.push(model);
}
}
for (const series of queryRes.series) {
data.push({
target: series.name,
datapoints: series.points,
});
}
});
for (const series of results.series || []) {
data.push({ target: series.name, datapoints: series.points });
}
}
return { data: data };

@ -32,14 +32,14 @@ export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
};
render() {
const { height, width, options, panelData, renderCounter } = this.props;
const { height, width, options, data, renderCounter } = this.props;
return (
<ProcessedValuesRepeater
getProcessedValues={this.getProcessedValues}
renderValue={this.renderValue}
width={width}
height={height}
source={panelData}
source={data}
renderCounter={renderCounter}
orientation={options.orientation}
/>

@ -37,14 +37,14 @@ export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
};
render() {
const { height, width, options, panelData, renderCounter } = this.props;
const { height, width, options, data, renderCounter } = this.props;
return (
<ProcessedValuesRepeater
getProcessedValues={this.getProcessedValues}
renderValue={this.renderValue}
width={width}
height={height}
source={panelData}
source={data}
renderCounter={renderCounter}
orientation={options.orientation}
/>

@ -16,13 +16,13 @@ interface Props extends PanelProps<Options> {}
export class GraphPanel extends PureComponent<Props> {
render() {
const { panelData, timeRange, width, height } = this.props;
const { data, timeRange, width, height } = this.props;
const { showLines, showBars, showPoints } = this.props.options;
let vmSeries: TimeSeriesVMs;
if (panelData.timeSeries) {
if (data) {
vmSeries = processTimeSeries({
timeSeries: panelData.timeSeries,
data,
nullValueMode: NullValueMode.Ignore,
});
}

@ -0,0 +1,47 @@
// Libraries
import React, { PureComponent } from 'react';
// Components
import { Select, FormLabel, PanelOptionsGroup } from '@grafana/ui';
// Types
import { FormField, PanelEditorProps } from '@grafana/ui';
import { PieChartType } from '@grafana/ui';
import { PieChartOptions } from './types';
const labelWidth = 8;
const pieChartOptions = [{ value: PieChartType.PIE, label: 'Pie' }, { value: PieChartType.DONUT, label: 'Donut' }];
export class PieChartOptionsBox extends PureComponent<PanelEditorProps<PieChartOptions>> {
onPieTypeChange = pieType => this.props.onOptionsChange({ ...this.props.options, pieType: pieType.value });
onStrokeWidthChange = ({ target }) =>
this.props.onOptionsChange({ ...this.props.options, strokeWidth: target.value });
render() {
const { options } = this.props;
const { pieType, strokeWidth } = options;
return (
<PanelOptionsGroup title="PieChart">
<div className="gf-form">
<FormLabel width={labelWidth}>Type</FormLabel>
<Select
width={12}
options={pieChartOptions}
onChange={this.onPieTypeChange}
value={pieChartOptions.find(option => option.value === pieType)}
/>
</div>
<div className="gf-form">
<FormField
label="Divider width"
labelWidth={labelWidth}
onChange={this.onStrokeWidthChange}
value={strokeWidth}
/>
</div>
</PanelOptionsGroup>
);
}
}

@ -0,0 +1,57 @@
// Libraries
import React, { PureComponent } from 'react';
// Services & Utils
import { processTimeSeries, ThemeContext } from '@grafana/ui';
// Components
import { PieChart, PieChartDataPoint } from '@grafana/ui';
// Types
import { PieChartOptions } from './types';
import { PanelProps, NullValueMode } from '@grafana/ui/src/types';
interface Props extends PanelProps<PieChartOptions> {}
export class PieChartPanel extends PureComponent<Props> {
render() {
const { data, width, height, options } = this.props;
const { valueOptions } = options;
const datapoints: PieChartDataPoint[] = [];
if (data) {
const vmSeries = processTimeSeries({
data,
nullValueMode: NullValueMode.Null,
});
for (let i = 0; i < vmSeries.length; i++) {
const serie = vmSeries[i];
if (serie) {
datapoints.push({
value: serie.stats[valueOptions.stat],
name: serie.label,
color: serie.color,
});
}
}
}
// TODO: support table data
return (
<ThemeContext.Consumer>
{theme => (
<PieChart
width={width}
height={height}
datapoints={datapoints}
pieType={options.pieType}
strokeWidth={options.strokeWidth}
unit={valueOptions.unit}
theme={theme}
/>
)}
</ThemeContext.Consumer>
);
}
}

@ -0,0 +1,27 @@
import React, { PureComponent } from 'react';
import { PanelEditorProps, PanelOptionsGrid } from '@grafana/ui';
import PieChartValueEditor from './PieChartValueEditor';
import { PieChartOptionsBox } from './PieChartOptionsBox';
import { PieChartOptions, PieChartValueOptions } from './types';
export default class PieChartPanelEditor extends PureComponent<PanelEditorProps<PieChartOptions>> {
onValueOptionsChanged = (valueOptions: PieChartValueOptions) =>
this.props.onOptionsChange({
...this.props.options,
valueOptions,
});
render() {
const { onOptionsChange, options } = this.props;
return (
<>
<PanelOptionsGrid>
<PieChartValueEditor onChange={this.onValueOptionsChanged} options={options.valueOptions} />
<PieChartOptionsBox onOptionsChange={onOptionsChange} options={options} />
</PanelOptionsGrid>
</>
);
}
}

@ -0,0 +1,54 @@
import React, { PureComponent } from 'react';
import { FormLabel, PanelOptionsGroup, Select, UnitPicker } from '@grafana/ui';
import { PieChartValueOptions } from './types';
const statOptions = [
{ value: 'min', label: 'Min' },
{ value: 'max', label: 'Max' },
{ value: 'avg', label: 'Average' },
{ value: 'current', label: 'Current' },
{ value: 'total', label: 'Total' },
];
const labelWidth = 6;
export interface Props {
options: PieChartValueOptions;
onChange: (valueOptions: PieChartValueOptions) => void;
}
export default class PieChartValueEditor extends PureComponent<Props> {
onUnitChange = unit =>
this.props.onChange({
...this.props.options,
unit: unit.value,
});
onStatChange = stat =>
this.props.onChange({
...this.props.options,
stat: stat.value,
});
render() {
const { stat, unit } = this.props.options;
return (
<PanelOptionsGroup title="Value">
<div className="gf-form">
<FormLabel width={labelWidth}>Unit</FormLabel>
<UnitPicker defaultValue={unit} onChange={this.onUnitChange} />
</div>
<div className="gf-form">
<FormLabel width={labelWidth}>Value</FormLabel>
<Select
width={12}
options={statOptions}
onChange={this.onStatChange}
value={statOptions.find(option => option.value === stat)}
/>
</div>
</PanelOptionsGroup>
);
}
}

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="100px" height="100px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:#C9202F;}
.st2{fill:url(#SVGID_2_);}
</style>
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="34.3609" y1="59.9311" x2="106.9924" y2="-19.0856">
<stop offset="0" style="stop-color:#FFF33B"/>
<stop offset="5.948725e-02" style="stop-color:#FFE029"/>
<stop offset="0.1303" style="stop-color:#FFD218"/>
<stop offset="0.2032" style="stop-color:#FEC90F"/>
<stop offset="0.2809" style="stop-color:#FDC70C"/>
<stop offset="0.6685" style="stop-color:#F3903F"/>
<stop offset="0.8876" style="stop-color:#ED683C"/>
<stop offset="1" style="stop-color:#E93E3A"/>
</linearGradient>
<path class="st0" d="M51.8,0.1v47.4l45.1-14.7C89.8,13.4,72.4,0.8,51.8,0.1z"/>
<path class="st1" d="M98,36.3L52.9,50.9l17.7,24.3l10.2,14C97.1,76.6,103.7,56.1,98,36.3z"/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="1.519853e-02" y1="50.001" x2="77.8424" y2="50.001">
<stop offset="0" style="stop-color:#04A64D"/>
<stop offset="1" style="stop-color:#007E39"/>
</linearGradient>
<path class="st2" d="M48.2,50.6V0.1C21.4,1,0,23,0,50C0,77.5,22.4,99.9,50,99.9c10.5,0,19.4-2.7,27.9-8.5L48.2,50.6z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -0,0 +1,10 @@
import { ReactPanelPlugin } from '@grafana/ui';
import PieChartPanelEditor from './PieChartPanelEditor';
import { PieChartPanel } from './PieChartPanel';
import { PieChartOptions, defaults } from './types';
export const reactPanel = new ReactPanelPlugin<PieChartOptions>(PieChartPanel);
reactPanel.setEditor(PieChartPanelEditor);
reactPanel.setDefaults(defaults);

@ -0,0 +1,19 @@
{
"type": "panel",
"name": "PieChart v2",
"id": "piechart",
"state": "alpha",
"dataFormats": ["time_series"],
"info": {
"author": {
"name": "Grafana Project",
"url": "https://grafana.com"
},
"logos": {
"small": "img/icon_piechart.svg",
"large": "img/icon_piechart.svg"
}
}
}

@ -0,0 +1,21 @@
import { PieChartType } from '@grafana/ui';
export interface PieChartOptions {
pieType: PieChartType;
strokeWidth: number;
valueOptions: PieChartValueOptions;
}
export interface PieChartValueOptions {
unit: string;
stat: string;
}
export const defaults: PieChartOptions = {
pieType: PieChartType.PIE,
strokeWidth: 1,
valueOptions: {
unit: 'short',
stat: 'current',
},
};

@ -8,7 +8,7 @@ import kbn from 'app/core/utils/kbn';
import config from 'app/core/config';
import TimeSeries from 'app/core/time_series2';
import { MetricsPanelCtrl } from 'app/plugins/sdk';
import { GrafanaThemeType, getValueFormat, getColorFromHexRgbOrName } from '@grafana/ui';
import { GrafanaThemeType, getValueFormat, getColorFromHexRgbOrName, isTableData } from '@grafana/ui';
class SingleStatCtrl extends MetricsPanelCtrl {
static templateUrl = 'module.html';
@ -112,7 +112,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
scopedVars: _.extend({}, this.panel.scopedVars),
};
if (dataList.length > 0 && dataList[0].type === 'table') {
if (dataList.length > 0 && isTableData(dataList[0])) {
this.dataType = 'table';
const tableData = dataList.map(this.tableHandler.bind(this));
this.setTableValues(tableData, data);

@ -4,28 +4,33 @@ import React, { PureComponent, CSSProperties } from 'react';
// Types
import { SingleStatOptions, SingleStatBaseOptions } from './types';
import { processSingleStatPanelData, DisplayValue, PanelProps } from '@grafana/ui';
import { DisplayValue, PanelProps, processTimeSeries, NullValueMode } from '@grafana/ui';
import { config } from 'app/core/config';
import { getDisplayProcessor } from '@grafana/ui';
import { ProcessedValuesRepeater } from './ProcessedValuesRepeater';
export const getSingleStatValues = (props: PanelProps<SingleStatBaseOptions>): DisplayValue[] => {
const { panelData, replaceVariables, options } = props;
const { data, replaceVariables, options } = props;
const { valueOptions, valueMappings } = options;
const { unit, decimals, stat } = valueOptions;
const processor = getDisplayProcessor({
unit: valueOptions.unit,
decimals: valueOptions.decimals,
unit,
decimals,
mappings: valueMappings,
thresholds: options.thresholds,
prefix: replaceVariables(valueOptions.prefix),
suffix: replaceVariables(valueOptions.suffix),
theme: config.theme,
});
return processSingleStatPanelData({
panelData: panelData,
stat: valueOptions.stat,
}).map(stat => processor(stat.value));
return processTimeSeries({
data,
nullValueMode: NullValueMode.Null,
}).map((series, index) => {
const value = stat !== 'name' ? series.stats[stat] : series.label;
return processor(value);
});
};
export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>> {
@ -50,14 +55,14 @@ export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>
};
render() {
const { height, width, options, panelData, renderCounter } = this.props;
const { height, width, options, data, renderCounter } = this.props;
return (
<ProcessedValuesRepeater
getProcessedValues={this.getProcessedValues}
renderValue={this.renderValue}
width={width}
height={height}
source={panelData}
source={data}
renderCounter={renderCounter}
orientation={options.orientation}
/>

@ -6,6 +6,7 @@ import { transformDataToTable } from './transformers';
import { tablePanelEditor } from './editor';
import { columnOptionsTab } from './column_options';
import { TableRenderer } from './renderer';
import { isTableData } from '@grafana/ui';
class TablePanelCtrl extends MetricsPanelCtrl {
static templateUrl = 'module.html';
@ -104,7 +105,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
// automatically correct transform mode based on data
if (this.dataRaw && this.dataRaw.length) {
if (this.dataRaw[0].type === 'table') {
if (isTableData(this.dataRaw[0])) {
this.panel.transform = 'table';
} else {
if (this.dataRaw[0].type === 'docs') {

@ -14,15 +14,15 @@ export class TablePanel extends Component<Props> {
}
render() {
const { panelData, options } = this.props;
const { data, options } = this.props;
if (!panelData || !panelData.tableData) {
if (data.length < 1) {
return <div>No Table Data...</div>;
}
return (
<ThemeContext.Consumer>
{theme => <Table {...this.props} {...options} theme={theme} data={panelData.tableData} />}
{theme => <Table {...this.props} {...options} theme={theme} data={data[0]} />}
</ThemeContext.Consumer>
);
}

@ -54,6 +54,7 @@
@import 'components/panel_alertlist';
@import 'components/panel_dashlist';
@import 'components/panel_gettingstarted';
@import 'components/panel_piechart';
@import 'components/panel_pluginlist';
@import 'components/panel_singlestat';
@import 'components/panel_table';

@ -90,7 +90,7 @@ $grid-gutter-width: 30px !default;
// Typography
// -------------------------
$font-family-sans-serif: 'Roboto', Helvetica, Arial, sans-serif;
$font-family-sans-serif: 'Roboto', 'Helvetica Neue', Arial, sans-serif;
$font-family-monospace: Menlo, Monaco, Consolas, 'Courier New', monospace;
$font-size-root: 14px !default;
@ -113,7 +113,6 @@ $font-size-h4: 18px !default;
$font-size-h5: 16px !default;
$font-size-h6: 14px !default;
$headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
$headings-line-height: 1.1 !default;
// Components
@ -200,10 +199,8 @@ $btn-semi-transparent: rgba(0, 0, 0, 0.2) !default;
$side-menu-width: 60px;
// dashboard
$dashboard-padding: 10px * 2;
$panel-horizontal-padding: 10;
$panel-vertical-padding: 5;
$panel-padding: 0px $panel-horizontal-padding + 0px $panel-vertical-padding + 0px $panel-horizontal-padding + 0px;
$dashboard-padding: $space-md;
$panel-padding: 0 $space-md $space-sm $space-md;
// tabs
$tabs-padding: 10px 15px 9px;

@ -110,7 +110,6 @@ h6,
.h5,
.h6 {
margin-bottom: $space-sm;
font-family: $headings-font-family;
font-weight: $font-weight-regular;
line-height: $headings-line-height;
color: $headings-color;

@ -83,10 +83,6 @@
padding: 0 $dashboard-padding $space-sm $dashboard-padding;
}
.panel-editor-container__panel {
margin: 0 $dashboard-padding;
}
.search-container {
left: 0 !important;
}

@ -3,8 +3,7 @@ $column-horizontal-spacing: 10px;
.logs-panel-options {
display: flex;
background-color: $page-bg;
padding: $panel-padding;
padding-top: 10px;
padding: $space-sm $space-md $space-sm $space-md;
border-radius: $border-radius;
margin: $space-md 0 $space-sm;
border: $panel-border;

@ -0,0 +1,40 @@
.piechart-panel {
position: relative;
display: table;
width: 100%;
height: 100%;
.piechart-container {
top: 10px;
margin: auto;
svg {
width: 100%;
height: 100%;
}
}
.piechart-tooltip {
white-space: nowrap;
font-size: 12px;
background-color: #141414;
color: #d8d9da;
opacity: 0;
position: absolute;
.piechart-tooltip-time {
text-align: center;
position: relative;
padding: 0.2rem;
font-weight: bold;
color: #d8d9da;
.piechart-tooltip-value {
display: table-cell;
font-weight: bold;
padding: 15px;
text-align: right;
}
}
}
}

@ -13,7 +13,7 @@
.tabbed-view-header {
box-shadow: $page-header-shadow;
border-bottom: 1px solid $page-header-border-color;
padding: 0 $dashboard-padding;
padding: 0 $space-md;
@include clearfix();
}

@ -260,7 +260,6 @@ div.flot-text {
}
.dashboard-header {
font-family: $headings-font-family;
font-size: $font-size-h3;
text-align: center;
overflow: hidden;
@ -273,10 +272,6 @@ div.flot-text {
}
}
.panel-full-edit {
padding-top: $dashboard-padding;
}
.dashboard-loading {
height: 60vh;
display: flex;

@ -176,12 +176,10 @@
}
.explore-panel__header {
padding: $panel-padding;
padding-top: 5px;
padding-bottom: 0;
padding: $space-sm $space-md 0 $space-md;
display: flex;
cursor: pointer;
margin-bottom: 5px;
margin-bottom: $space-sm;
transition: all 0.1s linear;
}

@ -12,8 +12,7 @@ module.exports = function(options) {
options: {
importLoaders: 2,
url: options.preserveUrl,
sourceMap: options.sourceMap,
minimize: options.minimize,
sourceMap: options.sourceMap
},
},
{

@ -19,13 +19,6 @@ module.exports = merge(common, {
light: './public/sass/grafana.light.scss',
},
output: {
path: path.resolve(__dirname, '../../public/build'),
filename: '[name].[hash].js',
// Keep publicPath relative for host.com/grafana/ deployments
publicPath: "public/build/",
},
module: {
rules: [
{
@ -50,7 +43,7 @@ module.exports = merge(common, {
},
},
},
require('./sass.rule.js')({ sourceMap: false, minimize: false, preserveUrl: false }),
require('./sass.rule.js')({ sourceMap: false, preserveUrl: false }),
{
test: /\.(png|jpg|gif|ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/,
loader: 'file-loader'
@ -59,7 +52,7 @@ module.exports = merge(common, {
},
plugins: [
new CleanWebpackPlugin('../../public/build', { allowExternal: true }),
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: "grafana.[name].[hash].css"
}),

@ -56,7 +56,7 @@ module.exports = merge(common, {
plugins: [
[require('@rtsao/plugin-proposal-class-properties'), { loose: true }],
'angularjs-annotate',
'syntax-dynamic-import', // needed for `() => import()` in routes.ts
'@babel/plugin-syntax-dynamic-import', // needed for `() => import()` in routes.ts
'react-hot-loader/babel',
],
presets: [
@ -98,7 +98,7 @@ module.exports = merge(common, {
},
plugins: [
new CleanWebpackPlugin('../public/build', { allowExternal: true }),
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
filename: path.resolve(__dirname, '../../public/views/index.html'),
template: path.resolve(__dirname, '../../public/views/index-template.html'),

@ -1,7 +1,7 @@
'use strict';
const merge = require('webpack-merge');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const common = require('./webpack.common.js');
const path = require('path');
const ngAnnotatePlugin = require('ng-annotate-webpack-plugin');
@ -43,14 +43,14 @@ module.exports = merge(common, {
},
},
require('./sass.rule.js')({
sourceMap: false, minimize: false, preserveUrl: false
sourceMap: false, preserveUrl: false
})
]
},
optimization: {
minimizer: [
new UglifyJsPlugin({
cache: true,
new TerserPlugin({
cache: false,
parallel: true,
sourceMap: true
}),

@ -27,7 +27,7 @@
"noUnusedLocals": true,
"baseUrl": "public",
"pretty": true,
"typeRoots": ["node_modules/@types", "types"],
"typeRoots": ["node_modules/@types", "public/app/types"],
"paths": {
"app": ["app"],
"sass": ["sass"]

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save