mirror of https://github.com/grafana/grafana
commit
6e0b873739
@ -0,0 +1,22 @@ |
||||
version: "3" |
||||
|
||||
networks: |
||||
loki: |
||||
|
||||
services: |
||||
loki: |
||||
image: grafana/loki:master |
||||
ports: |
||||
- "3100:3100" |
||||
command: -config.file=/etc/loki/local-config.yaml |
||||
networks: |
||||
- loki |
||||
|
||||
promtail: |
||||
image: grafana/promtail:master |
||||
volumes: |
||||
- /var/log:/var/log |
||||
command: |
||||
-config.file=/etc/promtail/docker-config.yaml |
||||
networks: |
||||
- loki |
@ -0,0 +1,159 @@ |
||||
+++ |
||||
title = "What's New in Grafana v6.0" |
||||
description = "Feature & improvement highlights for Grafana v6.0" |
||||
keywords = ["grafana", "new", "documentation", "6.0"] |
||||
type = "docs" |
||||
[menu.docs] |
||||
name = "Version 6.0" |
||||
identifier = "v6.0" |
||||
parent = "whatsnew" |
||||
weight = -11 |
||||
+++ |
||||
|
||||
# What's New in Grafana v6.0 |
||||
|
||||
This update to Grafana introduces a new way of exploring your data, support for log data and tons of other features. |
||||
|
||||
Grafana v6.0 is out in **Beta**, [Download Now!](https://grafana.com/grafana/download/beta) |
||||
|
||||
The main highlights are: |
||||
|
||||
- [Explore]({{< relref "#explore" >}}) - A new query focused workflow for ad-hoc data exploration and troubleshooting. |
||||
- [Grafana Loki]({{< relref "#explore-and-grafana-loki" >}}) - Integration with the new open source log aggregation system from Grafana Labs. |
||||
- [Gauge Panel]({{< relref "#gauge-panel" >}}) - A new standalone panel for gauges. |
||||
- [New Panel Editor UX]({{< relref "#new-panel-editor" >}}) improves panel editing |
||||
and enables easy switching between different visualizations. |
||||
- [Google Stackdriver Datasource]({{< relref "#google-stackdriver-datasource" >}}) is out of beta and is officially released. |
||||
- [Azure Monitor]({{< relref "#azure-monitor-datasource" >}}) plugin is ported from being an external plugin to being a core datasource |
||||
- [React Plugin]({{< relref "#react-panels-query-editors" >}}) support enables an easier way to build plugins. |
||||
- [Named Colors]({{< relref "#named-colors" >}}) in our new improved color picker. |
||||
|
||||
## Explore |
||||
|
||||
{{< docs-imagebox img="/img/docs/v60/explore_prometheus.png" max-width="800px" class="docs-image--right" caption="Screenshot of the new Explore option in the panel menu" >}} |
||||
|
||||
Grafana's dashboard UI is all about building dashboards for visualization. **Explore** strips away all the dashboard and panel options so that you can focus on the query & metric exploration. Iterate until you have a working query and then think about building a dashboard. You can also jump from a dashboard panel into **Explore** and from there do some ad-hoc query exporation with the panel queries as a starting point. |
||||
|
||||
For infrastructure monitoring and incident response, you no longer need to switch to other tools to debug what went wrong. **Explore** allows you to dig deeper into your metrics and logs to find the cause. Grafana's new logging datasource, [Loki](https://github.com/grafana/loki) is tightly integrated into Explore and allows you to correlate metrics and logs by viewing them side-by-side. |
||||
|
||||
**Explore** is a new paradigm for Grafana. It creates a new interactive debugging workflow that integrates two pillars |
||||
of observability - metrics and logs. Explore works with every datasource but for Prometheus we have customized the |
||||
query editor and the experience to provide the best possible exploration UX. |
||||
|
||||
### Explore and Prometheus |
||||
|
||||
Explore features a new [Prometheus query editor](/features/explore/#prometheus-specific-features). This new editor has improved autocomplete, metric tree selector, |
||||
integrations with the Explore table view for easy label filtering and useful query hints that can automatically apply |
||||
functions to your query. There is also integration between Prometheus and Grafana Loki (see more about Loki below) that |
||||
enabled jumping between metrics query and logs query with preserved label filters. |
||||
|
||||
### Explore splits |
||||
|
||||
Explore supports splitting the view so you can compare different queries, different datasources and metrics & logs side by side! |
||||
|
||||
{{< docs-imagebox img="/img/docs/v60/explore_split.png" max-width="800px" caption="Screenshot of the new Explore option in the panel menu" >}} |
||||
|
||||
<br /> |
||||
|
||||
### Explore and Grafana Loki |
||||
|
||||
The log exploration & visualization features in Explore are available to any data source but are currently only implemented by the new open source log |
||||
aggregation system from Grafana Lab called [Grafana Loki](https://github.com/grafana/loki). |
||||
|
||||
Loki is a horizontally-scalable, highly-available, multi-tenant log aggregation system inspired by Prometheus. It is designed to be very cost effective, as it does not index the contents of the logs, but rather a set of labels for each log stream. The logs from Loki are queried in a similar way to querying with label selectors in Prometheus. It uses labels to group log streams which can be made to match up with your Prometheus labels. |
||||
|
||||
Read more about Grafana Loki [here](https://github.com/grafana/loki) or [Grafana Labs hosted Loki](https://grafana.com/loki). |
||||
|
||||
The Explore feature allows you to query logs and features a new log panel. In the near future, we will be adding support |
||||
for other log sources to Explore and the next planned integration is Elasticsearch. |
||||
|
||||
<div class="medium-6 columns"> |
||||
<video width="800" height="500" controls> |
||||
<source src="/assets/videos/explore_loki.mp4" type="video/mp4"> |
||||
Your browser does not support the video tag. |
||||
</video> |
||||
</div> |
||||
|
||||
<br /> |
||||
|
||||
## New Panel Editor |
||||
|
||||
Grafana v6.0 has a completely redesigned UX around editing panels. You can now resize the visualization area if you want |
||||
more space for queries & options and vice versa. You can now also change visualization (panel type) from within the new |
||||
panel edit mode. No need to add a new panel to try out different visualizations! Checkout the |
||||
video below to see the new Panel Editor in action. |
||||
|
||||
<div class="medium-6 columns"> |
||||
<video width="800" height="500" controls> |
||||
<source src="/assets/videos/panel_change_viz.mp4" type="video/mp4"> |
||||
Your browser does not support the video tag. |
||||
</video> |
||||
</div> |
||||
|
||||
<br> |
||||
|
||||
### Gauge Panel |
||||
|
||||
We have created a new separate Gauge panel as we felt having this visualization be a hidden option in the Singlestat panel |
||||
was not ideal. When it supports 100% of the Singlestat Gauge features we plan to add a migration so all |
||||
singlestats that use it become Gauge panels instead. This new panel contains a new **Threshold** editor that we will |
||||
continue to refine and start using in other panels. |
||||
|
||||
{{< docs-imagebox img="/img/docs/v60/gauge_panel.png" max-width="600px" caption="Gauge Panel" >}} |
||||
|
||||
<br> |
||||
|
||||
### React Panels & Query Editors |
||||
|
||||
A major part of all the work that has gone into Grafana v6.0 has been on the migration to React. This investment |
||||
is part of the future proofing of Grafana and it's code base and ecosystem. Starting in v6.0 **Panels** and **Data |
||||
source** plugins can be written in React using our published `@grafana/ui` sdk library. More information on this |
||||
will be shared closer to or just after release. |
||||
|
||||
{{< docs-imagebox img="/img/docs/v60/react_panels.png" max-width="600px" caption="React Panel" >}} |
||||
<br /> |
||||
|
||||
### Google Stackdriver Datasource |
||||
|
||||
Built-in support for [Google Stackdriver](https://cloud.google.com/stackdriver/) is officially released in Grafana 6.0. Beta support was added in Grafana 5.3 and we have added lots of improvements since then. |
||||
|
||||
To get started read the guide: [Using Google Stackdriver in Grafana](/features/datasources/stackdriver/). |
||||
|
||||
### Azure Monitor Datasource |
||||
|
||||
One of the goals of the Grafana v6.0 release is to add support for the three major clouds. Amazon Cloudwatch has been a core datasource for years and Google Stackdriver is also now supported. We developed an external plugin for Azure Monitor last year and for this release the [plugin](https://grafana.com/plugins/grafana-azure-monitor-datasource) is being moved into Grafana to be one of the built-in datasources. For users of the external plugin, Grafana will automatically start using the built-in version. As a core datasource, the Azure Monitor datasource will get alerting support for the official 6.0 release. |
||||
|
||||
The Azure Monitor datasource integrates four Azure services with Grafana - Azure Monitor, Azure Log Analytics, Azure Application Insights and Azure Application Insights Analytics. |
||||
|
||||
### Provisioning support for alert notifiers |
||||
|
||||
Grafana now added support for provisioning alert notifiers from configuration files. Allowing operators to provision notifiers without using the UI or the API. A new field called `uid` has been introduced which is a string identifier that the administrator can set themselves. Same kind of identifier used for dashboards since v5.0. This feature makes it possible to use the same notifier configuration in multiple environments and refer to notifiers in dashboard json by a string identifier instead of the numeric id which depends on insert order and how many notifiers that exists in the instance. |
||||
|
||||
### Auth and session token improvements |
||||
|
||||
The previous session storage implementation in Grafana was causing problems in larger HA setups due to too many write requests to the database. The remember me token also have several security issues which is why we decided to rewrite auth middleware in Grafana and remove the session storage since most operations using the session storage could be rewritten to use cookies or data already made available earlier in the request. |
||||
If you are using `Auth proxy` for authentication the session storage will still be used but our goal is to remove this ASAP as well. |
||||
|
||||
This release will force all users to log in again since their previous token is not valid anymore. |
||||
|
||||
### Named Colors |
||||
|
||||
{{< docs-imagebox img="/img/docs/v60/named_colors.png" max-width="400px" class="docs-image--right" caption="Named Colors" >}} |
||||
|
||||
We have updated the color picker to show named colors and primary colors. We hope this will improve accessibility and |
||||
helps making colors more consistent across dashboards. We hope to do more in this color picker in the future, like show |
||||
colors used in the dashboard. |
||||
|
||||
Named colors also enables Grafana to adapt colors to the current theme. |
||||
|
||||
<div class="clearfix"></div> |
||||
|
||||
### Other features |
||||
|
||||
- The ElasticSearch datasource now supports [bucket script pipeline aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-bucket-script-aggregation.html). This gives the ability to do per bucket computations like the difference or ratio between two metrics. |
||||
- Support for Google Hangouts Chat alert notifications |
||||
- New built in template variables for the current time range in `$__from` and `$__to` |
||||
|
||||
## Changelog |
||||
|
||||
Checkout the [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) file for a complete list of new features, changes, and bug fixes. |
@ -1,3 +1,4 @@ |
||||
package util |
||||
|
||||
// DynMap defines a dynamic map interface.
|
||||
type DynMap map[string]interface{} |
||||
|
@ -0,0 +1,83 @@ |
||||
import { |
||||
actionCreatorFactory, |
||||
resetAllActionCreatorTypes, |
||||
noPayloadActionCreatorFactory, |
||||
} from './actionCreatorFactory'; |
||||
|
||||
interface Dummy { |
||||
n: number; |
||||
s: string; |
||||
o: { |
||||
n: number; |
||||
s: string; |
||||
b: boolean; |
||||
}; |
||||
b: boolean; |
||||
} |
||||
|
||||
const setup = (payload?: Dummy) => { |
||||
resetAllActionCreatorTypes(); |
||||
const actionCreator = actionCreatorFactory<Dummy>('dummy').create(); |
||||
const noPayloadactionCreator = noPayloadActionCreatorFactory('NoPayload').create(); |
||||
const result = actionCreator(payload); |
||||
const noPayloadResult = noPayloadactionCreator(); |
||||
|
||||
return { actionCreator, noPayloadactionCreator, result, noPayloadResult }; |
||||
}; |
||||
|
||||
describe('actionCreatorFactory', () => { |
||||
describe('when calling create', () => { |
||||
it('then it should create correct type string', () => { |
||||
const payload = { n: 1, b: true, s: 'dummy', o: { n: 1, b: true, s: 'dummy' } }; |
||||
const { actionCreator, result } = setup(payload); |
||||
|
||||
expect(actionCreator.type).toEqual('dummy'); |
||||
expect(result.type).toEqual('dummy'); |
||||
}); |
||||
|
||||
it('then it should create correct payload', () => { |
||||
const payload = { n: 1, b: true, s: 'dummy', o: { n: 1, b: true, s: 'dummy' } }; |
||||
const { result } = setup(payload); |
||||
|
||||
expect(result.payload).toEqual(payload); |
||||
}); |
||||
}); |
||||
|
||||
describe('when calling create with existing type', () => { |
||||
it('then it should throw error', () => { |
||||
const payload = { n: 1, b: true, s: 'dummy', o: { n: 1, b: true, s: 'dummy' } }; |
||||
setup(payload); |
||||
|
||||
expect(() => { |
||||
noPayloadActionCreatorFactory('DuMmY').create(); |
||||
}).toThrow(); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('noPayloadActionCreatorFactory', () => { |
||||
describe('when calling create', () => { |
||||
it('then it should create correct type string', () => { |
||||
const { noPayloadResult, noPayloadactionCreator } = setup(); |
||||
|
||||
expect(noPayloadactionCreator.type).toEqual('NoPayload'); |
||||
expect(noPayloadResult.type).toEqual('NoPayload'); |
||||
}); |
||||
|
||||
it('then it should create correct payload', () => { |
||||
const { noPayloadResult } = setup(); |
||||
|
||||
expect(noPayloadResult.payload).toBeUndefined(); |
||||
}); |
||||
}); |
||||
|
||||
describe('when calling create with existing type', () => { |
||||
it('then it should throw error', () => { |
||||
setup(); |
||||
|
||||
expect(() => { |
||||
actionCreatorFactory<Dummy>('nOpAyLoAd').create(); |
||||
}).toThrow(); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,57 @@ |
||||
import { Action } from 'redux'; |
||||
|
||||
const allActionCreators: string[] = []; |
||||
|
||||
export interface ActionOf<Payload> extends Action { |
||||
readonly type: string; |
||||
readonly payload: Payload; |
||||
} |
||||
|
||||
export interface ActionCreator<Payload> { |
||||
readonly type: string; |
||||
(payload: Payload): ActionOf<Payload>; |
||||
} |
||||
|
||||
export interface NoPayloadActionCreator { |
||||
readonly type: string; |
||||
(): ActionOf<undefined>; |
||||
} |
||||
|
||||
export interface ActionCreatorFactory<Payload> { |
||||
create: () => ActionCreator<Payload>; |
||||
} |
||||
|
||||
export interface NoPayloadActionCreatorFactory { |
||||
create: () => NoPayloadActionCreator; |
||||
} |
||||
|
||||
export const actionCreatorFactory = <Payload>(type: string): ActionCreatorFactory<Payload> => { |
||||
const create = (): ActionCreator<Payload> => { |
||||
return Object.assign((payload: Payload): ActionOf<Payload> => ({ type, payload }), { type }); |
||||
}; |
||||
|
||||
if (allActionCreators.some(t => (t && type ? t.toLocaleUpperCase() === type.toLocaleUpperCase() : false))) { |
||||
throw new Error(`There is already an actionCreator defined with the type ${type}`); |
||||
} |
||||
|
||||
allActionCreators.push(type); |
||||
|
||||
return { create }; |
||||
}; |
||||
|
||||
export const noPayloadActionCreatorFactory = (type: string): NoPayloadActionCreatorFactory => { |
||||
const create = (): NoPayloadActionCreator => { |
||||
return Object.assign((): ActionOf<undefined> => ({ type, payload: undefined }), { type }); |
||||
}; |
||||
|
||||
if (allActionCreators.some(t => (t && type ? t.toLocaleUpperCase() === type.toLocaleUpperCase() : false))) { |
||||
throw new Error(`There is already an actionCreator defined with the type ${type}`); |
||||
} |
||||
|
||||
allActionCreators.push(type); |
||||
|
||||
return { create }; |
||||
}; |
||||
|
||||
// Should only be used by tests
|
||||
export const resetAllActionCreatorTypes = () => (allActionCreators.length = 0); |
@ -0,0 +1,4 @@ |
||||
import { actionCreatorFactory } from './actionCreatorFactory'; |
||||
import { reducerFactory } from './reducerFactory'; |
||||
|
||||
export { actionCreatorFactory, reducerFactory }; |
@ -0,0 +1,97 @@ |
||||
import { reducerFactory } from './reducerFactory'; |
||||
import { actionCreatorFactory, ActionOf } from './actionCreatorFactory'; |
||||
|
||||
interface DummyReducerState { |
||||
n: number; |
||||
s: string; |
||||
b: boolean; |
||||
o: { |
||||
n: number; |
||||
s: string; |
||||
b: boolean; |
||||
}; |
||||
} |
||||
|
||||
const dummyReducerIntialState: DummyReducerState = { |
||||
n: 1, |
||||
s: 'One', |
||||
b: true, |
||||
o: { |
||||
n: 2, |
||||
s: 'two', |
||||
b: false, |
||||
}, |
||||
}; |
||||
|
||||
const dummyActionCreator = actionCreatorFactory<DummyReducerState>('dummy').create(); |
||||
|
||||
const dummyReducer = reducerFactory(dummyReducerIntialState) |
||||
.addMapper({ |
||||
filter: dummyActionCreator, |
||||
mapper: (state, action) => ({ ...state, ...action.payload }), |
||||
}) |
||||
.create(); |
||||
|
||||
describe('reducerFactory', () => { |
||||
describe('given it is created with a defined handler', () => { |
||||
describe('when reducer is called with no state', () => { |
||||
describe('and with an action that the handler can not handle', () => { |
||||
it('then the resulting state should be intial state', () => { |
||||
const result = dummyReducer(undefined as DummyReducerState, {} as ActionOf<any>); |
||||
|
||||
expect(result).toEqual(dummyReducerIntialState); |
||||
}); |
||||
}); |
||||
|
||||
describe('and with an action that the handler can handle', () => { |
||||
it('then the resulting state should correct', () => { |
||||
const payload = { n: 10, s: 'ten', b: false, o: { n: 20, s: 'twenty', b: true } }; |
||||
const result = dummyReducer(undefined as DummyReducerState, dummyActionCreator(payload)); |
||||
|
||||
expect(result).toEqual(payload); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('when reducer is called with a state', () => { |
||||
describe('and with an action that the handler can not handle', () => { |
||||
it('then the resulting state should be intial state', () => { |
||||
const result = dummyReducer(dummyReducerIntialState, {} as ActionOf<any>); |
||||
|
||||
expect(result).toEqual(dummyReducerIntialState); |
||||
}); |
||||
}); |
||||
|
||||
describe('and with an action that the handler can handle', () => { |
||||
it('then the resulting state should correct', () => { |
||||
const payload = { n: 10, s: 'ten', b: false, o: { n: 20, s: 'twenty', b: true } }; |
||||
const result = dummyReducer(dummyReducerIntialState, dummyActionCreator(payload)); |
||||
|
||||
expect(result).toEqual(payload); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('given a handler is added', () => { |
||||
describe('when a handler with the same creator is added', () => { |
||||
it('then is should throw', () => { |
||||
const faultyReducer = reducerFactory(dummyReducerIntialState).addMapper({ |
||||
filter: dummyActionCreator, |
||||
mapper: (state, action) => { |
||||
return { ...state, ...action.payload }; |
||||
}, |
||||
}); |
||||
|
||||
expect(() => { |
||||
faultyReducer.addMapper({ |
||||
filter: dummyActionCreator, |
||||
mapper: state => { |
||||
return state; |
||||
}, |
||||
}); |
||||
}).toThrow(); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,45 @@ |
||||
import { ActionOf, ActionCreator } from './actionCreatorFactory'; |
||||
import { Reducer } from 'redux'; |
||||
|
||||
export type Mapper<State, Payload> = (state: State, action: ActionOf<Payload>) => State; |
||||
|
||||
export interface MapperConfig<State, Payload> { |
||||
filter: ActionCreator<Payload>; |
||||
mapper: Mapper<State, Payload>; |
||||
} |
||||
|
||||
export interface AddMapper<State> { |
||||
addMapper: <Payload>(config: MapperConfig<State, Payload>) => CreateReducer<State>; |
||||
} |
||||
|
||||
export interface CreateReducer<State> extends AddMapper<State> { |
||||
create: () => Reducer<State, ActionOf<any>>; |
||||
} |
||||
|
||||
export const reducerFactory = <State>(initialState: State): AddMapper<State> => { |
||||
const allMappers: { [key: string]: Mapper<State, any> } = {}; |
||||
|
||||
const addMapper = <Payload>(config: MapperConfig<State, Payload>): CreateReducer<State> => { |
||||
if (allMappers[config.filter.type]) { |
||||
throw new Error(`There is already a mapper defined with the type ${config.filter.type}`); |
||||
} |
||||
|
||||
allMappers[config.filter.type] = config.mapper; |
||||
|
||||
return instance; |
||||
}; |
||||
|
||||
const create = (): Reducer<State, ActionOf<any>> => (state: State = initialState, action: ActionOf<any>): State => { |
||||
const mapper = allMappers[action.type]; |
||||
|
||||
if (mapper) { |
||||
return mapper(state, action); |
||||
} |
||||
|
||||
return state; |
||||
}; |
||||
|
||||
const instance: CreateReducer<State> = { addMapper, create }; |
||||
|
||||
return instance; |
||||
}; |
@ -0,0 +1,22 @@ |
||||
// https://github.com/facebook/react/issues/5465
|
||||
|
||||
export interface CancelablePromise<T> { |
||||
promise: Promise<T>; |
||||
cancel: () => void; |
||||
} |
||||
|
||||
export const makePromiseCancelable = <T>(promise: Promise<T>): CancelablePromise<T> => { |
||||
let hasCanceled_ = false; |
||||
|
||||
const wrappedPromise = new Promise<T>((resolve, reject) => { |
||||
promise.then(val => (hasCanceled_ ? reject({ isCanceled: true }) : resolve(val))); |
||||
promise.catch(error => (hasCanceled_ ? reject({ isCanceled: true }) : reject(error))); |
||||
}); |
||||
|
||||
return { |
||||
promise: wrappedPromise, |
||||
cancel() { |
||||
hasCanceled_ = true; |
||||
}, |
||||
}; |
||||
}; |
@ -1,118 +1,258 @@ |
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||
|
||||
exports[`ServerStats Should render table with stats 1`] = ` |
||||
<div> |
||||
<div |
||||
className="page-scrollbar-wrapper" |
||||
> |
||||
<div |
||||
className="page-header-canvas" |
||||
className="custom-scrollbars" |
||||
style={ |
||||
Object { |
||||
"height": "auto", |
||||
"maxHeight": "100%", |
||||
"minHeight": "100%", |
||||
"overflow": "hidden", |
||||
"position": "relative", |
||||
"width": "100%", |
||||
} |
||||
} |
||||
> |
||||
<div |
||||
className="page-container" |
||||
className="view" |
||||
style={ |
||||
Object { |
||||
"WebkitOverflowScrolling": "touch", |
||||
"bottom": undefined, |
||||
"left": undefined, |
||||
"marginBottom": 0, |
||||
"marginRight": 0, |
||||
"maxHeight": "calc(100% + 0px)", |
||||
"minHeight": "calc(100% + 0px)", |
||||
"overflow": "scroll", |
||||
"position": "relative", |
||||
"right": undefined, |
||||
"top": undefined, |
||||
} |
||||
} |
||||
> |
||||
<div |
||||
className="page-header" |
||||
className="page-scrollbar-content" |
||||
> |
||||
<div |
||||
className="page-header__inner" |
||||
className="page-header-canvas" |
||||
> |
||||
<span |
||||
className="page-header__logo" |
||||
> |
||||
<i |
||||
className="page-header__icon fa fa-fw fa-warning" |
||||
/> |
||||
</span> |
||||
<div |
||||
className="page-header__info-block" |
||||
className="page-container" |
||||
> |
||||
<h1 |
||||
className="page-header__title" |
||||
> |
||||
Admin |
||||
</h1> |
||||
<div |
||||
className="page-header__sub-title" |
||||
className="page-header" |
||||
> |
||||
subTitle |
||||
<div |
||||
className="page-header__inner" |
||||
> |
||||
<span |
||||
className="page-header__logo" |
||||
> |
||||
<i |
||||
className="page-header__icon fa fa-fw fa-warning" |
||||
/> |
||||
</span> |
||||
<div |
||||
className="page-header__info-block" |
||||
> |
||||
<h1 |
||||
className="page-header__title" |
||||
> |
||||
Admin |
||||
</h1> |
||||
<div |
||||
className="page-header__sub-title" |
||||
> |
||||
subTitle |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<nav> |
||||
<div |
||||
className="gf-form-select-wrapper width-20 page-header__select-nav" |
||||
> |
||||
<label |
||||
className="gf-form-select-icon icon" |
||||
htmlFor="page-header-select-nav" |
||||
/> |
||||
<select |
||||
className="gf-select-nav gf-form-input" |
||||
id="page-header-select-nav" |
||||
onChange={[Function]} |
||||
value="Admin" |
||||
> |
||||
<option |
||||
value="Admin" |
||||
> |
||||
Admin |
||||
</option> |
||||
</select> |
||||
</div> |
||||
<ul |
||||
className="gf-tabs page-header__tabs" |
||||
> |
||||
<li |
||||
className="gf-tabs-item" |
||||
> |
||||
<a |
||||
className="gf-tabs-link active" |
||||
href="Admin" |
||||
> |
||||
<i |
||||
className="icon" |
||||
/> |
||||
Admin |
||||
</a> |
||||
</li> |
||||
</ul> |
||||
</nav> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<nav> |
||||
<div |
||||
className="page-container page-body" |
||||
> |
||||
<table |
||||
className="filter-table form-inline" |
||||
> |
||||
<thead> |
||||
<tr> |
||||
<th> |
||||
Name |
||||
</th> |
||||
<th> |
||||
Value |
||||
</th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
<tr> |
||||
<td> |
||||
Total dashboards |
||||
</td> |
||||
<td> |
||||
10 |
||||
</td> |
||||
</tr> |
||||
<tr> |
||||
<td> |
||||
Total Users |
||||
</td> |
||||
<td> |
||||
1 |
||||
</td> |
||||
</tr> |
||||
</tbody> |
||||
</table> |
||||
</div> |
||||
<footer |
||||
className="footer" |
||||
> |
||||
<div |
||||
className="gf-form-select-wrapper width-20 page-header__select-nav" |
||||
className="text-center" |
||||
> |
||||
<label |
||||
className="gf-form-select-icon icon" |
||||
htmlFor="page-header-select-nav" |
||||
/> |
||||
<select |
||||
className="gf-select-nav gf-form-input" |
||||
id="page-header-select-nav" |
||||
onChange={[Function]} |
||||
value="Admin" |
||||
> |
||||
<option |
||||
value="Admin" |
||||
> |
||||
Admin |
||||
</option> |
||||
</select> |
||||
<ul> |
||||
<li> |
||||
<a |
||||
href="http://docs.grafana.org" |
||||
target="_blank" |
||||
> |
||||
<i |
||||
className="fa fa-file-code-o" |
||||
/> |
||||
Docs |
||||
</a> |
||||
</li> |
||||
<li> |
||||
<a |
||||
href="https://grafana.com/services/support" |
||||
target="_blank" |
||||
> |
||||
<i |
||||
className="fa fa-support" |
||||
/> |
||||
Support Plans |
||||
</a> |
||||
</li> |
||||
<li> |
||||
<a |
||||
href="https://community.grafana.com/" |
||||
target="_blank" |
||||
> |
||||
<i |
||||
className="fa fa-comments-o" |
||||
/> |
||||
Community |
||||
</a> |
||||
</li> |
||||
<li> |
||||
<a |
||||
href="https://grafana.com" |
||||
target="_blank" |
||||
> |
||||
Grafana |
||||
</a> |
||||
|
||||
<span> |
||||
v |
||||
v1.0 |
||||
(commit: |
||||
1 |
||||
) |
||||
</span> |
||||
</li> |
||||
</ul> |
||||
</div> |
||||
<ul |
||||
className="gf-tabs page-header__tabs" |
||||
> |
||||
<li |
||||
className="gf-tabs-item" |
||||
> |
||||
<a |
||||
className="gf-tabs-link active" |
||||
href="Admin" |
||||
> |
||||
<i |
||||
className="icon" |
||||
/> |
||||
Admin |
||||
</a> |
||||
</li> |
||||
</ul> |
||||
</nav> |
||||
</footer> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div |
||||
className="page-container page-body" |
||||
> |
||||
<table |
||||
className="filter-table form-inline" |
||||
<div |
||||
className="track-horizontal" |
||||
style={ |
||||
Object { |
||||
"display": "none", |
||||
"height": 6, |
||||
"position": "absolute", |
||||
} |
||||
} |
||||
> |
||||
<thead> |
||||
<tr> |
||||
<th> |
||||
Name |
||||
</th> |
||||
<th> |
||||
Value |
||||
</th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
<tr> |
||||
<td> |
||||
Total dashboards |
||||
</td> |
||||
<td> |
||||
10 |
||||
</td> |
||||
</tr> |
||||
<tr> |
||||
<td> |
||||
Total Users |
||||
</td> |
||||
<td> |
||||
1 |
||||
</td> |
||||
</tr> |
||||
</tbody> |
||||
</table> |
||||
<div |
||||
className="thumb-horizontal" |
||||
style={ |
||||
Object { |
||||
"display": "block", |
||||
"height": "100%", |
||||
"position": "relative", |
||||
} |
||||
} |
||||
/> |
||||
</div> |
||||
<div |
||||
className="track-vertical" |
||||
style={ |
||||
Object { |
||||
"display": "none", |
||||
"position": "absolute", |
||||
"width": 6, |
||||
} |
||||
} |
||||
> |
||||
<div |
||||
className="thumb-vertical" |
||||
style={ |
||||
Object { |
||||
"display": "block", |
||||
"position": "relative", |
||||
"width": "100%", |
||||
} |
||||
} |
||||
/> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
`; |
||||
|
@ -1,7 +1,7 @@ |
||||
import React from 'react'; |
||||
import { shallow } from 'enzyme'; |
||||
import { DashboardRow } from '../dashgrid/DashboardRow'; |
||||
import { PanelModel } from '../panel_model'; |
||||
import { DashboardRow } from './DashboardRow'; |
||||
import { PanelModel } from '../../state/PanelModel'; |
||||
|
||||
describe('DashboardRow', () => { |
||||
let wrapper, panel, dashboardMock; |
@ -0,0 +1 @@ |
||||
export { DashboardRow } from './DashboardRow'; |
@ -0,0 +1,123 @@ |
||||
// Libraries
|
||||
import React, { Component } from 'react'; |
||||
import { hot } from 'react-hot-loader'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
// Utils & Services
|
||||
import appEvents from 'app/core/app_events'; |
||||
import locationUtil from 'app/core/utils/location_util'; |
||||
import { getBackendSrv } from 'app/core/services/backend_srv'; |
||||
|
||||
// Components
|
||||
import { DashboardPanel } from '../dashgrid/DashboardPanel'; |
||||
|
||||
// Redux
|
||||
import { updateLocation } from 'app/core/actions'; |
||||
|
||||
// Types
|
||||
import { StoreState } from 'app/types'; |
||||
import { PanelModel, DashboardModel } from 'app/features/dashboard/state'; |
||||
|
||||
interface Props { |
||||
panelId: string; |
||||
urlUid?: string; |
||||
urlSlug?: string; |
||||
urlType?: string; |
||||
$scope: any; |
||||
$injector: any; |
||||
updateLocation: typeof updateLocation; |
||||
} |
||||
|
||||
interface State { |
||||
panel: PanelModel | null; |
||||
dashboard: DashboardModel | null; |
||||
notFound: boolean; |
||||
} |
||||
|
||||
export class SoloPanelPage extends Component<Props, State> { |
||||
|
||||
state: State = { |
||||
panel: null, |
||||
dashboard: null, |
||||
notFound: false, |
||||
}; |
||||
|
||||
componentDidMount() { |
||||
const { $injector, $scope, urlUid, urlType, urlSlug } = this.props; |
||||
|
||||
// handle old urls with no uid
|
||||
if (!urlUid && !(urlType === 'script' || urlType === 'snapshot')) { |
||||
this.redirectToNewUrl(); |
||||
return; |
||||
} |
||||
|
||||
const dashboardLoaderSrv = $injector.get('dashboardLoaderSrv'); |
||||
|
||||
// subscribe to event to know when dashboard controller is done with inititalization
|
||||
appEvents.on('dashboard-initialized', this.onDashoardInitialized); |
||||
|
||||
dashboardLoaderSrv.loadDashboard(urlType, urlSlug, urlUid).then(result => { |
||||
result.meta.soloMode = true; |
||||
$scope.initDashboard(result, $scope); |
||||
}); |
||||
} |
||||
|
||||
redirectToNewUrl() { |
||||
getBackendSrv().getDashboardBySlug(this.props.urlSlug).then(res => { |
||||
if (res) { |
||||
const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/')); |
||||
this.props.updateLocation(url); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
onDashoardInitialized = () => { |
||||
const { $scope, panelId } = this.props; |
||||
|
||||
const dashboard: DashboardModel = $scope.dashboard; |
||||
const panel = dashboard.getPanelById(parseInt(panelId, 10)); |
||||
|
||||
if (!panel) { |
||||
this.setState({ notFound: true }); |
||||
return; |
||||
} |
||||
|
||||
this.setState({ dashboard, panel }); |
||||
}; |
||||
|
||||
render() { |
||||
const { panelId } = this.props; |
||||
const { notFound, panel, dashboard } = this.state; |
||||
|
||||
if (notFound) { |
||||
return ( |
||||
<div className="alert alert-error"> |
||||
Panel with id { panelId } not found |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
if (!panel) { |
||||
return <div>Loading & initializing dashboard</div>; |
||||
} |
||||
|
||||
return ( |
||||
<div className="panel-solo"> |
||||
<DashboardPanel dashboard={dashboard} panel={panel} isEditing={false} isFullscreen={false} /> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
const mapStateToProps = (state: StoreState) => ({ |
||||
urlUid: state.location.routeParams.uid, |
||||
urlSlug: state.location.routeParams.slug, |
||||
urlType: state.location.routeParams.type, |
||||
panelId: state.location.query.panelId |
||||
}); |
||||
|
||||
const mapDispatchToProps = { |
||||
updateLocation |
||||
}; |
||||
|
||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(SoloPanelPage)); |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue