diff --git a/.circleci/config.yml b/.circleci/config.yml
index 69cea87dccd..da0e0665285 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -56,6 +56,20 @@ jobs:
name: postgres integration tests
command: './scripts/circle-test-postgres.sh'
+ cache-server-test:
+ docker:
+ - image: circleci/golang:1.11.5
+ - image: circleci/redis:4-alpine
+ - image: memcached
+ working_directory: /go/src/github.com/grafana/grafana
+ steps:
+ - checkout
+ - run: dockerize -wait tcp://127.0.0.1:11211 -timeout 120s
+ - run: dockerize -wait tcp://127.0.0.1:6379 -timeout 120s
+ - run:
+ name: cache server tests
+ command: './scripts/circle-test-cache-servers.sh'
+
codespell:
docker:
- image: circleci/python
@@ -545,6 +559,8 @@ workflows:
filters: *filter-not-release-or-master
- postgres-integration-test:
filters: *filter-not-release-or-master
+ - cache-server-test:
+ filters: *filter-not-release-or-master
- grafana-docker-pr:
requires:
- build
@@ -554,4 +570,5 @@ workflows:
- gometalinter
- mysql-integration-test
- postgres-integration-test
+ - cache-server-test
filters: *filter-not-release-or-master
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b648603579a..1f09ead1fe3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,10 @@
* **Heatmap**: `Middle` bucket bound option [#15683](https://github.com/grafana/grafana/issues/15683)
* **Heatmap**: `Reverse order` option for changing order of buckets [#15683](https://github.com/grafana/grafana/issues/15683)
* **VictorOps**: Adds more information to the victor ops notifiers [#15744](https://github.com/grafana/grafana/issues/15744), thx [@zhulongcheng](https://github.com/zhulongcheng)
+* **Cache**: Adds support for using out of proc caching in the backend [#10816](https://github.com/grafana/grafana/issues/10816)
+* **Dataproxy**: Make it possible to add user details to requests sent to the dataproxy [#6359](https://github.com/grafana/grafana/issues/6359) and [#15931](https://github.com/grafana/grafana/issues/15931)
+* **Auth**: Support listing and revoking auth tokens via API [#15836](https://github.com/grafana/grafana/issues/15836)
+* **Datasource**: Only log connection string in dev environment [#16001](https://github.com/grafana/grafana/issues/16001)
### Bug Fixes
* **Api**: Invalid org invite code [#10506](https://github.com/grafana/grafana/issues/10506)
diff --git a/conf/defaults.ini b/conf/defaults.ini
index 044d8e59a7a..492525e6b5f 100644
--- a/conf/defaults.ini
+++ b/conf/defaults.ini
@@ -106,6 +106,17 @@ path = grafana.db
# For "sqlite3" only. cache mode setting used for connecting to the database
cache_mode = private
+#################################### Cache server #############################
+[remote_cache]
+# Either "redis", "memcached" or "database" default is "database"
+type = database
+
+# cache connectionstring options
+# database: will use Grafana primary database.
+# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=grafana`
+# memcache: 127.0.0.1:11211
+connstr =
+
#################################### Session #############################
[session]
# Either "memory", "file", "redis", "mysql", "postgres", "memcache", default is "file"
@@ -146,6 +157,9 @@ logging = false
# How long the data proxy should wait before timing out default is 30 (seconds)
timeout = 30
+# If enabled and user is not anonymous, data proxy will add X-Grafana-User header with username into the request, default is false.
+send_user_header = false
+
#################################### Analytics ###########################
[analytics]
# Server reporting, sends usage counters to stats.grafana.org every 24 hours.
diff --git a/conf/sample.ini b/conf/sample.ini
index dc1e4fbde8e..fd414c2af47 100644
--- a/conf/sample.ini
+++ b/conf/sample.ini
@@ -102,6 +102,17 @@ log_queries =
# For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared)
;cache_mode = private
+#################################### Cache server #############################
+[remote_cache]
+# Either "redis", "memcached" or "database" default is "database"
+;type = database
+
+# cache connectionstring options
+# database: will use Grafana primary database.
+# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=grafana`
+# memcache: 127.0.0.1:11211
+;connstr =
+
#################################### Session ####################################
[session]
# Either "memory", "file", "redis", "mysql", "postgres", default is "file"
@@ -133,6 +144,9 @@ log_queries =
# How long the data proxy should wait before timing out default is 30 (seconds)
;timeout = 30
+# If enabled and user is not anonymous, data proxy will add X-Grafana-User header with username into the request, default is false.
+;send_user_header = false
+
#################################### Analytics ####################################
[analytics]
# Server reporting, sends usage counters to stats.grafana.org every 24 hours.
diff --git a/devenv/docker/blocks/redis/docker-compose.yaml b/devenv/docker/blocks/redis/docker-compose.yaml
index 65071d4966b..fb56afaac1c 100644
--- a/devenv/docker/blocks/redis/docker-compose.yaml
+++ b/devenv/docker/blocks/redis/docker-compose.yaml
@@ -1,4 +1,4 @@
- memcached:
+ redis:
image: redis:latest
ports:
- "6379:6379"
diff --git a/docs/sources/enterprise/index.md b/docs/sources/enterprise/index.md
index 5d524dcbee2..421e94cbf9f 100644
--- a/docs/sources/enterprise/index.md
+++ b/docs/sources/enterprise/index.md
@@ -38,6 +38,8 @@ With a Grafana Enterprise license you will get access to premium plugins, includ
* [DataDog](https://grafana.com/plugins/grafana-datadog-datasource)
* [Dynatrace](https://grafana.com/plugins/grafana-dynatrace-datasource)
* [New Relic](https://grafana.com/plugins/grafana-newrelic-datasource)
+* [Amazon Timestream](https://grafana.com/plugins/grafana-timestream-datasource)
+* [Oracle Database](https://grafana.com/plugins/grafana-oracle-datasource)
## Try Grafana Enterprise
diff --git a/docs/sources/http_api/admin.md b/docs/sources/http_api/admin.md
index a27fd2aac14..c2d540c452b 100644
--- a/docs/sources/http_api/admin.md
+++ b/docs/sources/http_api/admin.md
@@ -341,3 +341,105 @@ Content-Type: application/json
{"state": "new state", "message": "alerts pause/un paused", "alertsAffected": 100}
```
+
+## Auth tokens for User
+
+`GET /api/admin/users/:id/auth-tokens`
+
+Return a list of all auth tokens (devices) that the user currently have logged in from.
+
+Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
+
+**Example Request**:
+
+```http
+GET /api/admin/users/1/auth-tokens HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+[
+ {
+ "id": 361,
+ "isActive": false,
+ "clientIp": "127.0.0.1",
+ "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36",
+ "createdAt": "2019-03-05T21:22:54+01:00",
+ "seenAt": "2019-03-06T19:41:06+01:00"
+ },
+ {
+ "id": 364,
+ "isActive": false,
+ "clientIp": "127.0.0.1",
+ "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1",
+ "createdAt": "2019-03-06T19:41:19+01:00",
+ "seenAt": "2019-03-06T19:41:21+01:00"
+ }
+]
+```
+
+## Revoke auth token for User
+
+`POST /api/admin/users/:id/revoke-auth-token`
+
+Revokes the given auth token (device) for the user. User of issued auth token (device) will no longer be logged in
+and will be required to authenticate again upon next activity.
+
+Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
+
+**Example Request**:
+
+```http
+POST /api/admin/users/1/revoke-auth-token HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+
+{
+ "authTokenId": 364
+}
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+{
+ "message": "User auth token revoked"
+}
+```
+
+## Logout User
+
+`POST /api/admin/users/:id/logout`
+
+Logout user revokes all auth tokens (devices) for the user. User of issued auth tokens (devices) will no longer be logged in
+and will be required to authenticate again upon next activity.
+
+Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
+
+**Example Request**:
+
+```http
+POST /api/admin/users/1/logout HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+{
+ "message": "User auth token revoked"
+}
+```
diff --git a/docs/sources/http_api/user.md b/docs/sources/http_api/user.md
index 669e8003247..a81f608c2f5 100644
--- a/docs/sources/http_api/user.md
+++ b/docs/sources/http_api/user.md
@@ -478,3 +478,75 @@ Content-Type: application/json
{"message":"Dashboard unstarred"}
```
+
+## Auth tokens of the actual User
+
+`GET /api/user/auth-tokens`
+
+Return a list of all auth tokens (devices) that the actual user currently have logged in from.
+
+**Example Request**:
+
+```http
+GET /api/user/auth-tokens HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+[
+ {
+ "id": 361,
+ "isActive": true,
+ "clientIp": "127.0.0.1",
+ "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36",
+ "createdAt": "2019-03-05T21:22:54+01:00",
+ "seenAt": "2019-03-06T19:41:06+01:00"
+ },
+ {
+ "id": 364,
+ "isActive": false,
+ "clientIp": "127.0.0.1",
+ "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1",
+ "createdAt": "2019-03-06T19:41:19+01:00",
+ "seenAt": "2019-03-06T19:41:21+01:00"
+ }
+]
+```
+
+## Revoke an auth token of the actual User
+
+`POST /api/user/revoke-auth-token`
+
+Revokes the given auth token (device) for the actual user. User of issued auth token (device) will no longer be logged in
+and will be required to authenticate again upon next activity.
+
+**Example Request**:
+
+```http
+POST /api/user/revoke-auth-token HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+{
+ "authTokenId": 364
+}
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+{
+ "message": "User auth token revoked"
+}
+```
diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md
index 3d1b25979c3..d94bacc5779 100644
--- a/docs/sources/installation/configuration.md
+++ b/docs/sources/installation/configuration.md
@@ -179,7 +179,6 @@ Path to the certificate key file (if `protocol` is set to `https`).
Set to true for Grafana to log all HTTP requests (not just errors). These are logged as Info level events
to grafana log.
-
@@ -262,6 +261,19 @@ Set to `true` to log the sql calls and execution times.
For "sqlite3" only. [Shared cache](https://www.sqlite.org/sharedcache.html) setting used for connecting to the database. (private, shared)
Defaults to private.
+
+
+## [remote_cache]
+
+### type
+
+Either `redis`, `memcached` or `database` default is `database`
+
+### connstr
+
+The remote cache connection string. Leave empty when using `database` since it will use the primary database.
+Redis example config: `addr=127.0.0.1:6379,pool_size=100,db=grafana`
+Memcache example: `127.0.0.1:11211`
@@ -399,6 +411,22 @@ How long sessions lasts in seconds. Defaults to `86400` (24 hours).
+## [dataproxy]
+
+### logging
+
+This enables data proxy logging, default is false.
+
+### timeout
+
+How long the data proxy should wait before timing out default is 30 (seconds)
+
+### send_user_header
+
+If enabled and user is not anonymous, data proxy will add X-Grafana-User header with username into the request, default is false.
+
+
+
## [analytics]
### reporting_enabled
diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx
index eca2e8079e2..69cdbf5bff1 100644
--- a/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx
+++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx
@@ -11,17 +11,15 @@ jest.mock('jquery', () => ({
const setup = (propOverrides?: object) => {
const props: Props = {
maxValue: 100,
- valueMappings: [],
minValue: 0,
- prefix: '',
- suffix: '',
displayMode: 'basic',
thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }],
- unit: 'none',
height: 300,
width: 300,
- value: 25,
- decimals: 0,
+ value: {
+ text: '25',
+ numeric: 25,
+ },
theme: getTheme(),
orientation: VizOrientation.Horizontal,
};
diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx
index e4f0f37b4a1..99d2d061a11 100644
--- a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx
+++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx
@@ -3,26 +3,21 @@ import React, { PureComponent, CSSProperties, ReactNode } from 'react';
import tinycolor from 'tinycolor2';
// Utils
-import { getColorFromHexRgbOrName, getValueFormat, getThresholdForValue } from '../../utils';
+import { getColorFromHexRgbOrName, getThresholdForValue, DisplayValue } from '../../utils';
// Types
-import { Themeable, TimeSeriesValue, Threshold, ValueMapping, VizOrientation } from '../../types';
+import { Themeable, TimeSeriesValue, Threshold, VizOrientation } from '../../types';
const BAR_SIZE_RATIO = 0.8;
export interface Props extends Themeable {
height: number;
- unit: string;
width: number;
thresholds: Threshold[];
- valueMappings: ValueMapping[];
- value: TimeSeriesValue;
+ value: DisplayValue;
maxValue: number;
minValue: number;
orientation: VizOrientation;
- prefix?: string;
- suffix?: string;
- decimals?: number;
displayMode: 'basic' | 'lcd' | 'gradient';
}
@@ -30,44 +25,30 @@ export class BarGauge extends PureComponent {
static defaultProps: Partial = {
maxValue: 100,
minValue: 0,
- value: 100,
- unit: 'none',
- displayMode: 'basic',
+ value: {
+ text: '100',
+ numeric: 100,
+ },
+ displayMode: 'lcd',
orientation: VizOrientation.Horizontal,
thresholds: [],
- valueMappings: [],
};
render() {
- const { maxValue, minValue, unit, decimals, displayMode } = this.props;
-
- const numericValue = this.getNumericValue();
- const valuePercent = Math.min(numericValue / (maxValue - minValue), 1);
-
- const formatFunc = getValueFormat(unit);
- const valueFormatted = formatFunc(numericValue, decimals);
-
- switch (displayMode) {
+ switch (this.props.displayMode) {
case 'lcd':
- return this.renderRetroBars(valueFormatted, valuePercent);
+ return this.renderRetroBars();
case 'basic':
case 'gradient':
default:
- return this.renderBasicAndGradientBars(valueFormatted, valuePercent);
+ return this.renderBasicAndGradientBars();
}
}
- getNumericValue(): number {
- if (Number.isFinite(this.props.value as number)) {
- return this.props.value as number;
- }
- return 0;
- }
-
getValueColors(): BarColors {
const { thresholds, theme, value } = this.props;
- const activeThreshold = getThresholdForValue(thresholds, value);
+ const activeThreshold = getThresholdForValue(thresholds, value.numeric);
if (activeThreshold !== null) {
const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
@@ -111,9 +92,8 @@ export class BarGauge extends PureComponent {
}
getBarGradient(maxSize: number): string {
- const { minValue, maxValue, thresholds } = this.props;
+ const { minValue, maxValue, thresholds, value } = this.props;
const cssDirection = this.isVertical ? '0deg' : '90deg';
- const currentValue = this.getNumericValue();
let gradient = '';
let lastpos = 0;
@@ -127,7 +107,7 @@ export class BarGauge extends PureComponent {
if (gradient === '') {
gradient = `linear-gradient(${cssDirection}, ${color}, ${color}`;
- } else if (currentValue < threshold.value) {
+ } else if (value.numeric < threshold.value) {
break;
} else {
lastpos = pos;
@@ -135,18 +115,18 @@ export class BarGauge extends PureComponent {
}
}
- console.log(gradient);
return gradient + ')';
}
- renderBasicAndGradientBars(valueFormatted: string, valuePercent: number): ReactNode {
- const { height, width, displayMode } = this.props;
+ renderBasicAndGradientBars(): ReactNode {
+ const { height, width, displayMode, maxValue, minValue, value } = this.props;
+ const valuePercent = Math.min(value.numeric / (maxValue - minValue), 1);
const maxSize = this.size * BAR_SIZE_RATIO;
const barSize = Math.max(valuePercent * maxSize, 0);
const colors = this.getValueColors();
const spaceForText = this.isVertical ? width : Math.min(this.size - maxSize, height);
- const valueStyles = this.getValueStyles(valueFormatted, colors.value, spaceForText);
+ const valueStyles = this.getValueStyles(value.text, colors.value, spaceForText);
const isBasic = displayMode === 'basic';
const containerStyles: CSSProperties = {
@@ -199,7 +179,7 @@ export class BarGauge extends PureComponent {
return (
diff --git a/public/app/features/explore/Table.tsx b/public/app/features/explore/Table.tsx
index 4946a6a505d..bbf338df8f9 100644
--- a/public/app/features/explore/Table.tsx
+++ b/public/app/features/explore/Table.tsx
@@ -40,11 +40,15 @@ export default class Table extends PureComponent {
const tableModel = data || EMPTY_TABLE;
const columnNames = tableModel.columns.map(({ text }) => text);
const columns = tableModel.columns.map(({ filterable, text }) => ({
- Header: text,
+ Header: () => {text},
accessor: text,
className: VALUE_REGEX.test(text) ? 'text-right' : '',
show: text !== 'Time',
- Cell: row => {row.value},
+ Cell: row => (
+
+ {row.value}
+
+ ),
}));
const noDataText = data ? 'The queries returned no data for a table.' : '';
diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts
index e0b84320fa7..fda8fe5eef4 100644
--- a/public/app/features/explore/state/actions.ts
+++ b/public/app/features/explore/state/actions.ts
@@ -60,7 +60,6 @@ import {
splitCloseAction,
splitOpenAction,
addQueryRowAction,
- AddQueryRowPayload,
toggleGraphAction,
toggleLogsAction,
toggleTableAction,
@@ -87,9 +86,12 @@ const updateExploreUIState = (exploreId, uiStateFragment: Partial {
- const query = generateEmptyQuery(index + 1);
- return addQueryRowAction({ exploreId, index, query });
+export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult {
+ return (dispatch, getState) => {
+ const query = generateEmptyQuery(getState().explore[exploreId].queries, index);
+
+ dispatch(addQueryRowAction({ exploreId, index, query }));
+ };
}
/**
@@ -126,10 +128,10 @@ export function changeQuery(
index: number,
override: boolean
): ThunkResult {
- return dispatch => {
+ return (dispatch, getState) => {
// Null query means reset
if (query === null) {
- query = { ...generateEmptyQuery(index) };
+ query = { ...generateEmptyQuery(getState().explore[exploreId].queries) };
}
dispatch(changeQueryAction({ exploreId, query, index, override }));
@@ -287,7 +289,7 @@ export function importQueries(
const nextQueries = importedQueries.map((q, i) => ({
...q,
- ...generateEmptyQuery(i),
+ ...generateEmptyQuery(queries),
}));
dispatch(queriesImportedAction({ exploreId, queries: nextQueries }));
@@ -629,9 +631,9 @@ export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkRes
* Use this action for clicks on query examples. Triggers a query run.
*/
export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): ThunkResult {
- return dispatch => {
+ return (dispatch, getState) => {
// Inject react keys into query objects
- const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery() }));
+ const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery(getState().explore[exploreId].queries) }));
dispatch(setQueriesAction({ exploreId, queries }));
dispatch(runQueries(exploreId));
};
diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts
index a8815842c89..32bfe09a96b 100644
--- a/public/app/features/explore/state/reducers.ts
+++ b/public/app/features/explore/state/reducers.ts
@@ -127,7 +127,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta
const { query, index } = action.payload;
// Override path: queries are completely reset
- const nextQuery: DataQuery = { ...query, ...generateEmptyQuery(index) };
+ const nextQuery: DataQuery = { ...query, ...generateEmptyQuery(state.queries) };
const nextQueries = [...queries];
nextQueries[index] = nextQuery;
@@ -267,7 +267,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta
// Modify all queries
nextQueries = queries.map((query, i) => ({
...modifier({ ...query }, modification),
- ...generateEmptyQuery(i),
+ ...generateEmptyQuery(state.queries),
}));
// Discard all ongoing transactions
nextQueryTransactions = [];
@@ -276,7 +276,9 @@ export const itemReducer = reducerFactory({} as ExploreItemSta
nextQueries = queries.map((query, i) => {
// Synchronize all queries with local query cache to ensure consistency
// TODO still needed?
- return i === index ? { ...modifier({ ...query }, modification), ...generateEmptyQuery(i) } : query;
+ return i === index
+ ? { ...modifier({ ...query }, modification), ...generateEmptyQuery(state.queries) }
+ : query;
});
nextQueryTransactions = queryTransactions
// Consume the hint corresponding to the action
diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts
index 9a156652a65..ab9d9aba08a 100644
--- a/public/app/features/plugins/built_in_plugins.ts
+++ b/public/app/features/plugins/built_in_plugins.ts
@@ -25,6 +25,7 @@ import * as heatmapPanel from 'app/plugins/panel/heatmap/module';
import * as tablePanel from 'app/plugins/panel/table/module';
import * as table2Panel from 'app/plugins/panel/table2/module';
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 barGaugePanel from 'app/plugins/panel/bargauge/module';
@@ -57,6 +58,7 @@ const builtInPlugins = {
'app/plugins/panel/table/module': tablePanel,
'app/plugins/panel/table2/module': table2Panel,
'app/plugins/panel/singlestat/module': singlestatPanel,
+ 'app/plugins/panel/singlestat2/module': singlestatPanel2,
'app/plugins/panel/gettingstarted/module': gettingStartedPanel,
'app/plugins/panel/gauge/module': gaugePanel,
'app/plugins/panel/bargauge/module': barGaugePanel,
diff --git a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx
index 708b472ec2e..8b612e6869e 100644
--- a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx
+++ b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx
@@ -2,56 +2,47 @@
import React, { PureComponent } from 'react';
// Services & Utils
-import { processSingleStatPanelData } from '@grafana/ui';
+import { DisplayValue, PanelProps, BarGauge } from '@grafana/ui';
import { config } from 'app/core/config';
-// Components
-import { BarGauge, VizRepeater } from '@grafana/ui';
-
// Types
import { BarGaugeOptions } from './types';
-import { PanelProps, SingleStatValueInfo } from '@grafana/ui/src/types';
+import { getSingleStatValues } from '../singlestat2/SingleStatPanel';
+import { ProcessedValuesRepeater } from '../singlestat2/ProcessedValuesRepeater';
-interface Props extends PanelProps {}
-
-export class BarGaugePanel extends PureComponent {
- renderBarGauge(value: SingleStatValueInfo, width, height) {
- const { replaceVariables, options } = this.props;
- const { valueOptions } = options;
-
- const prefix = replaceVariables(valueOptions.prefix);
- const suffix = replaceVariables(valueOptions.suffix);
+export class BarGaugePanel extends PureComponent> {
+ renderValue = (value: DisplayValue, width: number, height: number): JSX.Element => {
+ const { options } = this.props;
return (
);
- }
+ };
+
+ getProcessedValues = (): DisplayValue[] => {
+ return getSingleStatValues(this.props);
+ };
render() {
- const { panelData, options, width, height } = this.props;
-
- const values = processSingleStatPanelData({
- panelData: panelData,
- stat: options.valueOptions.stat,
- });
-
+ const { height, width, options, panelData } = this.props;
+ const { orientation } = options;
return (
-
- {({ vizHeight, vizWidth, value }) => this.renderBarGauge(value, vizWidth, vizHeight)}
-
+
);
}
}
diff --git a/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx b/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx
index 87e5defd277..3404c8d8805 100644
--- a/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx
+++ b/public/app/plugins/panel/bargauge/BarGaugePanelEditor.tsx
@@ -2,13 +2,13 @@
import React, { PureComponent } from 'react';
// Components
-import { SingleStatValueEditor } from 'app/plugins/panel/gauge/SingleStatValueEditor';
import { ThresholdsEditor, ValueMappingsEditor, PanelOptionsGrid, PanelOptionsGroup, FormField } from '@grafana/ui';
// Types
import { FormLabel, PanelEditorProps, Threshold, Select, ValueMapping } from '@grafana/ui';
import { BarGaugeOptions, orientationOptions, displayModes } from './types';
-import { SingleStatValueOptions } from '../gauge/types';
+import { SingleStatValueEditor } from '../singlestat2/SingleStatValueEditor';
+import { SingleStatValueOptions } from '../singlestat2/types';
export class BarGaugePanelEditor extends PureComponent> {
onThresholdsChanged = (thresholds: Threshold[]) =>
diff --git a/public/app/plugins/panel/bargauge/module.tsx b/public/app/plugins/panel/bargauge/module.tsx
index e7f2e2d7738..3c46adeb4f9 100644
--- a/public/app/plugins/panel/bargauge/module.tsx
+++ b/public/app/plugins/panel/bargauge/module.tsx
@@ -3,18 +3,10 @@ import { ReactPanelPlugin } from '@grafana/ui';
import { BarGaugePanel } from './BarGaugePanel';
import { BarGaugePanelEditor } from './BarGaugePanelEditor';
import { BarGaugeOptions, defaults } from './types';
+import { singleStatBaseOptionsCheck } from '../singlestat2/module';
export const reactPanel = new ReactPanelPlugin(BarGaugePanel);
reactPanel.setEditor(BarGaugePanelEditor);
reactPanel.setDefaults(defaults);
-reactPanel.setPanelTypeChangedHook((options: BarGaugeOptions, prevPluginId?: string, prevOptions?: any) => {
- if (prevOptions && prevOptions.valueOptions) {
- options.valueOptions = prevOptions.valueOptions;
- options.thresholds = prevOptions.thresholds;
- options.maxValue = prevOptions.maxValue;
- options.minValue = prevOptions.minValue;
- }
-
- return options;
-});
+reactPanel.setPanelTypeChangedHook(singleStatBaseOptionsCheck);
diff --git a/public/app/plugins/panel/bargauge/types.ts b/public/app/plugins/panel/bargauge/types.ts
index 6c45b535b36..4a22a64bdd4 100644
--- a/public/app/plugins/panel/bargauge/types.ts
+++ b/public/app/plugins/panel/bargauge/types.ts
@@ -1,31 +1,27 @@
-import { Threshold, SelectOptionItem, ValueMapping, VizOrientation } from '@grafana/ui';
-import { SingleStatValueOptions } from '../gauge/types';
+import { VizOrientation, SelectOptionItem } from '@grafana/ui';
+import { SingleStatBaseOptions } from '../singlestat2/types';
-export interface BarGaugeOptions {
+export interface BarGaugeOptions extends SingleStatBaseOptions {
minValue: number;
maxValue: number;
- orientation: VizOrientation;
- valueOptions: SingleStatValueOptions;
- valueMappings: ValueMapping[];
- thresholds: Threshold[];
displayMode: 'basic' | 'lcd' | 'gradient';
}
-export const orientationOptions: SelectOptionItem[] = [
- { value: VizOrientation.Horizontal, label: 'Horizontal' },
- { value: VizOrientation.Vertical, label: 'Vertical' },
-];
-
export const displayModes: SelectOptionItem[] = [
{ value: 'gradient', label: 'Gradient' },
{ value: 'lcd', label: 'Retro LCD' },
{ value: 'basic', label: 'Basic' },
];
+export const orientationOptions: SelectOptionItem[] = [
+ { value: VizOrientation.Horizontal, label: 'Horizontal' },
+ { value: VizOrientation.Vertical, label: 'Vertical' },
+];
+
export const defaults: BarGaugeOptions = {
minValue: 0,
maxValue: 100,
- displayMode: 'basic',
+ displayMode: 'lcd',
orientation: VizOrientation.Horizontal,
valueOptions: {
unit: 'none',
diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx
index f309b38e67d..b83dc9ad440 100644
--- a/public/app/plugins/panel/gauge/GaugePanel.tsx
+++ b/public/app/plugins/panel/gauge/GaugePanel.tsx
@@ -2,37 +2,27 @@
import React, { PureComponent } from 'react';
// Services & Utils
-import { processSingleStatPanelData } from '@grafana/ui';
import { config } from 'app/core/config';
// Components
-import { Gauge, VizRepeater } from '@grafana/ui';
+import { Gauge } from '@grafana/ui';
// Types
import { GaugeOptions } from './types';
-import { PanelProps, VizOrientation, SingleStatValueInfo } from '@grafana/ui/src/types';
+import { DisplayValue, PanelProps } from '@grafana/ui';
+import { getSingleStatValues } from '../singlestat2/SingleStatPanel';
+import { ProcessedValuesRepeater } from '../singlestat2/ProcessedValuesRepeater';
-interface Props extends PanelProps {}
-
-export class GaugePanel extends PureComponent {
- renderGauge(value: SingleStatValueInfo, width, height) {
- const { replaceVariables, options } = this.props;
- const { valueOptions } = options;
-
- const prefix = replaceVariables(valueOptions.prefix);
- const suffix = replaceVariables(valueOptions.suffix);
+export class GaugePanel extends PureComponent> {
+ renderValue = (value: DisplayValue, width: number, height: number): JSX.Element => {
+ const { options } = this.props;
return (
{
theme={config.theme}
/>
);
- }
+ };
+
+ getProcessedValues = (): DisplayValue[] => {
+ return getSingleStatValues(this.props);
+ };
render() {
- const { panelData, options, height, width } = this.props;
-
- const values = processSingleStatPanelData({
- panelData: panelData,
- stat: options.valueOptions.stat,
- });
-
+ const { height, width, options, panelData } = this.props;
+ const { orientation } = options;
return (
-
- {({ vizHeight, vizWidth, value }) => this.renderGauge(value, vizWidth, vizHeight)}
-
+
);
}
}
diff --git a/public/app/plugins/panel/gauge/GaugePanelEditor.tsx b/public/app/plugins/panel/gauge/GaugePanelEditor.tsx
index f33ded6a28d..e3b21302331 100644
--- a/public/app/plugins/panel/gauge/GaugePanelEditor.tsx
+++ b/public/app/plugins/panel/gauge/GaugePanelEditor.tsx
@@ -9,9 +9,10 @@ import {
ValueMapping,
} from '@grafana/ui';
-import { SingleStatValueEditor } from 'app/plugins/panel/gauge/SingleStatValueEditor';
import { GaugeOptionsBox } from './GaugeOptionsBox';
-import { GaugeOptions, SingleStatValueOptions } from './types';
+import { GaugeOptions } from './types';
+import { SingleStatValueEditor } from '../singlestat2/SingleStatValueEditor';
+import { SingleStatValueOptions } from '../singlestat2/types';
export class GaugePanelEditor extends PureComponent> {
onThresholdsChanged = (thresholds: Threshold[]) =>
diff --git a/public/app/plugins/panel/gauge/module.tsx b/public/app/plugins/panel/gauge/module.tsx
index b8e90a27bff..340af06a080 100644
--- a/public/app/plugins/panel/gauge/module.tsx
+++ b/public/app/plugins/panel/gauge/module.tsx
@@ -3,18 +3,10 @@ import { ReactPanelPlugin } from '@grafana/ui';
import { GaugePanelEditor } from './GaugePanelEditor';
import { GaugePanel } from './GaugePanel';
import { GaugeOptions, defaults } from './types';
+import { singleStatBaseOptionsCheck } from '../singlestat2/module';
export const reactPanel = new ReactPanelPlugin(GaugePanel);
reactPanel.setEditor(GaugePanelEditor);
reactPanel.setDefaults(defaults);
-reactPanel.setPanelTypeChangedHook((options: GaugeOptions, prevPluginId?: string, prevOptions?: any) => {
- if (prevOptions && prevOptions.valueOptions) {
- options.valueOptions = prevOptions.valueOptions;
- options.thresholds = prevOptions.thresholds;
- options.maxValue = prevOptions.maxValue;
- options.minValue = prevOptions.minValue;
- }
-
- return options;
-});
+reactPanel.setPanelTypeChangedHook(singleStatBaseOptionsCheck);
diff --git a/public/app/plugins/panel/gauge/types.ts b/public/app/plugins/panel/gauge/types.ts
index 6922538c210..bab29d5d2ad 100644
--- a/public/app/plugins/panel/gauge/types.ts
+++ b/public/app/plugins/panel/gauge/types.ts
@@ -1,21 +1,11 @@
-import { Threshold, ValueMapping } from '@grafana/ui';
+import { SingleStatBaseOptions } from '../singlestat2/types';
+import { VizOrientation } from '@grafana/ui';
-export interface GaugeOptions {
- valueMappings: ValueMapping[];
+export interface GaugeOptions extends SingleStatBaseOptions {
maxValue: number;
minValue: number;
showThresholdLabels: boolean;
showThresholdMarkers: boolean;
- thresholds: Threshold[];
- valueOptions: SingleStatValueOptions;
-}
-
-export interface SingleStatValueOptions {
- unit: string;
- suffix: string;
- stat: string;
- prefix: string;
- decimals?: number | null;
}
export const defaults: GaugeOptions = {
@@ -32,4 +22,5 @@ export const defaults: GaugeOptions = {
},
valueMappings: [],
thresholds: [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 80, color: 'red' }],
+ orientation: VizOrientation.Auto,
};
diff --git a/public/app/plugins/panel/singlestat2/ProcessedValuesRepeater.tsx b/public/app/plugins/panel/singlestat2/ProcessedValuesRepeater.tsx
new file mode 100644
index 00000000000..d42c033eac2
--- /dev/null
+++ b/public/app/plugins/panel/singlestat2/ProcessedValuesRepeater.tsx
@@ -0,0 +1,48 @@
+import React, { PureComponent } from 'react';
+import { VizOrientation } from '@grafana/ui';
+import { VizRepeater } from '@grafana/ui';
+
+export interface Props {
+ width: number;
+ height: number;
+ orientation: VizOrientation;
+ source: any; // If this changes, the values will be processed
+ processFlag?: boolean; // change to force processing
+
+ getProcessedValues: () => T[];
+ renderValue: (value: T, width: number, height: number) => JSX.Element;
+}
+
+interface State {
+ values: T[];
+}
+
+/**
+ * This is essentially a cache of processed values. This checks for changes
+ * to the source and then saves the processed values in the State
+ */
+export class ProcessedValuesRepeater extends PureComponent, State> {
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ values: props.getProcessedValues(),
+ };
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ const { processFlag, source } = this.props;
+ if (processFlag !== prevProps.processFlag || source !== prevProps.source) {
+ this.setState({ values: this.props.getProcessedValues() });
+ }
+ }
+
+ render() {
+ const { orientation, height, width, renderValue } = this.props;
+ const { values } = this.state;
+ return (
+
+ {({ vizHeight, vizWidth, value }) => renderValue(value, vizWidth, vizHeight)}
+
+ );
+ }
+}
diff --git a/public/app/plugins/panel/singlestat2/README.md b/public/app/plugins/panel/singlestat2/README.md
new file mode 100644
index 00000000000..42d72825c27
--- /dev/null
+++ b/public/app/plugins/panel/singlestat2/README.md
@@ -0,0 +1,9 @@
+# Singlestat Panel - Native Plugin
+
+The Singlestat Panel is **included** with Grafana.
+
+The Singlestat Panel allows you to show the one main summary stat of a SINGLE series. It reduces the series into a single number (by looking at the max, min, average, or sum of values in the series). Singlestat also provides thresholds to color the stat or the Panel background. It can also translate the single number into a text value, and show a sparkline summary of the series.
+
+Read more about it here:
+
+[http://docs.grafana.org/reference/singlestat/](http://docs.grafana.org/reference/singlestat/)
\ No newline at end of file
diff --git a/public/app/plugins/panel/singlestat2/SingleStatEditor.tsx b/public/app/plugins/panel/singlestat2/SingleStatEditor.tsx
new file mode 100644
index 00000000000..61b8588adce
--- /dev/null
+++ b/public/app/plugins/panel/singlestat2/SingleStatEditor.tsx
@@ -0,0 +1,48 @@
+// Libraries
+import React, { PureComponent } from 'react';
+import {
+ PanelEditorProps,
+ ThresholdsEditor,
+ Threshold,
+ PanelOptionsGrid,
+ ValueMappingsEditor,
+ ValueMapping,
+} from '@grafana/ui';
+
+import { SingleStatOptions, SingleStatValueOptions } from './types';
+import { SingleStatValueEditor } from './SingleStatValueEditor';
+
+export class SingleStatEditor extends PureComponent> {
+ onThresholdsChanged = (thresholds: Threshold[]) =>
+ this.props.onOptionsChange({
+ ...this.props.options,
+ thresholds,
+ });
+
+ onValueMappingsChanged = (valueMappings: ValueMapping[]) =>
+ this.props.onOptionsChange({
+ ...this.props.options,
+ valueMappings,
+ });
+
+ onValueOptionsChanged = (valueOptions: SingleStatValueOptions) =>
+ this.props.onOptionsChange({
+ ...this.props.options,
+ valueOptions,
+ });
+
+ render() {
+ const { options } = this.props;
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+ }
+}
diff --git a/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx b/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx
new file mode 100644
index 00000000000..1c731e0a0c7
--- /dev/null
+++ b/public/app/plugins/panel/singlestat2/SingleStatPanel.tsx
@@ -0,0 +1,66 @@
+// Libraries
+import React, { PureComponent, CSSProperties } from 'react';
+
+// Types
+import { SingleStatOptions, SingleStatBaseOptions } from './types';
+
+import { processSingleStatPanelData, DisplayValue, PanelProps } from '@grafana/ui';
+import { config } from 'app/core/config';
+import { getDisplayProcessor } from '@grafana/ui';
+import { ProcessedValuesRepeater } from './ProcessedValuesRepeater';
+
+export const getSingleStatValues = (props: PanelProps): DisplayValue[] => {
+ const { panelData, replaceVariables, options } = props;
+ const { valueOptions, valueMappings } = options;
+ const processor = getDisplayProcessor({
+ unit: valueOptions.unit,
+ decimals: valueOptions.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));
+};
+
+export class SingleStatPanel extends PureComponent> {
+ renderValue = (value: DisplayValue, width: number, height: number): JSX.Element => {
+ const style: CSSProperties = {};
+ style.margin = '0 auto';
+ style.fontSize = '250%';
+ style.textAlign = 'center';
+ if (value.color) {
+ style.color = value.color;
+ }
+
+ return (
+
+
{value.text}
+
+ );
+ };
+
+ getProcessedValues = (): DisplayValue[] => {
+ return getSingleStatValues(this.props);
+ };
+
+ render() {
+ const { height, width, options, panelData } = this.props;
+ const { orientation } = options;
+ return (
+
+ );
+ }
+}
diff --git a/public/app/plugins/panel/gauge/SingleStatValueEditor.tsx b/public/app/plugins/panel/singlestat2/SingleStatValueEditor.tsx
similarity index 100%
rename from public/app/plugins/panel/gauge/SingleStatValueEditor.tsx
rename to public/app/plugins/panel/singlestat2/SingleStatValueEditor.tsx
diff --git a/public/app/plugins/panel/singlestat2/img/icn-singlestat-panel.svg b/public/app/plugins/panel/singlestat2/img/icn-singlestat-panel.svg
new file mode 100644
index 00000000000..746687d360f
--- /dev/null
+++ b/public/app/plugins/panel/singlestat2/img/icn-singlestat-panel.svg
@@ -0,0 +1,83 @@
+
+
+
+
diff --git a/public/app/plugins/panel/singlestat2/module.tsx b/public/app/plugins/panel/singlestat2/module.tsx
new file mode 100644
index 00000000000..283b32802e1
--- /dev/null
+++ b/public/app/plugins/panel/singlestat2/module.tsx
@@ -0,0 +1,29 @@
+import { ReactPanelPlugin } from '@grafana/ui';
+import { SingleStatOptions, defaults, SingleStatBaseOptions } from './types';
+import { SingleStatPanel } from './SingleStatPanel';
+import cloneDeep from 'lodash/cloneDeep';
+import { SingleStatEditor } from './SingleStatEditor';
+
+export const reactPanel = new ReactPanelPlugin(SingleStatPanel);
+
+const optionsToKeep = ['valueOptions', 'stat', 'maxValue', 'maxValue', 'thresholds', 'valueMappings'];
+
+export const singleStatBaseOptionsCheck = (
+ options: Partial,
+ prevPluginId?: string,
+ prevOptions?: any
+) => {
+ if (prevOptions) {
+ optionsToKeep.forEach(v => {
+ if (prevOptions.hasOwnProperty(v)) {
+ options[v] = cloneDeep(prevOptions.display);
+ }
+ });
+ }
+
+ return options;
+};
+
+reactPanel.setEditor(SingleStatEditor);
+reactPanel.setDefaults(defaults);
+reactPanel.setPanelTypeChangedHook(singleStatBaseOptionsCheck);
diff --git a/public/app/plugins/panel/singlestat2/plugin.json b/public/app/plugins/panel/singlestat2/plugin.json
new file mode 100644
index 00000000000..6828399ec2b
--- /dev/null
+++ b/public/app/plugins/panel/singlestat2/plugin.json
@@ -0,0 +1,20 @@
+{
+ "type": "panel",
+ "name": "Singlestat (react)",
+ "id": "singlestat2",
+ "state": "alpha",
+
+ "dataFormats": ["time_series", "table"],
+
+ "info": {
+ "description": "Singlestat Panel for Grafana",
+ "author": {
+ "name": "Grafana Project",
+ "url": "https://grafana.com"
+ },
+ "logos": {
+ "small": "img/icn-singlestat-panel.svg",
+ "large": "img/icn-singlestat-panel.svg"
+ }
+ }
+}
diff --git a/public/app/plugins/panel/singlestat2/types.ts b/public/app/plugins/panel/singlestat2/types.ts
new file mode 100644
index 00000000000..1f31783e814
--- /dev/null
+++ b/public/app/plugins/panel/singlestat2/types.ts
@@ -0,0 +1,33 @@
+import { VizOrientation, ValueMapping, Threshold } from '@grafana/ui';
+
+export interface SingleStatBaseOptions {
+ valueMappings: ValueMapping[];
+ thresholds: Threshold[];
+ valueOptions: SingleStatValueOptions;
+ orientation: VizOrientation;
+}
+
+export interface SingleStatValueOptions {
+ unit: string;
+ suffix: string;
+ stat: string;
+ prefix: string;
+ decimals?: number | null;
+}
+
+export interface SingleStatOptions extends SingleStatBaseOptions {
+ // TODO, fill in with options from angular
+}
+
+export const defaults: SingleStatOptions = {
+ valueOptions: {
+ prefix: '',
+ suffix: '',
+ decimals: null,
+ stat: 'avg',
+ unit: 'none',
+ },
+ valueMappings: [],
+ thresholds: [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 80, color: 'red' }],
+ orientation: VizOrientation.Auto,
+};
diff --git a/public/app/types/index.ts b/public/app/types/index.ts
index eefba746c61..3bf76aeb3c3 100644
--- a/public/app/types/index.ts
+++ b/public/app/types/index.ts
@@ -12,6 +12,5 @@ export * from './plugins';
export * from './organization';
export * from './appNotifications';
export * from './search';
-export * from './form';
export * from './explore';
export * from './store';
diff --git a/public/sass/components/_panel_logs.scss b/public/sass/components/_panel_logs.scss
index 367d25ada6b..22c82461e85 100644
--- a/public/sass/components/_panel_logs.scss
+++ b/public/sass/components/_panel_logs.scss
@@ -299,6 +299,8 @@ $column-horizontal-spacing: 10px;
&__value {
flex: 1;
+ text-overflow: ellipsis;
+ overflow: hidden;
}
&__count,
diff --git a/public/test/specs/helpers.ts b/public/test/specs/helpers.ts
index 58403ac7ed7..f9124773c97 100644
--- a/public/test/specs/helpers.ts
+++ b/public/test/specs/helpers.ts
@@ -3,6 +3,7 @@ import config from 'app/core/config';
import * as dateMath from 'app/core/utils/datemath';
import { angularMocks, sinon } from '../lib/common';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
+import { PanelPlugin } from 'app/types';
export function ControllerTestContext(this: any) {
const self = this;
@@ -62,7 +63,7 @@ export function ControllerTestContext(this: any) {
$rootScope.colors.push('#' + i);
}
- config.panels['test'] = { info: {} };
+ config.panels['test'] = { info: {} } as PanelPlugin;
self.ctrl = $controller(
Ctrl,
{ $scope: self.scope },
diff --git a/scripts/circle-test-cache-servers.sh b/scripts/circle-test-cache-servers.sh
new file mode 100755
index 00000000000..bacd9928362
--- /dev/null
+++ b/scripts/circle-test-cache-servers.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+function exit_if_fail {
+ command=$@
+ echo "Executing '$command'"
+ eval $command
+ rc=$?
+ if [ $rc -ne 0 ]; then
+ echo "'$command' returned $rc."
+ exit $rc
+ fi
+}
+
+echo "running redis and memcache tests"
+
+time exit_if_fail go test -tags=redis ./pkg/infra/remotecache/...
+time exit_if_fail go test -tags=memcached ./pkg/infra/remotecache/...
diff --git a/scripts/cli/utils/useSpinner.ts b/scripts/cli/utils/useSpinner.ts
index 81ed9bb6fcf..298a6516689 100644
--- a/scripts/cli/utils/useSpinner.ts
+++ b/scripts/cli/utils/useSpinner.ts
@@ -10,8 +10,7 @@ export const useSpinner = (spinnerLabel: string, fn: FnToSpin, killProcess
await fn(options);
spinner.succeed();
} catch (e) {
- spinner.fail();
- console.log(e);
+ spinner.fail(e);
if (killProcess) {
process.exit(1);
}
diff --git a/scripts/grunt/default_task.js b/scripts/grunt/default_task.js
index 8a71ea26627..a656e0c60af 100644
--- a/scripts/grunt/default_task.js
+++ b/scripts/grunt/default_task.js
@@ -34,14 +34,17 @@ module.exports = function (grunt) {
]);
grunt.registerTask('no-only-tests', function () {
- var files = grunt.file.expand('public/**/*_specs\.ts', 'public/**/*_specs\.js');
+ var files = grunt.file.expand(
+ 'public/**/*@(_specs|\.test)\.@(ts|js|tsx|jsx)',
+ 'packages/grafana-ui/**/*@(_specs|\.test)\.@(ts|js|tsx|jsx)'
+ );
files.forEach(function (spec) {
var rows = grunt.file.read(spec).split('\n');
rows.forEach(function (row) {
if (row.indexOf('.only(') > 0) {
grunt.log.errorlns(row);
- grunt.fail.warn('found only statement in test: ' + spec)
+ grunt.fail.warn('found only statement in test: ' + spec);
}
});
});
diff --git a/style_guides/backend.md b/style_guides/backend.md
new file mode 100644
index 00000000000..1c6c86efc0b
--- /dev/null
+++ b/style_guides/backend.md
@@ -0,0 +1,30 @@
+# Backend style guide
+
+Grafanas backend has been developed for a long time with a mix of code styles.
+
+This style guide is a guide for how we want to write Go code in the future. Generally, we want to follow the style guides used in Go [Code Review Comments](https://code.google.com/p/go-wiki/wiki/CodeReviewComments) and Peter Bourgon's [Go: Best Practices for Production Environments](http://peter.bourgon.org/go-in-production/#formatting-and-style)
+
+
+## Global state
+Global state makes testing and debugging software harder and its something we want to avoid when possible.
+Unfortunately, there is quite a lot of global state in Grafana. The way we want to migrate away from this
+is to use the `inject` package to wire up all dependencies either in `pkg/cmd/grafana-server/main.go` or
+self registering using `registry.RegisterService` ex https://github.com/grafana/grafana/blob/master/pkg/services/cleanup/cleanup.go#L25
+
+### the `bus`
+`bus.Dispatch` is used in many places and something we want to avoid in the future since it refers to a global instance.
+The preferred solution, in this case, is to inject the `bus` into services or take the bus instance as a parameter into functions.
+
+### settings package
+In the `setting` packages there are many global variables which Grafana sets at startup. This is also something we want to move
+away from and move as much configuration as possible to the `setting.Cfg` struct and pass it around, just like the bus.
+
+## Linting and formatting
+We enforce strict `gofmt` formating and use some linters on our codebase. You can find the current list of linters at https://github.com/grafana/grafana/blob/master/scripts/gometalinter.sh#L23
+
+We don't enforce `golint` but we encourage it and we will test so the number of linting errors does not increase over time.
+
+## Testing
+We use GoConvey for BDD/scenario based testing. Which we think is useful for testing certain chain or interactions. Ex https://github.com/grafana/grafana/blob/master/pkg/services/auth/auth_token_test.go
+
+For smaller tests its preferred to use standard library testing.
diff --git a/style_guides/frontend.md b/style_guides/frontend.md
index caef4f711ef..18069183e66 100644
--- a/style_guides/frontend.md
+++ b/style_guides/frontend.md
@@ -1,36 +1,36 @@
# Frontend Style Guide
-Generally we follow the Airbnb [React Style Guide](https://github.com/airbnb/javascript/tree/master/react).
+Generally we follow the Airbnb [React Style Guide](https://github.com/airbnb/javascript/tree/master/react).
## Table of Contents
- 1. [Basic Rules](#basic-rules)
- 1. [File & Component Organization](#Organization)
- 1. [Naming](#naming)
- 1. [Declaration](#declaration)
- 1. [Props](#props)
- 1. [Refs](#refs)
- 1. [Methods](#methods)
- 1. [Ordering](#ordering)
+1. [Basic Rules](#basic-rules)
+1. [File & Component Organization](#Organization)
+1. [Naming](#naming)
+1. [Declaration](#declaration)
+1. [Props](#props)
+1. [Refs](#refs)
+1. [Methods](#methods)
+1. [Ordering](#ordering)
## Basic rules
-* Try to keep files small and focused and break large components up into sub components.
+- Try to keep files small and focused and break large components up into sub components.
## Organization
-* Components and types that needs to be used by external plugins needs to go into @grafana/ui
-* Components should get their own folder under features/xxx/components
- * Sub components can live in that component folders, so small component do not need their own folder
- * Place test next to their component file (same dir)
- * Component sass should live in the same folder as component code
-* State logic & domain models should live in features/xxx/state
-* Containers (pages) can live in feature root features/xxx
- * up for debate?
+- Components and types that needs to be used by external plugins needs to go into @grafana/ui
+- Components should get their own folder under features/xxx/components
+ - Sub components can live in that component folders, so small component do not need their own folder
+ - Place test next to their component file (same dir)
+ - Component sass should live in the same folder as component code
+- State logic & domain models should live in features/xxx/state
+- Containers (pages) can live in feature root features/xxx
+ - up for debate?
## Props
-* Name callback props & handlers with a "on" prefix.
+- Name callback props & handlers with a "on" prefix.
```tsx
// good
@@ -56,5 +56,32 @@ render() {
}
```
+- React Component definitions
+```jsx
+// good
+export class YourClass extends PureComponent<{},{}> { ... }
+// bad
+export class YourClass extends PureComponent { ... }
+```
+
+- React Component constructor
+
+```typescript
+// good
+constructor(props:Props) {...}
+
+// bad
+constructor(props) {...}
+```
+
+- React Component defaultProps
+
+```typescript
+// good
+static defaultProps: Partial = { ... }
+
+// bad
+static defaultProps = { ... }
+```
diff --git a/style_guides/pull-request-review-checklist.md b/style_guides/pull-request-review-checklist.md
new file mode 100644
index 00000000000..2fd017386ea
--- /dev/null
+++ b/style_guides/pull-request-review-checklist.md
@@ -0,0 +1,36 @@
+# Pull Request Review Checklist
+
+## High level checks
+
+- [ ] The pull request adds value and the impact of the change is in line with [Frontend Style Guide](https://github.com/grafana/grafana/blob/master/style_guides/frontend.md).
+- [ ] The pull request works the way it says it should do.
+- [ ] The pull request does not increase the Angular code base.
+ > We are in the process of migrating to React so any increment of Angular code is generally discouraged from. (there are a few exceptions)
+- [ ] The pull request closes one issue if possible and does not fix unrelated issues within the same pull request.
+- [ ] The pull request contains necessary tests.
+
+## Low level checks
+
+- [ ] The pull request contains a title that explains the PR.
+- [ ] The pull request contains necessary link(s) to issue(s).
+- [ ] The pull request contains commits with commit messages that are small and understandable.
+- [ ] The pull request does not contain magic strings or numbers that could be replaced with an `Enum` or `const` instead.
+
+### Bug specific checks
+
+- [ ] The pull request contains only one commit if possible.
+- [ ] The pull request contains `closes: #Issue` or `fixes: #Issue` in pull request description.
+
+## Frontend specific checks
+
+- [ ] The pull request does not increase the number of `implicit any` errors.
+- [ ] The pull request does not contain uses of `any` or `{}` without comments describing why.
+- [ ] The pull request does not contain large React component that could easily be split into several smaller components.
+- [ ] The pull request does not contain back end calls directly from components, use actions and Redux instead.
+
+### Redux specific checks (skip if pull request does not contain Redux changes)
+
+- [ ] The pull request does not contain code that mutate state in reducers or thunks.
+- [ ] The pull request uses helpers `actionCreatorFactory` and `reducerFactory` instead of traditional `switch statement` reducers in Redux. ([Redux framework](https://github.com/grafana/grafana/blob/master/style_guides/redux.md))
+- [ ] The pull request uses `reducerTester` to test reducers.([Redux framework](https://github.com/grafana/grafana/blob/master/style_guides/redux.md))
+- [ ] The pull request does not contain code that access reducers state slice directly, instead the code uses state selectors to access state.
diff --git a/style_guides/redux.md b/style_guides/redux.md
new file mode 100644
index 00000000000..ff64fe400f3
--- /dev/null
+++ b/style_guides/redux.md
@@ -0,0 +1,158 @@
+# Redux framework
+
+To reduce the amount of boilerplate code used to create a strongly typed redux solution with actions, action creators, reducers and tests we've introduced a small framework around Redux.
+
+`+` Much less boilerplate code
+`-` Non Redux standard api
+
+## New core functionality
+
+### actionCreatorFactory
+
+Used to create an action creator with the following signature
+
+```typescript
+{ type: string , (payload: T): {type: string; payload: T;} }
+```
+
+where the `type` string will be ensured to be unique and `T` is the type supplied to the factory.
+
+#### Example
+
+```typescript
+export const someAction = actionCreatorFactory('SOME_ACTION').create();
+
+// later when dispatched
+someAction('this rocks!');
+```
+
+```typescript
+// best practices, always use an interface as type
+interface SomeAction {
+ data: string;
+}
+export const someAction = actionCreatorFactory('SOME_ACTION').create();
+
+// later when dispatched
+someAction({ data: 'best practices' });
+```
+
+```typescript
+// declaring an action creator with a type string that has already been defined will throw
+export const someAction = actionCreatorFactory('SOME_ACTION').create();
+export const theAction = actionCreatorFactory('SOME_ACTION').create(); // will throw
+```
+
+### noPayloadActionCreatorFactory
+
+Used when you don't need to supply a payload for your action. Will create an action creator with the following signature
+
+```typescript
+{ type: string , (): {type: string; payload: undefined;} }
+```
+
+where the `type` string will be ensured to be unique.
+
+#### Example
+
+```typescript
+export const noPayloadAction = noPayloadActionCreatorFactory('NO_PAYLOAD').create();
+
+// later when dispatched
+noPayloadAction();
+```
+
+```typescript
+// declaring an action creator with a type string that has already been defined will throw
+export const noPayloadAction = noPayloadActionCreatorFactory('NO_PAYLOAD').create();
+export const noAction = noPayloadActionCreatorFactory('NO_PAYLOAD').create(); // will throw
+```
+
+### reducerFactory
+
+Fluent API used to create a reducer. (same as implementing the standard switch statement in Redux)
+
+#### Example
+
+```typescript
+interface ExampleReducerState {
+ data: string[];
+}
+
+const intialState: ExampleReducerState = { data: [] };
+
+export const someAction = actionCreatorFactory('SOME_ACTION').create();
+export const otherAction = actionCreatorFactory('Other_ACTION').create();
+
+export const exampleReducer = reducerFactory(intialState)
+ // addMapper is the function that ties an action creator to a state change
+ .addMapper({
+ // action creator to filter out which mapper to use
+ filter: someAction,
+ // mapper function where the state change occurs
+ mapper: (state, action) => ({ ...state, data: state.data.concat(action.payload) }),
+ })
+ // a developer can just chain addMapper functions until reducer is done
+ .addMapper({
+ filter: otherAction,
+ mapper: (state, action) => ({ ...state, data: action.payload }),
+ })
+ .create(); // this will return the reducer
+```
+
+#### Typing limitations
+
+There is a challenge left with the mapper function that I can not solve with TypeScript. The signature of a mapper is
+
+```typescript
+(state: State, action: ActionOf) => State;
+```
+
+If you would to return an object that is not of the state type like the following mapper
+
+```typescript
+mapper: (state, action) => ({ nonExistingProperty: ''}),
+```
+
+Then you would receive the following compile error
+
+```shell
+[ts] Property 'data' is missing in type '{ nonExistingProperty: string; }' but required in type 'ExampleReducerState'. [2741]
+```
+
+But if you return an object that is spreading state and add a non existing property type like the following mapper
+
+```typescript
+mapper: (state, action) => ({ ...state, nonExistingProperty: ''}),
+```
+
+Then you would not receive any compile error.
+
+If you want to make sure that never happens you can just supply the State type to the mapper callback like the following mapper:
+
+```typescript
+mapper: (state, action): ExampleReducerState => ({ ...state, nonExistingProperty: 'kalle' }),
+```
+
+Then you would receive the following compile error
+
+```shell
+[ts]
+Type '{ nonExistingProperty: string; data: string[]; }' is not assignable to type 'ExampleReducerState'.
+ Object literal may only specify known properties, and 'nonExistingProperty' does not exist in type 'ExampleReducerState'. [2322]
+```
+
+## New test functionality
+
+### reducerTester
+
+Fluent API that simplifies the testing of reducers
+
+#### Example
+
+```typescript
+reducerTester()
+ .givenReducer(someReducer, initialState)
+ .whenActionIsDispatched(someAction('reducer tests'))
+ .thenStateShouldEqual({ ...initialState, data: 'reducer tests' });
+```