Merge 'master' into 751-stack_graph, resolve conflicts
commit
6c67dcf402
|
@ -5,11 +5,16 @@
|
|||
2. [#907](https://github.com/influxdata/chronograf/pull/907): Fix react-router warning
|
||||
|
||||
### Features
|
||||
1. [#873](https://github.com/influxdata/chronograf/pull/873): Add [TLS](https://github.com/influxdata/chronograf/blob/master/docs/tls.md) support
|
||||
1. [#873](https://github.com/influxdata/chronograf/pull/873): Add [TLS](https://github.com/influxdata/chronograf/blob/master/docs/tls.md) support
|
||||
2. [#885](https://github.com/influxdata/chronograf/issues/885): Add presentation mode to dashboard page
|
||||
3. [#891](https://github.com/influxdata/chronograf/issues/891): Make dashboard visualizations draggable
|
||||
4. [#892](https://github.com/influxdata/chronograf/issues/891): Make dashboard visualizations resizable
|
||||
5. [#893](https://github.com/influxdata/chronograf/issues/893): Persist dashboard visualization position
|
||||
|
||||
### UI Improvements
|
||||
1. [#905](https://github.com/influxdata/chronograf/pull/905): Make scroll bar thumb element bigger
|
||||
2. [#920](https://github.com/influxdata/chronograf/pull/920): Display stacked and step plot graphs
|
||||
2. [#917](https://github.com/influxdata/chronograf/pull/917): Simplify side navigation
|
||||
3. [#920](https://github.com/influxdata/chronograf/pull/920): Display stacked and step plot graphs
|
||||
|
||||
## v1.2.0-beta3 [2017-02-15]
|
||||
|
||||
|
|
|
@ -10,6 +10,13 @@ import (
|
|||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultWidth is used if not specified
|
||||
DefaultWidth = 4
|
||||
// DefaultHeight is used if not specified
|
||||
DefaultHeight = 4
|
||||
)
|
||||
|
||||
type dashboardLinks struct {
|
||||
Self string `json:"self"` // Self link mapping to this resource
|
||||
}
|
||||
|
@ -25,6 +32,7 @@ type getDashboardsResponse struct {
|
|||
|
||||
func newDashboardResponse(d chronograf.Dashboard) dashboardResponse {
|
||||
base := "/chronograf/v1/dashboards"
|
||||
DashboardDefaults(&d)
|
||||
return dashboardResponse{
|
||||
Dashboard: d,
|
||||
Links: dashboardLinks{
|
||||
|
@ -80,7 +88,7 @@ func (s *Service) NewDashboard(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := ValidDashboardRequest(dashboard); err != nil {
|
||||
if err := ValidDashboardRequest(&dashboard); err != nil {
|
||||
invalidData(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
|
@ -119,8 +127,8 @@ func (s *Service) RemoveDashboard(w http.ResponseWriter, r *http.Request) {
|
|||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// UpdateDashboard replaces a dashboard
|
||||
func (s *Service) UpdateDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
// ReplaceDashboard completely replaces a dashboard
|
||||
func (s *Service) ReplaceDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
idParam, err := strconv.Atoi(httprouter.GetParamFromContext(ctx, "id"))
|
||||
if err != nil {
|
||||
|
@ -142,7 +150,7 @@ func (s *Service) UpdateDashboard(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
req.ID = id
|
||||
|
||||
if err := ValidDashboardRequest(req); err != nil {
|
||||
if err := ValidDashboardRequest(&req); err != nil {
|
||||
invalidData(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
|
@ -157,17 +165,85 @@ func (s *Service) UpdateDashboard(w http.ResponseWriter, r *http.Request) {
|
|||
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||
}
|
||||
|
||||
// UpdateDashboard completely updates either the dashboard name or the cells
|
||||
func (s *Service) UpdateDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
idParam, err := strconv.Atoi(httprouter.GetParamFromContext(ctx, "id"))
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("Could not parse dashboard ID: %s", err)
|
||||
Error(w, http.StatusInternalServerError, msg, s.Logger)
|
||||
}
|
||||
id := chronograf.DashboardID(idParam)
|
||||
|
||||
orig, err := s.DashboardsStore.Get(ctx, id)
|
||||
if err != nil {
|
||||
Error(w, http.StatusNotFound, fmt.Sprintf("ID %d not found", id), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req chronograf.Dashboard
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
invalidJSON(w, s.Logger)
|
||||
return
|
||||
}
|
||||
req.ID = id
|
||||
|
||||
if req.Name != "" {
|
||||
orig.Name = req.Name
|
||||
} else if len(req.Cells) > 0 {
|
||||
if err := ValidDashboardRequest(&req); err != nil {
|
||||
invalidData(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
orig.Cells = req.Cells
|
||||
} else {
|
||||
invalidData(w, fmt.Errorf("Update must include either name or cells"), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.DashboardsStore.Update(ctx, orig); err != nil {
|
||||
msg := fmt.Sprintf("Error updating dashboard ID %d: %v", id, err)
|
||||
Error(w, http.StatusInternalServerError, msg, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
res := newDashboardResponse(orig)
|
||||
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||
}
|
||||
|
||||
// ValidDashboardRequest verifies that the dashboard cells have a query
|
||||
func ValidDashboardRequest(d chronograf.Dashboard) error {
|
||||
func ValidDashboardRequest(d *chronograf.Dashboard) error {
|
||||
if len(d.Cells) == 0 {
|
||||
return fmt.Errorf("cells are required")
|
||||
}
|
||||
|
||||
for _, c := range d.Cells {
|
||||
if (len(c.Queries) == 0) {
|
||||
for i, c := range d.Cells {
|
||||
if len(c.Queries) == 0 {
|
||||
return fmt.Errorf("query required")
|
||||
}
|
||||
CorrectWidthHeight(&c)
|
||||
d.Cells[i] = c
|
||||
}
|
||||
|
||||
DashboardDefaults(d)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DashboardDefaults updates the dashboard with the default values
|
||||
// if none are specified
|
||||
func DashboardDefaults(d *chronograf.Dashboard) {
|
||||
for i, c := range d.Cells {
|
||||
CorrectWidthHeight(&c)
|
||||
d.Cells[i] = c
|
||||
}
|
||||
}
|
||||
|
||||
// CorrectWidthHeight changes the cell to have at least the
|
||||
// minimum width and height
|
||||
func CorrectWidthHeight(c *chronograf.DashboardCell) {
|
||||
if c.W < 1 {
|
||||
c.W = DefaultWidth
|
||||
}
|
||||
if c.H < 1 {
|
||||
c.H = DefaultHeight
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,299 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
func TestCorrectWidthHeight(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
cell chronograf.DashboardCell
|
||||
want chronograf.DashboardCell
|
||||
}{
|
||||
{
|
||||
name: "updates width",
|
||||
cell: chronograf.DashboardCell{
|
||||
W: 0,
|
||||
H: 4,
|
||||
},
|
||||
want: chronograf.DashboardCell{
|
||||
W: 4,
|
||||
H: 4,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "updates height",
|
||||
cell: chronograf.DashboardCell{
|
||||
W: 4,
|
||||
H: 0,
|
||||
},
|
||||
want: chronograf.DashboardCell{
|
||||
W: 4,
|
||||
H: 4,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "updates both",
|
||||
cell: chronograf.DashboardCell{
|
||||
W: 0,
|
||||
H: 0,
|
||||
},
|
||||
want: chronograf.DashboardCell{
|
||||
W: 4,
|
||||
H: 4,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "updates neither",
|
||||
cell: chronograf.DashboardCell{
|
||||
W: 4,
|
||||
H: 4,
|
||||
},
|
||||
want: chronograf.DashboardCell{
|
||||
W: 4,
|
||||
H: 4,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if CorrectWidthHeight(&tt.cell); !reflect.DeepEqual(tt.cell, tt.want) {
|
||||
t.Errorf("%q. CorrectWidthHeight() = %v, want %v", tt.name, tt.cell, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDashboardDefaults(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
d chronograf.Dashboard
|
||||
want chronograf.Dashboard
|
||||
}{
|
||||
{
|
||||
name: "Updates all cell widths/heights",
|
||||
d: chronograf.Dashboard{
|
||||
Cells: []chronograf.DashboardCell{
|
||||
{
|
||||
W: 0,
|
||||
H: 0,
|
||||
},
|
||||
{
|
||||
W: 2,
|
||||
H: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: chronograf.Dashboard{
|
||||
Cells: []chronograf.DashboardCell{
|
||||
{
|
||||
W: 4,
|
||||
H: 4,
|
||||
},
|
||||
{
|
||||
W: 2,
|
||||
H: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Updates no cell",
|
||||
d: chronograf.Dashboard{
|
||||
Cells: []chronograf.DashboardCell{
|
||||
{
|
||||
W: 4,
|
||||
H: 4,
|
||||
}, {
|
||||
W: 2,
|
||||
H: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: chronograf.Dashboard{
|
||||
Cells: []chronograf.DashboardCell{
|
||||
{
|
||||
W: 4,
|
||||
H: 4,
|
||||
},
|
||||
{
|
||||
W: 2,
|
||||
H: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if DashboardDefaults(&tt.d); !reflect.DeepEqual(tt.d, tt.want) {
|
||||
t.Errorf("%q. DashboardDefaults() = %v, want %v", tt.name, tt.d, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidDashboardRequest(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
d chronograf.Dashboard
|
||||
want chronograf.Dashboard
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Updates all cell widths/heights",
|
||||
d: chronograf.Dashboard{
|
||||
Cells: []chronograf.DashboardCell{
|
||||
{
|
||||
W: 0,
|
||||
H: 0,
|
||||
Queries: []chronograf.Query{
|
||||
{
|
||||
Command: "SELECT donors from hill_valley_preservation_society where time > 1985-10-25T08:00:00",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
W: 2,
|
||||
H: 2,
|
||||
Queries: []chronograf.Query{
|
||||
{
|
||||
Command: "SELECT winning_horses from grays_sports_alamanc where time > 1955-11-1T00:00:00",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: chronograf.Dashboard{
|
||||
Cells: []chronograf.DashboardCell{
|
||||
{
|
||||
W: 4,
|
||||
H: 4,
|
||||
Queries: []chronograf.Query{
|
||||
{
|
||||
Command: "SELECT donors from hill_valley_preservation_society where time > 1985-10-25T08:00:00",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
W: 2,
|
||||
H: 2,
|
||||
Queries: []chronograf.Query{
|
||||
{
|
||||
Command: "SELECT winning_horses from grays_sports_alamanc where time > 1955-11-1T00:00:00",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "No queries",
|
||||
d: chronograf.Dashboard{
|
||||
Cells: []chronograf.DashboardCell{
|
||||
{
|
||||
W: 2,
|
||||
H: 2,
|
||||
Queries: []chronograf.Query{},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: chronograf.Dashboard{
|
||||
Cells: []chronograf.DashboardCell{
|
||||
{
|
||||
W: 2,
|
||||
H: 2,
|
||||
Queries: []chronograf.Query{},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Empty Cells",
|
||||
d: chronograf.Dashboard{
|
||||
Cells: []chronograf.DashboardCell{},
|
||||
},
|
||||
want: chronograf.Dashboard{
|
||||
Cells: []chronograf.DashboardCell{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
err := ValidDashboardRequest(&tt.d)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("%q. ValidDashboardRequest() error = %v, wantErr %v", tt.name, err, tt.wantErr)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(tt.d, tt.want) {
|
||||
t.Errorf("%q. ValidDashboardRequest() = %v, want %v", tt.name, tt.d, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_newDashboardResponse(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
d chronograf.Dashboard
|
||||
want dashboardResponse
|
||||
}{
|
||||
{
|
||||
name: "Updates all cell widths/heights",
|
||||
d: chronograf.Dashboard{
|
||||
Cells: []chronograf.DashboardCell{
|
||||
{
|
||||
W: 0,
|
||||
H: 0,
|
||||
Queries: []chronograf.Query{
|
||||
{
|
||||
Command: "SELECT donors from hill_valley_preservation_society where time > 1985-10-25T08:00:00",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
W: 0,
|
||||
H: 0,
|
||||
Queries: []chronograf.Query{
|
||||
{
|
||||
Command: "SELECT winning_horses from grays_sports_alamanc where time > 1955-11-1T00:00:00",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: dashboardResponse{
|
||||
Dashboard: chronograf.Dashboard{
|
||||
Cells: []chronograf.DashboardCell{
|
||||
{
|
||||
W: 4,
|
||||
H: 4,
|
||||
Queries: []chronograf.Query{
|
||||
{
|
||||
Command: "SELECT donors from hill_valley_preservation_society where time > 1985-10-25T08:00:00",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
W: 4,
|
||||
H: 4,
|
||||
Queries: []chronograf.Query{
|
||||
{
|
||||
Command: "SELECT winning_horses from grays_sports_alamanc where time > 1955-11-1T00:00:00",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Links: dashboardLinks{
|
||||
Self: "/chronograf/v1/dashboards/0",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := newDashboardResponse(tt.d); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("%q. newDashboardResponse() = %v, want %v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -117,7 +117,8 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
|
|||
|
||||
router.GET("/chronograf/v1/dashboards/:id", service.DashboardID)
|
||||
router.DELETE("/chronograf/v1/dashboards/:id", service.RemoveDashboard)
|
||||
router.PUT("/chronograf/v1/dashboards/:id", service.UpdateDashboard)
|
||||
router.PUT("/chronograf/v1/dashboards/:id", service.ReplaceDashboard)
|
||||
router.PATCH("/chronograf/v1/dashboards/:id", service.UpdateDashboard)
|
||||
|
||||
/* Authentication */
|
||||
if opts.UseAuth {
|
||||
|
|
|
@ -1519,6 +1519,51 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"tags": [
|
||||
"layouts"
|
||||
],
|
||||
"summary": "Update dashboard information.",
|
||||
"description": "Update either the dashboard name or the dashboard cells",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"type": "integer",
|
||||
"description": "ID of a dashboard",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "config",
|
||||
"in": "body",
|
||||
"description": "dashboard configuration update parameters. Must be either name or cells",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/Dashboard"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Dashboard has been updated and the new dashboard is returned.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/Dashboard"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Happens when trying to access a non-existent dashboard.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/Error"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "A processing or an unexpected error.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -2248,12 +2293,16 @@
|
|||
"w": {
|
||||
"description": "Width of Cell in the Dashboard",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
"format": "int32",
|
||||
"minimum": 1,
|
||||
"default": 4
|
||||
},
|
||||
"h": {
|
||||
"description": "Height of Cell in the Dashboard",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
"format": "int32",
|
||||
"minimum": 1,
|
||||
"default": 4
|
||||
},
|
||||
"queries": {
|
||||
"description": "Time-series data queries for Cell.",
|
||||
|
|
|
@ -182,7 +182,7 @@
|
|||
'quote-props': [2, 'as-needed', {keywords: true, numbers: false }],
|
||||
'require-jsdoc': 0,
|
||||
'semi-spacing': [2, {before: false, after: true}],
|
||||
'semi': [2, 'always'],
|
||||
'semi': [0, 'always'],
|
||||
'sort-vars': 0,
|
||||
'keyword-spacing': 'error',
|
||||
'space-before-blocks': [2, 'always'],
|
||||
|
@ -194,7 +194,7 @@
|
|||
'wrap-regex': 0,
|
||||
'arrow-body-style': 0,
|
||||
'arrow-spacing': [2, {before: true, after: true}],
|
||||
'no-confusing-arrow': 2,
|
||||
'no-confusing-arrow': 0,
|
||||
'no-class-assign': 2,
|
||||
'no-const-assign': 2,
|
||||
'no-dupe-class-members': 2,
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import reducer from 'src/dashboards/reducers/ui'
|
||||
import timeRanges from 'hson!src/shared/data/timeRanges.hson';
|
||||
|
||||
import {
|
||||
loadDashboards,
|
||||
setDashboard,
|
||||
setTimeRange,
|
||||
setEditMode,
|
||||
} from 'src/dashboards/actions'
|
||||
|
||||
const noopAction = () => {
|
||||
return {type: 'NOOP'}
|
||||
}
|
||||
|
||||
let state = undefined
|
||||
const timeRange = timeRanges[1];
|
||||
const d1 = {id: 1, cells: [], name: "d1"}
|
||||
const d2 = {id: 2, cells: [], name: "d2"}
|
||||
const dashboards = [d1, d2]
|
||||
|
||||
describe('DataExplorer.Reducers.UI', () => {
|
||||
it('can load the dashboards', () => {
|
||||
const actual = reducer(state, loadDashboards(dashboards, d1.id))
|
||||
const expected = {
|
||||
dashboards,
|
||||
dashboard: d1,
|
||||
}
|
||||
|
||||
expect(actual.dashboards).to.deep.equal(expected.dashboards)
|
||||
expect(actual.dashboard).to.deep.equal(expected.dashboard)
|
||||
})
|
||||
|
||||
it('can set a dashboard', () => {
|
||||
const loadedState = reducer(state, loadDashboards(dashboards, d1.id))
|
||||
const actual = reducer(loadedState, setDashboard(d2.id))
|
||||
|
||||
expect(actual.dashboard).to.deep.equal(d2)
|
||||
})
|
||||
|
||||
it('can set the time range', () => {
|
||||
const expected = {upper: null, lower: 'now() - 1h'}
|
||||
const actual = reducer(state, setTimeRange(expected))
|
||||
|
||||
expect(actual.timeRange).to.deep.equal(expected)
|
||||
})
|
||||
|
||||
it('can set edit mode', () => {
|
||||
const isEditMode = true
|
||||
const actual = reducer(state, setEditMode(isEditMode))
|
||||
expect(actual.isEditMode).to.equal(isEditMode)
|
||||
})
|
||||
})
|
|
@ -8,22 +8,29 @@ import {
|
|||
dismissAllNotifications as dismissAllNotificationsAction,
|
||||
} from 'src/shared/actions/notifications';
|
||||
|
||||
const {
|
||||
node,
|
||||
shape,
|
||||
string,
|
||||
func,
|
||||
} = PropTypes
|
||||
|
||||
const App = React.createClass({
|
||||
propTypes: {
|
||||
children: PropTypes.node.isRequired,
|
||||
location: PropTypes.shape({
|
||||
pathname: PropTypes.string,
|
||||
children: node.isRequired,
|
||||
location: shape({
|
||||
pathname: string,
|
||||
}),
|
||||
params: PropTypes.shape({
|
||||
sourceID: PropTypes.string.isRequired,
|
||||
params: shape({
|
||||
sourceID: string.isRequired,
|
||||
}).isRequired,
|
||||
publishNotification: PropTypes.func.isRequired,
|
||||
dismissNotification: PropTypes.func.isRequired,
|
||||
dismissAllNotifications: PropTypes.func.isRequired,
|
||||
notifications: PropTypes.shape({
|
||||
success: PropTypes.string,
|
||||
error: PropTypes.string,
|
||||
warning: PropTypes.string,
|
||||
publishNotification: func.isRequired,
|
||||
dismissNotification: func.isRequired,
|
||||
dismissAllNotifications: func.isRequired,
|
||||
notifications: shape({
|
||||
success: string,
|
||||
error: string,
|
||||
warning: string,
|
||||
}),
|
||||
},
|
||||
|
||||
|
@ -46,11 +53,15 @@ const App = React.createClass({
|
|||
},
|
||||
|
||||
render() {
|
||||
const {sourceID} = this.props.params;
|
||||
const {params: {sourceID}} = this.props;
|
||||
|
||||
return (
|
||||
<div className="chronograf-root">
|
||||
<SideNavContainer sourceID={sourceID} addFlashMessage={this.handleNotification} currentLocation={this.props.location.pathname} />
|
||||
<SideNavContainer
|
||||
sourceID={sourceID}
|
||||
addFlashMessage={this.handleNotification}
|
||||
currentLocation={this.props.location.pathname}
|
||||
/>
|
||||
{this.renderNotifications()}
|
||||
{this.props.children && React.cloneElement(this.props.children, {
|
||||
addFlashMessage: this.handleNotification,
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
import {
|
||||
getDashboards as getDashboardsAJAX,
|
||||
updateDashboard as updateDashboardAJAX,
|
||||
} from 'src/dashboards/apis'
|
||||
|
||||
export function loadDashboards(dashboards, dashboardID) {
|
||||
return {
|
||||
type: 'LOAD_DASHBOARDS',
|
||||
payload: {
|
||||
dashboards,
|
||||
dashboardID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function setDashboard(dashboardID) {
|
||||
return {
|
||||
type: 'SET_DASHBOARD',
|
||||
payload: {
|
||||
dashboardID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function setTimeRange(timeRange) {
|
||||
return {
|
||||
type: 'SET_DASHBOARD_TIME_RANGE',
|
||||
payload: {
|
||||
timeRange,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function setEditMode(isEditMode) {
|
||||
return {
|
||||
type: 'SET_EDIT_MODE',
|
||||
payload: {
|
||||
isEditMode,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function getDashboards(dashboardID) {
|
||||
return (dispatch) => {
|
||||
getDashboardsAJAX().then(({data: {dashboards}}) => {
|
||||
dispatch(loadDashboards(dashboards, dashboardID))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function putDashboard(dashboard) {
|
||||
return (dispatch) => {
|
||||
updateDashboardAJAX(dashboard).then(({data}) => {
|
||||
dispatch(updateDashboard(data))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function updateDashboard(dashboard) {
|
||||
return {
|
||||
type: 'UPDATE_DASHBOARD',
|
||||
payload: {
|
||||
dashboard,
|
||||
},
|
||||
}
|
||||
}
|
|
@ -6,3 +6,11 @@ export function getDashboards() {
|
|||
url: `/chronograf/v1/dashboards`,
|
||||
});
|
||||
}
|
||||
|
||||
export function updateDashboard(dashboard) {
|
||||
return AJAX({
|
||||
method: 'PUT',
|
||||
url: dashboard.links.self,
|
||||
data: dashboard,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import LayoutRenderer from 'shared/components/LayoutRenderer'
|
||||
import Visualizations from 'src/dashboards/components/VisualizationSelector'
|
||||
|
||||
const Dashboard = ({
|
||||
dashboard,
|
||||
isEditMode,
|
||||
inPresentationMode,
|
||||
onPositionChange,
|
||||
source,
|
||||
timeRange,
|
||||
}) => {
|
||||
if (dashboard.id === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classnames({'page-contents': true, 'presentation-mode': inPresentationMode})}>
|
||||
<div className={classnames('container-fluid full-width dashboard', {'dashboard-edit': isEditMode})}>
|
||||
{isEditMode ? <Visualizations/> : null}
|
||||
{Dashboard.renderDashboard(dashboard, timeRange, source, onPositionChange)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Dashboard.renderDashboard = (dashboard, timeRange, source, onPositionChange) => {
|
||||
const autoRefreshMs = 15000
|
||||
const cells = dashboard.cells.map((cell, i) => {
|
||||
i = `${i}`
|
||||
const dashboardCell = {...cell, i}
|
||||
dashboardCell.queries.forEach((q) => {
|
||||
q.text = q.query;
|
||||
q.database = source.telegraf;
|
||||
});
|
||||
return dashboardCell;
|
||||
})
|
||||
|
||||
return (
|
||||
<LayoutRenderer
|
||||
timeRange={timeRange}
|
||||
cells={cells}
|
||||
autoRefreshMs={autoRefreshMs}
|
||||
source={source.links.proxy}
|
||||
onPositionChange={onPositionChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const {
|
||||
bool,
|
||||
func,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
|
||||
Dashboard.propTypes = {
|
||||
dashboard: shape({}).isRequired,
|
||||
isEditMode: bool,
|
||||
inPresentationMode: bool,
|
||||
onPositionChange: func,
|
||||
source: shape({
|
||||
links: shape({
|
||||
proxy: string,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
timeRange: shape({}).isRequired,
|
||||
}
|
||||
|
||||
export default Dashboard
|
|
@ -0,0 +1,77 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import ReactTooltip from 'react-tooltip'
|
||||
import {Link} from 'react-router';
|
||||
|
||||
import TimeRangeDropdown from 'shared/components/TimeRangeDropdown'
|
||||
|
||||
const DashboardHeader = ({
|
||||
children,
|
||||
buttonText,
|
||||
dashboard,
|
||||
headerText,
|
||||
timeRange,
|
||||
isHidden,
|
||||
handleChooseTimeRange,
|
||||
handleClickPresentationButton,
|
||||
sourceID,
|
||||
}) => isHidden ? null : (
|
||||
<div className="page-header full-width">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
{buttonText &&
|
||||
<div className="dropdown page-header-dropdown">
|
||||
<button className="dropdown-toggle" type="button" data-toggle="dropdown">
|
||||
<span className="button-text">{buttonText}</span>
|
||||
<span className="caret"></span>
|
||||
</button>
|
||||
<ul className="dropdown-menu" aria-labelledby="dropdownMenu1">
|
||||
{children}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
{headerText &&
|
||||
<h1>Kubernetes Dashboard</h1>
|
||||
}
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
{sourceID ?
|
||||
<Link className="btn btn-info btn-sm" to={`/sources/${sourceID}/dashboards/${dashboard && dashboard.id}/edit`} >
|
||||
<span className="icon pencil" />
|
||||
Edit
|
||||
</Link> : null
|
||||
}
|
||||
<div className="btn btn-info btn-sm" data-for="graph-tips-tooltip" data-tip="<p><code>Click + Drag</code> Zoom in (X or Y)</p><p><code>Shift + Click</code> Pan Graph Window</p><p><code>Double Click</code> Reset Graph Window</p>">
|
||||
<span className="icon heart"></span>
|
||||
Graph Tips
|
||||
</div>
|
||||
<ReactTooltip id="graph-tips-tooltip" effect="solid" html={true} offset={{top: 2}} place="bottom" class="influx-tooltip place-bottom" />
|
||||
<TimeRangeDropdown onChooseTimeRange={handleChooseTimeRange} selected={timeRange.inputValue} />
|
||||
<div className="btn btn-info btn-sm" onClick={handleClickPresentationButton}>
|
||||
<span className="icon keynote" style={{margin: 0}}></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const {
|
||||
shape,
|
||||
array,
|
||||
string,
|
||||
func,
|
||||
bool,
|
||||
} = PropTypes
|
||||
|
||||
DashboardHeader.propTypes = {
|
||||
sourceID: string,
|
||||
children: array,
|
||||
buttonText: string,
|
||||
dashboard: shape({}),
|
||||
headerText: string,
|
||||
timeRange: shape({}).isRequired,
|
||||
isHidden: bool.isRequired,
|
||||
handleChooseTimeRange: func.isRequired,
|
||||
handleClickPresentationButton: func.isRequired,
|
||||
}
|
||||
|
||||
export default DashboardHeader
|
|
@ -0,0 +1,36 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const DashboardEditHeader = ({
|
||||
dashboard,
|
||||
onSave,
|
||||
}) => (
|
||||
<div className="page-header full-width">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<input
|
||||
className="chronograf-header__editing"
|
||||
autoFocus={true}
|
||||
defaultValue={dashboard && dashboard.name}
|
||||
placeholder="Dashboard name"
|
||||
/>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<div className="btn btn-sm btn-success" onClick={onSave}>
|
||||
Save
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const {
|
||||
shape,
|
||||
func,
|
||||
} = PropTypes
|
||||
|
||||
DashboardEditHeader.propTypes = {
|
||||
dashboard: shape({}),
|
||||
onSave: func.isRequired,
|
||||
}
|
||||
|
||||
export default DashboardEditHeader
|
|
@ -0,0 +1,24 @@
|
|||
import React from 'react'
|
||||
|
||||
const VisualizationSelector = () => (
|
||||
<div className="" style={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
background: '#676978',
|
||||
padding: '10px',
|
||||
borderRadius: '3px',
|
||||
marginBottom: '10px',
|
||||
}}>
|
||||
<div className="">
|
||||
VISUALIZATIONS
|
||||
<div className="btn btn-info" style={{margin: "0 5px 0 5px"}}>
|
||||
Line Graph
|
||||
</div>
|
||||
<div className="btn btn-info" style={{margin: "0 5px 0 5px"}}>
|
||||
SingleStat
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default VisualizationSelector
|
|
@ -0,0 +1,12 @@
|
|||
export const EMPTY_DASHBOARD = {
|
||||
id: 0,
|
||||
name: '',
|
||||
cells: [
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
queries: [],
|
||||
name: 'Loading...',
|
||||
},
|
||||
],
|
||||
}
|
|
@ -1,130 +1,169 @@
|
|||
import React, {PropTypes} from 'react';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import {Link} from 'react-router';
|
||||
import _ from 'lodash';
|
||||
import React, {PropTypes} from 'react'
|
||||
import {Link} from 'react-router'
|
||||
import {connect} from 'react-redux'
|
||||
import {bindActionCreators} from 'redux'
|
||||
|
||||
import LayoutRenderer from 'shared/components/LayoutRenderer';
|
||||
import TimeRangeDropdown from '../../shared/components/TimeRangeDropdown';
|
||||
import timeRanges from 'hson!../../shared/data/timeRanges.hson';
|
||||
import Header from 'src/dashboards/components/DashboardHeader'
|
||||
import EditHeader from 'src/dashboards/components/DashboardHeaderEdit'
|
||||
import Dashboard from 'src/dashboards/components/Dashboard'
|
||||
import timeRanges from 'hson!../../shared/data/timeRanges.hson'
|
||||
|
||||
import {getDashboards} from '../apis';
|
||||
import {getSource} from 'shared/apis';
|
||||
import * as dashboardActionCreators from 'src/dashboards/actions'
|
||||
|
||||
import {presentationButtonDispatcher} from 'shared/dispatchers'
|
||||
|
||||
const {
|
||||
arrayOf,
|
||||
bool,
|
||||
func,
|
||||
number,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
|
||||
const DashboardPage = React.createClass({
|
||||
propTypes: {
|
||||
params: PropTypes.shape({
|
||||
sourceID: PropTypes.string.isRequired,
|
||||
dashboardID: PropTypes.string.isRequired,
|
||||
source: PropTypes.shape({
|
||||
links: PropTypes.shape({
|
||||
proxy: PropTypes.string,
|
||||
self: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
params: shape({
|
||||
sourceID: string.isRequired,
|
||||
dashboardID: string.isRequired,
|
||||
}).isRequired,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
const fifteenMinutesIndex = 1;
|
||||
|
||||
return {
|
||||
dashboards: [],
|
||||
timeRange: timeRanges[fifteenMinutesIndex],
|
||||
};
|
||||
location: shape({
|
||||
pathname: string.isRequired,
|
||||
}).isRequired,
|
||||
dashboardActions: shape({
|
||||
putDashboard: func.isRequired,
|
||||
getDashboards: func.isRequired,
|
||||
setDashboard: func.isRequired,
|
||||
setTimeRange: func.isRequired,
|
||||
setEditMode: func.isRequired,
|
||||
}).isRequired,
|
||||
dashboards: arrayOf(shape({
|
||||
id: number.isRequired,
|
||||
cells: arrayOf(shape({})).isRequired,
|
||||
})).isRequired,
|
||||
dashboard: shape({
|
||||
id: number.isRequired,
|
||||
cells: arrayOf(shape({})).isRequired,
|
||||
}).isRequired,
|
||||
timeRange: shape({}).isRequired,
|
||||
inPresentationMode: bool.isRequired,
|
||||
isEditMode: bool.isRequired,
|
||||
handleClickPresentationButton: func,
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
getDashboards().then((resp) => {
|
||||
getSource(this.props.params.sourceID).then(({data: source}) => {
|
||||
this.setState({
|
||||
dashboards: resp.data.dashboards,
|
||||
source,
|
||||
});
|
||||
});
|
||||
});
|
||||
const {
|
||||
params: {dashboardID},
|
||||
dashboardActions: {getDashboards},
|
||||
} = this.props;
|
||||
|
||||
getDashboards(dashboardID)
|
||||
},
|
||||
|
||||
currentDashboard(dashboards, dashboardID) {
|
||||
return _.find(dashboards, (d) => d.id.toString() === dashboardID);
|
||||
},
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {location: {pathname}} = this.props
|
||||
const {
|
||||
location: {pathname: nextPathname},
|
||||
params: {dashboardID: nextID},
|
||||
dashboardActions: {setDashboard, setEditMode},
|
||||
} = nextProps
|
||||
|
||||
renderDashboard(dashboard) {
|
||||
const autoRefreshMs = 15000;
|
||||
const {timeRange} = this.state;
|
||||
const {source} = this.state;
|
||||
if (nextPathname.pathname === pathname) {
|
||||
return
|
||||
}
|
||||
|
||||
const cellWidth = 4;
|
||||
const cellHeight = 4;
|
||||
|
||||
const cells = dashboard.cells.map((cell, i) => {
|
||||
const dashboardCell = Object.assign(cell, {
|
||||
w: cellWidth,
|
||||
h: cellHeight,
|
||||
queries: cell.queries,
|
||||
i: i.toString(),
|
||||
});
|
||||
|
||||
dashboardCell.queries.forEach((q) => {
|
||||
q.text = q.query;
|
||||
q.database = source.telegraf;
|
||||
});
|
||||
return dashboardCell;
|
||||
});
|
||||
|
||||
return (
|
||||
<LayoutRenderer
|
||||
timeRange={timeRange}
|
||||
cells={cells}
|
||||
autoRefreshMs={autoRefreshMs}
|
||||
source={source.links.proxy}
|
||||
/>
|
||||
);
|
||||
setDashboard(nextID)
|
||||
setEditMode(nextPathname.includes('/edit'))
|
||||
},
|
||||
|
||||
handleChooseTimeRange({lower}) {
|
||||
const timeRange = timeRanges.find((range) => range.queryValue === lower);
|
||||
this.setState({timeRange});
|
||||
this.props.dashboardActions.setTimeRange(timeRange)
|
||||
},
|
||||
|
||||
handleUpdatePosition(cells) {
|
||||
this.props.dashboardActions.putDashboard({...this.props.dashboard, cells})
|
||||
},
|
||||
|
||||
render() {
|
||||
const {dashboards, timeRange} = this.state;
|
||||
const dashboard = this.currentDashboard(dashboards, this.props.params.dashboardID);
|
||||
const {
|
||||
dashboards,
|
||||
dashboard,
|
||||
params: {sourceID},
|
||||
inPresentationMode,
|
||||
isEditMode,
|
||||
handleClickPresentationButton,
|
||||
source,
|
||||
timeRange,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<div className="page-header full-width">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<div className="dropdown page-header-dropdown">
|
||||
<button className="dropdown-toggle" type="button" data-toggle="dropdown">
|
||||
<span className="button-text">{dashboard ? dashboard.name : ''}</span>
|
||||
<span className="caret"></span>
|
||||
</button>
|
||||
<ul className="dropdown-menu" aria-labelledby="dropdownMenu1">
|
||||
{(dashboards).map((d, i) => {
|
||||
return (
|
||||
<li key={i}>
|
||||
<Link to={`/sources/${this.props.params.sourceID}/dashboards/${d.id}`} className="role-option">
|
||||
{d.name}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<div className="btn btn-info btn-sm" data-for="graph-tips-tooltip" data-tip="<p><code>Click + Drag</code> Zoom in (X or Y)</p><p><code>Shift + Click</code> Pan Graph Window</p><p><code>Double Click</code> Reset Graph Window</p>">
|
||||
<span className="icon heart"></span>
|
||||
Graph Tips
|
||||
</div>
|
||||
<ReactTooltip id="graph-tips-tooltip" effect="solid" html={true} offset={{top: 2}} place="bottom" class="influx-tooltip place-bottom" />
|
||||
<TimeRangeDropdown onChooseTimeRange={this.handleChooseTimeRange} selected={timeRange.inputValue} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="page-contents">
|
||||
<div className="container-fluid full-width">
|
||||
{ dashboard ? this.renderDashboard(dashboard) : '' }
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
isEditMode ?
|
||||
<EditHeader dashboard={dashboard} onSave={() => {}} /> :
|
||||
<Header
|
||||
buttonText={dashboard ? dashboard.name : ''}
|
||||
timeRange={timeRange}
|
||||
handleChooseTimeRange={this.handleChooseTimeRange}
|
||||
isHidden={inPresentationMode}
|
||||
handleClickPresentationButton={handleClickPresentationButton}
|
||||
dashboard={dashboard}
|
||||
sourceID={sourceID}
|
||||
>
|
||||
{(dashboards).map((d, i) => {
|
||||
return (
|
||||
<li key={i}>
|
||||
<Link to={`/sources/${sourceID}/dashboards/${d.id}`} className="role-option">
|
||||
{d.name}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</Header>
|
||||
}
|
||||
<Dashboard
|
||||
dashboard={dashboard}
|
||||
isEditMode={isEditMode}
|
||||
inPresentationMode={inPresentationMode}
|
||||
source={source}
|
||||
timeRange={timeRange}
|
||||
onPositionChange={this.handleUpdatePosition}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default DashboardPage;
|
||||
const mapStateToProps = (state) => {
|
||||
const {
|
||||
appUI,
|
||||
dashboardUI: {
|
||||
dashboards,
|
||||
dashboard,
|
||||
timeRange,
|
||||
isEditMode,
|
||||
},
|
||||
} = state
|
||||
|
||||
return {
|
||||
inPresentationMode: appUI.presentationMode,
|
||||
dashboards,
|
||||
dashboard,
|
||||
timeRange,
|
||||
isEditMode,
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
handleClickPresentationButton: presentationButtonDispatcher(dispatch),
|
||||
dashboardActions: bindActionCreators(dashboardActionCreators, dispatch),
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DashboardPage);
|
||||
|
|
|
@ -34,13 +34,14 @@ const DashboardsPage = React.createClass({
|
|||
},
|
||||
|
||||
render() {
|
||||
const dashboardLink = `/sources/${this.props.source.id}`;
|
||||
let tableHeader;
|
||||
if (this.state.waiting) {
|
||||
tableHeader = "Loading Dashboards...";
|
||||
} else if (this.state.dashboards.length === 0) {
|
||||
tableHeader = "No Dashboards";
|
||||
tableHeader = "1 Dashboard";
|
||||
} else {
|
||||
tableHeader = `${this.state.dashboards.length} Dashboards`;
|
||||
tableHeader = `${this.state.dashboards.length + 1} Dashboards`;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -75,7 +76,7 @@ const DashboardsPage = React.createClass({
|
|||
return (
|
||||
<tr key={dashboard.id}>
|
||||
<td className="monotype">
|
||||
<Link to={`/sources/${this.props.source.id}/dashboards/${dashboard.id}`}>
|
||||
<Link to={`${dashboardLink}/dashboards/${dashboard.id}`}>
|
||||
{dashboard.name}
|
||||
</Link>
|
||||
</td>
|
||||
|
@ -83,6 +84,13 @@ const DashboardsPage = React.createClass({
|
|||
);
|
||||
})
|
||||
}
|
||||
<tr>
|
||||
<td className="monotype">
|
||||
<Link to={`${dashboardLink}/kubernetes`}>
|
||||
{'Kubernetes'}
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import _ from 'lodash';
|
||||
import {EMPTY_DASHBOARD} from 'src/dashboards/constants'
|
||||
import timeRanges from 'hson!../../shared/data/timeRanges.hson';
|
||||
|
||||
const initialState = {
|
||||
dashboards: [],
|
||||
dashboard: EMPTY_DASHBOARD,
|
||||
timeRange: timeRanges[1],
|
||||
isEditMode: false,
|
||||
};
|
||||
|
||||
export default function ui(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case 'LOAD_DASHBOARDS': {
|
||||
const {dashboards, dashboardID} = action.payload;
|
||||
const newState = {
|
||||
dashboards,
|
||||
dashboard: _.find(dashboards, (d) => d.id === +dashboardID),
|
||||
};
|
||||
|
||||
return {...state, ...newState};
|
||||
}
|
||||
|
||||
case 'SET_DASHBOARD': {
|
||||
const {dashboardID} = action.payload
|
||||
const newState = {
|
||||
dashboard: _.find(state.dashboards, (d) => d.id === +dashboardID),
|
||||
};
|
||||
|
||||
return {...state, ...newState}
|
||||
}
|
||||
|
||||
case 'SET_DASHBOARD_TIME_RANGE': {
|
||||
const {timeRange} = action.payload
|
||||
|
||||
return {...state, timeRange};
|
||||
}
|
||||
|
||||
case 'SET_EDIT_MODE': {
|
||||
const {isEditMode} = action.payload
|
||||
return {...state, isEditMode}
|
||||
}
|
||||
|
||||
case 'UPDATE_DASHBOARD': {
|
||||
const {dashboard} = action.payload
|
||||
const newState = {
|
||||
dashboard,
|
||||
dashboards: state.dashboards.map((d) => d.id === dashboard.id ? dashboard : d),
|
||||
}
|
||||
|
||||
return {...state, ...newState}
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
|
@ -73,7 +73,7 @@ const Visualization = React.createClass({
|
|||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames("", {"graph-container": isGraphInView, "table-container": !isGraphInView})}>
|
||||
<div className={classNames({"graph-container": isGraphInView, "table-container": !isGraphInView})}>
|
||||
{isGraphInView ? (
|
||||
<RefreshingLineGraph
|
||||
queries={queries}
|
||||
|
|
|
@ -1,31 +1,42 @@
|
|||
import React, {PropTypes} from 'react';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import {Link} from 'react-router';
|
||||
import _ from 'lodash';
|
||||
import React, {PropTypes} from 'react'
|
||||
import {Link} from 'react-router'
|
||||
import {connect} from 'react-redux'
|
||||
import _ from 'lodash'
|
||||
import classnames from 'classnames';
|
||||
|
||||
import LayoutRenderer from 'shared/components/LayoutRenderer';
|
||||
import TimeRangeDropdown from '../../shared/components/TimeRangeDropdown';
|
||||
import DashboardHeader from 'src/dashboards/components/DashboardHeader';
|
||||
import timeRanges from 'hson!../../shared/data/timeRanges.hson';
|
||||
import {getMappings, getAppsForHosts, getMeasurementsForHost, getAllHosts} from 'src/hosts/apis';
|
||||
import {fetchLayouts} from 'shared/apis';
|
||||
import {presentationButtonDispatcher} from 'shared/dispatchers'
|
||||
|
||||
const {
|
||||
shape,
|
||||
string,
|
||||
bool,
|
||||
func,
|
||||
} = PropTypes
|
||||
|
||||
export const HostPage = React.createClass({
|
||||
propTypes: {
|
||||
source: PropTypes.shape({
|
||||
links: PropTypes.shape({
|
||||
proxy: PropTypes.string.isRequired,
|
||||
source: shape({
|
||||
links: shape({
|
||||
proxy: string.isRequired,
|
||||
}).isRequired,
|
||||
telegraf: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
telegraf: string.isRequired,
|
||||
id: string.isRequired,
|
||||
}),
|
||||
params: PropTypes.shape({
|
||||
hostID: PropTypes.string.isRequired,
|
||||
params: shape({
|
||||
hostID: string.isRequired,
|
||||
}).isRequired,
|
||||
location: PropTypes.shape({
|
||||
query: PropTypes.shape({
|
||||
app: PropTypes.string,
|
||||
location: shape({
|
||||
query: shape({
|
||||
app: string,
|
||||
}),
|
||||
}),
|
||||
inPresentationMode: bool,
|
||||
handleClickPresentationButton: func,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
|
@ -134,45 +145,34 @@ export const HostPage = React.createClass({
|
|||
},
|
||||
|
||||
render() {
|
||||
const hostID = this.props.params.hostID;
|
||||
const {layouts, timeRange, hosts} = this.state;
|
||||
const appParam = this.props.location.query.app ? `?app=${this.props.location.query.app}` : '';
|
||||
const {params: {hostID}, location: {query: {app}}, source: {id}, inPresentationMode, handleClickPresentationButton} = this.props
|
||||
const {layouts, timeRange, hosts} = this.state
|
||||
const appParam = app ? `?app=${app}` : ''
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<div className="page-header full-width">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<div className="dropdown page-header-dropdown">
|
||||
<button className="dropdown-toggle" type="button" data-toggle="dropdown">
|
||||
<span className="button-text">{hostID}</span>
|
||||
<span className="caret"></span>
|
||||
</button>
|
||||
<ul className="dropdown-menu" aria-labelledby="dropdownMenu1">
|
||||
{Object.keys(hosts).map((host, i) => {
|
||||
return (
|
||||
<li key={i}>
|
||||
<Link to={`/sources/${this.props.source.id}/hosts/${host + appParam}`} className="role-option">
|
||||
{host}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<div className="btn btn-info btn-sm" data-for="graph-tips-tooltip" data-tip="<p><code>Click + Drag</code> Zoom in (X or Y)</p><p><code>Shift + Click</code> Pan Graph Window</p><p><code>Double Click</code> Reset Graph Window</p>">
|
||||
<span className="icon heart"></span>
|
||||
Graph Tips
|
||||
</div>
|
||||
<ReactTooltip id="graph-tips-tooltip" effect="solid" html={true} offset={{top: 2}} place="bottom" class="influx-tooltip place-bottom" />
|
||||
<TimeRangeDropdown onChooseTimeRange={this.handleChooseTimeRange} selected={timeRange.inputValue} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="page-contents">
|
||||
<div className="container-fluid full-width">
|
||||
<DashboardHeader
|
||||
buttonText={hostID}
|
||||
timeRange={timeRange}
|
||||
isHidden={inPresentationMode}
|
||||
handleChooseTimeRange={this.handleChooseTimeRange}
|
||||
handleClickPresentationButton={handleClickPresentationButton}
|
||||
>
|
||||
{Object.keys(hosts).map((host, i) => {
|
||||
return (
|
||||
<li key={i}>
|
||||
<Link to={`/sources/${id}/hosts/${host + appParam}`} className="role-option">
|
||||
{host}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</DashboardHeader>
|
||||
<div className={classnames({
|
||||
'page-contents': true,
|
||||
'presentation-mode': inPresentationMode,
|
||||
})}>
|
||||
<div className="container-fluid full-width dashboard">
|
||||
{ (layouts.length > 0) ? this.renderLayouts(layouts) : '' }
|
||||
</div>
|
||||
</div>
|
||||
|
@ -181,4 +181,12 @@ export const HostPage = React.createClass({
|
|||
},
|
||||
});
|
||||
|
||||
export default HostPage;
|
||||
const mapStateToProps = (state) => ({
|
||||
inPresentationMode: state.appUI.presentationMode,
|
||||
})
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
handleClickPresentationButton: presentationButtonDispatcher(dispatch),
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(HostPage)
|
||||
|
|
|
@ -18,6 +18,7 @@ import NotFound from 'src/shared/components/NotFound';
|
|||
import configureStore from 'src/store/configureStore';
|
||||
import {getMe, getSources} from 'shared/apis';
|
||||
import {receiveMe} from 'shared/actions/me';
|
||||
import {disablePresentationMode} from 'shared/actions/ui';
|
||||
import {loadLocalStorage} from './localStorage';
|
||||
|
||||
import 'src/style/chronograf.scss';
|
||||
|
@ -38,6 +39,12 @@ if (basepath) {
|
|||
});
|
||||
}
|
||||
|
||||
window.addEventListener('keyup', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
store.dispatch(disablePresentationMode())
|
||||
}
|
||||
})
|
||||
|
||||
const Root = React.createClass({
|
||||
getInitialState() {
|
||||
return {
|
||||
|
@ -116,6 +123,7 @@ const Root = React.createClass({
|
|||
<Route path="alerts" component={AlertsApp} />
|
||||
<Route path="dashboards" component={DashboardsPage} />
|
||||
<Route path="dashboards/:dashboardID" component={DashboardPage} />
|
||||
<Route path="dashboards/:dashboardID/edit" component={DashboardPage} />
|
||||
<Route path="alert-rules" component={KapacitorRulesPage} />
|
||||
<Route path="alert-rules/:ruleID" component={KapacitorRulePage} />
|
||||
<Route path="alert-rules/new" component={KapacitorRulePage} />
|
||||
|
|
|
@ -1,18 +1,29 @@
|
|||
import React, {PropTypes} from 'react';
|
||||
import classnames from 'classnames'
|
||||
|
||||
import LayoutRenderer from 'shared/components/LayoutRenderer';
|
||||
import TimeRangeDropdown from '../../shared/components/TimeRangeDropdown';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import DashboardHeader from 'src/dashboards/components/DashboardHeader';
|
||||
import timeRanges from 'hson!../../shared/data/timeRanges.hson';
|
||||
|
||||
export const KubernetesPage = React.createClass({
|
||||
const {
|
||||
shape,
|
||||
string,
|
||||
arrayOf,
|
||||
bool,
|
||||
func,
|
||||
} = PropTypes
|
||||
|
||||
export const KubernetesDashboard = React.createClass({
|
||||
propTypes: {
|
||||
source: PropTypes.shape({
|
||||
links: PropTypes.shape({
|
||||
proxy: PropTypes.string.isRequired,
|
||||
source: shape({
|
||||
links: shape({
|
||||
proxy: string.isRequired,
|
||||
}).isRequired,
|
||||
telegraf: PropTypes.string.isRequired,
|
||||
telegraf: string.isRequired,
|
||||
}),
|
||||
layouts: PropTypes.arrayOf(PropTypes.shape().isRequired).isRequired,
|
||||
layouts: arrayOf(shape().isRequired).isRequired,
|
||||
inPresentationMode: bool.isRequired,
|
||||
handleClickPresentationButton: func,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
|
@ -57,7 +68,7 @@ export const KubernetesPage = React.createClass({
|
|||
},
|
||||
|
||||
render() {
|
||||
const {layouts} = this.props;
|
||||
const {layouts, inPresentationMode, handleClickPresentationButton} = this.props;
|
||||
const {timeRange} = this.state;
|
||||
const emptyState = (
|
||||
<div className="generic-empty-state">
|
||||
|
@ -68,23 +79,18 @@ export const KubernetesPage = React.createClass({
|
|||
|
||||
return (
|
||||
<div className="page">
|
||||
<div className="page-header full-width">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1>Kubernetes Dashboard</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<div className="btn btn-info btn-sm" data-for="graph-tips-tooltip" data-tip="<p><code>Click + Drag</code> Zoom in (X or Y)</p><p><code>Shift + Click</code> Pan Graph Window</p><p><code>Double Click</code> Reset Graph Window</p>">
|
||||
<span className="icon heart"></span>
|
||||
Graph Tips
|
||||
</div>
|
||||
<ReactTooltip id="graph-tips-tooltip" effect="solid" html={true} offset={{top: 2}} place="bottom" class="influx-tooltip place-bottom" />
|
||||
<TimeRangeDropdown onChooseTimeRange={this.handleChooseTimeRange} selected={timeRange.inputValue} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="page-contents">
|
||||
<div className="container-fluid full-width">
|
||||
<DashboardHeader
|
||||
headerText="Kubernetes Dashboard"
|
||||
timeRange={timeRange}
|
||||
handleChooseTimeRange={this.handleChooseTimeRange}
|
||||
isHidden={inPresentationMode}
|
||||
handleClickPresentationButton={handleClickPresentationButton}
|
||||
/>
|
||||
<div className={classnames({
|
||||
'page-contents': true,
|
||||
'presentation-mode': inPresentationMode,
|
||||
})}>
|
||||
<div className="container-fluid full-width dashboard">
|
||||
{layouts.length ? this.renderLayouts(layouts) : emptyState}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -92,4 +98,5 @@ export const KubernetesPage = React.createClass({
|
|||
);
|
||||
},
|
||||
});
|
||||
export default KubernetesPage;
|
||||
|
||||
export default KubernetesDashboard;
|
||||
|
|
|
@ -1,14 +1,26 @@
|
|||
import React, {PropTypes} from 'react';
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
import {fetchLayouts} from 'shared/apis';
|
||||
import KubernetesDashboard from 'src/kubernetes/components/KubernetesDashboard';
|
||||
import {presentationButtonDispatcher} from 'shared/dispatchers'
|
||||
|
||||
const {
|
||||
shape,
|
||||
string,
|
||||
bool,
|
||||
func,
|
||||
} = PropTypes
|
||||
|
||||
export const KubernetesPage = React.createClass({
|
||||
propTypes: {
|
||||
source: PropTypes.shape({
|
||||
links: PropTypes.shape({
|
||||
proxy: PropTypes.string.isRequired,
|
||||
source: shape({
|
||||
links: shape({
|
||||
proxy: string.isRequired,
|
||||
}).isRequired,
|
||||
}),
|
||||
inPresentationMode: bool.isRequired,
|
||||
handleClickPresentationButton: func,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
|
@ -25,10 +37,26 @@ export const KubernetesPage = React.createClass({
|
|||
},
|
||||
|
||||
render() {
|
||||
const {layouts} = this.state
|
||||
const {source, inPresentationMode, handleClickPresentationButton} = this.props
|
||||
|
||||
return (
|
||||
<KubernetesDashboard layouts={this.state.layouts} source={this.props.source} />
|
||||
<KubernetesDashboard
|
||||
layouts={layouts}
|
||||
source={source}
|
||||
inPresentationMode={inPresentationMode}
|
||||
handleClickPresentationButton={handleClickPresentationButton}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default KubernetesPage;
|
||||
const mapStateToProps = (state) => ({
|
||||
inPresentationMode: state.appUI.presentationMode,
|
||||
})
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
handleClickPresentationButton: presentationButtonDispatcher(dispatch),
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(KubernetesPage);
|
||||
|
|
|
@ -17,6 +17,12 @@ export function dismissNotification(type) {
|
|||
};
|
||||
}
|
||||
|
||||
export function delayDismissNotification(type, wait) {
|
||||
return (dispatch) => {
|
||||
setTimeout(() => dispatch(dismissNotification(type)), wait)
|
||||
}
|
||||
}
|
||||
|
||||
export function dismissAllNotifications() {
|
||||
return {
|
||||
type: 'ALL_NOTIFICATIONS_DISMISSED',
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import {PRESENTATION_MODE_ANIMATION_DELAY} from '../constants'
|
||||
|
||||
export function enablePresentationMode() {
|
||||
return {
|
||||
type: 'ENABLE_PRESENTATION_MODE',
|
||||
}
|
||||
}
|
||||
|
||||
export function disablePresentationMode() {
|
||||
return {
|
||||
type: 'DISABLE_PRESENTATION_MODE',
|
||||
}
|
||||
}
|
||||
|
||||
export function delayEnablePresentationMode() {
|
||||
return (dispatch) => {
|
||||
setTimeout(() => dispatch(enablePresentationMode()), PRESENTATION_MODE_ANIMATION_DELAY)
|
||||
}
|
||||
}
|
|
@ -108,6 +108,7 @@ export default React.createClass({
|
|||
const legendWidth = legendRect.width;
|
||||
const legendMaxLeft = graphWidth - (legendWidth / 2);
|
||||
const trueGraphX = (e.pageX - graphRect.left);
|
||||
const legendTop = graphRect.height + 0
|
||||
let legendLeft = trueGraphX;
|
||||
// Enforcing max & min legend offsets
|
||||
if (trueGraphX < (legendWidth / 2)) {
|
||||
|
@ -117,6 +118,7 @@ export default React.createClass({
|
|||
}
|
||||
|
||||
legendContainerNode.style.left = `${legendLeft}px`;
|
||||
legendContainerNode.style.top = `${legendTop}px`;
|
||||
setMarker(points);
|
||||
},
|
||||
unhighlightCallback() {
|
||||
|
|
|
@ -4,50 +4,52 @@ import LineGraph from 'shared/components/LineGraph';
|
|||
import SingleStat from 'shared/components/SingleStat';
|
||||
import ReactGridLayout, {WidthProvider} from 'react-grid-layout';
|
||||
const GridLayout = WidthProvider(ReactGridLayout);
|
||||
import _ from 'lodash';
|
||||
|
||||
const RefreshingLineGraph = AutoRefresh(LineGraph);
|
||||
const RefreshingSingleStat = AutoRefresh(SingleStat);
|
||||
|
||||
const {
|
||||
arrayOf,
|
||||
func,
|
||||
number,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes;
|
||||
|
||||
export const LayoutRenderer = React.createClass({
|
||||
propTypes: {
|
||||
timeRange: PropTypes.shape({
|
||||
defaultGroupBy: PropTypes.string.isRequired,
|
||||
queryValue: PropTypes.string.isRequired,
|
||||
timeRange: shape({
|
||||
defaultGroupBy: string.isRequired,
|
||||
queryValue: string.isRequired,
|
||||
}).isRequired,
|
||||
cells: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
queries: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
label: PropTypes.string,
|
||||
range: PropTypes.shape({
|
||||
upper: PropTypes.number,
|
||||
lower: PropTypes.number,
|
||||
cells: arrayOf(
|
||||
shape({
|
||||
queries: arrayOf(
|
||||
shape({
|
||||
label: string,
|
||||
range: shape({
|
||||
upper: number,
|
||||
lower: number,
|
||||
}),
|
||||
rp: PropTypes.string,
|
||||
text: PropTypes.string.isRequired,
|
||||
database: PropTypes.string.isRequired,
|
||||
groupbys: PropTypes.arrayOf(PropTypes.string),
|
||||
wheres: PropTypes.arrayOf(PropTypes.string),
|
||||
rp: string,
|
||||
text: string.isRequired,
|
||||
database: string.isRequired,
|
||||
groupbys: arrayOf(string),
|
||||
wheres: arrayOf(string),
|
||||
}).isRequired
|
||||
).isRequired,
|
||||
x: PropTypes.number.isRequired,
|
||||
y: PropTypes.number.isRequired,
|
||||
w: PropTypes.number.isRequired,
|
||||
h: PropTypes.number.isRequired,
|
||||
i: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
x: number.isRequired,
|
||||
y: number.isRequired,
|
||||
w: number.isRequired,
|
||||
h: number.isRequired,
|
||||
i: string.isRequired,
|
||||
name: string.isRequired,
|
||||
}).isRequired
|
||||
),
|
||||
autoRefreshMs: PropTypes.number.isRequired,
|
||||
host: PropTypes.string,
|
||||
source: PropTypes.string,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return ({
|
||||
layout: _.without(this.props.cells, ['queries']),
|
||||
});
|
||||
autoRefreshMs: number.isRequired,
|
||||
host: string,
|
||||
source: string,
|
||||
onPositionChange: func,
|
||||
},
|
||||
|
||||
buildQuery(q) {
|
||||
|
@ -96,8 +98,8 @@ export const LayoutRenderer = React.createClass({
|
|||
if (cell.type === 'single-stat') {
|
||||
return (
|
||||
<div key={cell.i}>
|
||||
<h2 className="hosts-graph-heading">{cell.name}</h2>
|
||||
<div className="hosts-graph graph-container">
|
||||
<h2 className="dash-graph--heading">{cell.name || `Graph`}</h2>
|
||||
<div className="dash-graph--container">
|
||||
<RefreshingSingleStat queries={[qs[0]]} autoRefresh={autoRefreshMs} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -111,8 +113,8 @@ export const LayoutRenderer = React.createClass({
|
|||
|
||||
return (
|
||||
<div key={cell.i}>
|
||||
<h2 className="hosts-graph-heading">{cell.name}</h2>
|
||||
<div className="hosts-graph graph-container">
|
||||
<h2 className="dash-graph--heading">{cell.name || `Graph`}</h2>
|
||||
<div className="dash-graph--container">
|
||||
<RefreshingLineGraph
|
||||
queries={qs}
|
||||
autoRefresh={autoRefreshMs}
|
||||
|
@ -125,14 +127,52 @@ export const LayoutRenderer = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
handleLayoutChange(layout) {
|
||||
this.triggerWindowResize()
|
||||
|
||||
if (!this.props.onPositionChange) {
|
||||
return
|
||||
}
|
||||
|
||||
const newCells = this.props.cells.map((cell) => {
|
||||
const l = layout.find((ly) => ly.i === cell.i)
|
||||
const newLayout = {x: l.x, y: l.y, h: l.h, w: l.w}
|
||||
return {...cell, ...newLayout}
|
||||
})
|
||||
|
||||
this.props.onPositionChange(newCells)
|
||||
},
|
||||
|
||||
render() {
|
||||
const layoutMargin = 4;
|
||||
const layoutMargin = 4
|
||||
const isDashboard = !!this.props.onPositionChange
|
||||
|
||||
return (
|
||||
<GridLayout layout={this.state.layout} isDraggable={false} isResizable={false} cols={12} rowHeight={83.5} margin={[layoutMargin, layoutMargin]} containerPadding={[0, 0]} useCSSTransforms={false} >
|
||||
<GridLayout
|
||||
layout={this.props.cells}
|
||||
cols={12}
|
||||
rowHeight={83.5}
|
||||
margin={[layoutMargin, layoutMargin]}
|
||||
containerPadding={[0, 0]}
|
||||
useCSSTransforms={false}
|
||||
onResize={this.triggerWindowResize}
|
||||
onLayoutChange={this.handleLayoutChange}
|
||||
draggableHandle={'.dash-graph--heading'}
|
||||
isDraggable={isDashboard}
|
||||
isResizable={isDashboard}
|
||||
>
|
||||
{this.generateVisualizations()}
|
||||
</GridLayout>
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
triggerWindowResize() {
|
||||
// Hack to get dygraphs to fit properly during and after resize (dispatchEvent is a global method on window).
|
||||
const evt = document.createEvent('CustomEvent'); // MUST be 'CustomEvent'
|
||||
evt.initCustomEvent('resize', false, false, null);
|
||||
dispatchEvent(evt);
|
||||
},
|
||||
});
|
||||
|
||||
export default LayoutRenderer;
|
||||
|
|
|
@ -103,7 +103,7 @@ export default React.createClass({
|
|||
<div className={classNames({"graph--hasYLabel": !!(options.ylabel || options.y2label)})}>
|
||||
{isRefreshing ? this.renderSpinner() : null}
|
||||
<Dygraph
|
||||
containerStyle={{width: '100%', height: '300px'}}
|
||||
containerStyle={{width: '100%', height: '100%'}}
|
||||
overrideLineColors={overrideLineColors}
|
||||
isGraphFilled={isGraphFilled}
|
||||
timeSeries={timeSeries}
|
||||
|
|
|
@ -466,4 +466,7 @@ export const DEFAULT_LINE_COLORS = [
|
|||
export const STROKE_WIDTH = {
|
||||
heavy: 3.5,
|
||||
light: 1.5,
|
||||
};
|
||||
};
|
||||
|
||||
export const PRESENTATION_MODE_ANIMATION_DELAY = 250 // In milliseconds.
|
||||
export const PRESENTATION_MODE_NOTIFICATION_DELAY = 2000 // In milliseconds.
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import {delayEnablePresentationMode} from 'shared/actions/ui'
|
||||
import {publishNotification, delayDismissNotification} from 'shared/actions/notifications'
|
||||
import {PRESENTATION_MODE_NOTIFICATION_DELAY} from 'shared/constants'
|
||||
|
||||
export const presentationButtonDispatcher = (dispatch) => () => {
|
||||
dispatch(delayEnablePresentationMode())
|
||||
dispatch(publishNotification('success', 'Press ESC to disable presentation mode.'))
|
||||
dispatch(delayDismissNotification('success', PRESENTATION_MODE_NOTIFICATION_DELAY))
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
import appUI from './ui';
|
||||
import me from './me';
|
||||
import notifications from './notifications';
|
||||
import sources from './sources';
|
||||
|
||||
export {
|
||||
appUI,
|
||||
me,
|
||||
notifications,
|
||||
sources,
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
const initialState = {
|
||||
presentationMode: false,
|
||||
};
|
||||
|
||||
export default function ui(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case 'ENABLE_PRESENTATION_MODE': {
|
||||
return {
|
||||
...state,
|
||||
presentationMode: true,
|
||||
}
|
||||
}
|
||||
|
||||
case 'DISABLE_PRESENTATION_MODE': {
|
||||
return {
|
||||
...state,
|
||||
presentationMode: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
|
@ -1,7 +1,11 @@
|
|||
import React, {PropTypes} from 'react';
|
||||
import {NavBar, NavBlock, NavHeader, NavListItem} from 'src/side_nav/components/NavItems';
|
||||
|
||||
const {string, shape} = PropTypes;
|
||||
const {
|
||||
string,
|
||||
shape,
|
||||
bool,
|
||||
} = PropTypes;
|
||||
const SideNav = React.createClass({
|
||||
propTypes: {
|
||||
location: string.isRequired,
|
||||
|
@ -9,36 +13,36 @@ const SideNav = React.createClass({
|
|||
me: shape({
|
||||
email: string,
|
||||
}),
|
||||
isHidden: bool.isRequired,
|
||||
},
|
||||
|
||||
render() {
|
||||
const {me, location, sourceID} = this.props;
|
||||
const {me, location, sourceID, isHidden} = this.props;
|
||||
const sourcePrefix = `/sources/${sourceID}`;
|
||||
const dataExplorerLink = `${sourcePrefix}/chronograf/data-explorer`;
|
||||
|
||||
const loggedIn = !!(me && me.email);
|
||||
|
||||
return (
|
||||
return isHidden ? null : (
|
||||
<NavBar location={location}>
|
||||
<div className="sidebar__logo">
|
||||
<a href="/"><span className="icon cubo-uniform"></span></a>
|
||||
</div>
|
||||
<NavBlock icon="cpu" link={`${sourcePrefix}/hosts`}>
|
||||
<NavHeader link={`${sourcePrefix}/hosts`} title="Infrastructure" />
|
||||
<NavListItem link={`${sourcePrefix}/hosts`}>Host List</NavListItem>
|
||||
<NavListItem link={`${sourcePrefix}/kubernetes`}>Kubernetes Dashboard</NavListItem>
|
||||
<NavBlock icon="cubo-node" link={`${sourcePrefix}/hosts`}>
|
||||
<NavHeader link={`${sourcePrefix}/hosts`} title="Host List" />
|
||||
</NavBlock>
|
||||
<NavBlock icon="graphline" link={dataExplorerLink}>
|
||||
<NavHeader link={dataExplorerLink} title={'Data'} />
|
||||
<NavListItem link={dataExplorerLink}>Explorer</NavListItem>
|
||||
<NavListItem link={`${sourcePrefix}/dashboards`}>Dashboards</NavListItem>
|
||||
<NavHeader link={dataExplorerLink} title="Data Explorer" />
|
||||
</NavBlock>
|
||||
<NavBlock matcher="alerts" icon="pulse-b" link={`${sourcePrefix}/alerts`}>
|
||||
<NavBlock icon="dash-h" link={`${sourcePrefix}/dashboards`}>
|
||||
<NavHeader link={`${sourcePrefix}/dashboards`} title={'Dashboards'} />
|
||||
</NavBlock>
|
||||
<NavBlock matcher="alerts" icon="alert-triangle" link={`${sourcePrefix}/alerts`}>
|
||||
<NavHeader link={`${sourcePrefix}/alerts`} title="Alerting" />
|
||||
<NavListItem link={`${sourcePrefix}/alerts`}>Alert History</NavListItem>
|
||||
<NavListItem link={`${sourcePrefix}/alert-rules`}>Kapacitor Rules</NavListItem>
|
||||
</NavBlock>
|
||||
<NavBlock icon="access-key" link={`${sourcePrefix}/manage-sources`}>
|
||||
<NavBlock icon="cog-thick" link={`${sourcePrefix}/manage-sources`}>
|
||||
<NavHeader link={`${sourcePrefix}/manage-sources`} title="Configuration" />
|
||||
<NavListItem link={`${sourcePrefix}/manage-sources`}>InfluxDB</NavListItem>
|
||||
<NavListItem link={`${sourcePrefix}/kapacitor-config`}>Kapacitor</NavListItem>
|
||||
|
|
|
@ -2,7 +2,13 @@ import React, {PropTypes} from 'react';
|
|||
import {connect} from 'react-redux';
|
||||
import SideNav from '../components/SideNav';
|
||||
|
||||
const {func, string, shape} = PropTypes;
|
||||
const {
|
||||
func,
|
||||
string,
|
||||
shape,
|
||||
bool,
|
||||
} = PropTypes
|
||||
|
||||
const SideNavApp = React.createClass({
|
||||
propTypes: {
|
||||
currentLocation: string.isRequired,
|
||||
|
@ -11,25 +17,27 @@ const SideNavApp = React.createClass({
|
|||
me: shape({
|
||||
email: string,
|
||||
}),
|
||||
inPresentationMode: bool.isRequired,
|
||||
},
|
||||
|
||||
render() {
|
||||
const {me, currentLocation, sourceID} = this.props;
|
||||
const {me, currentLocation, sourceID, inPresentationMode} = this.props;
|
||||
|
||||
return (
|
||||
<SideNav
|
||||
sourceID={sourceID}
|
||||
location={currentLocation}
|
||||
me={me}
|
||||
isHidden={inPresentationMode}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
me: state.me,
|
||||
inPresentationMode: state.appUI.presentationMode,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -5,12 +5,14 @@ import makeQueryExecuter from 'src/shared/middleware/queryExecuter';
|
|||
import * as dataExplorerReducers from 'src/data_explorer/reducers';
|
||||
import * as sharedReducers from 'src/shared/reducers';
|
||||
import rulesReducer from 'src/kapacitor/reducers/rules';
|
||||
import dashboardUI from 'src/dashboards/reducers/ui';
|
||||
import persistStateEnhancer from './persistStateEnhancer';
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
...sharedReducers,
|
||||
...dataExplorerReducers,
|
||||
rules: rulesReducer,
|
||||
dashboardUI,
|
||||
});
|
||||
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
@import 'pages/hosts';
|
||||
@import 'pages/kapacitor';
|
||||
@import 'pages/data-explorer';
|
||||
@import 'pages/dashboards';
|
||||
|
||||
// TODO
|
||||
@import 'unsorted';
|
||||
|
|
|
@ -15,9 +15,8 @@
|
|||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='fade-out($g20-white, 0.71)', endColorstr='fade-out($g20-white, 0.71)',GradientType=0 );
|
||||
}
|
||||
.container--dygraph-legend {
|
||||
top: 300px !important;
|
||||
transform: translate(-50%,-6px);
|
||||
background-color: $g1-raven;
|
||||
transform: translateX(-50%);
|
||||
background-color: $g0-obsidian;
|
||||
display: block;
|
||||
position: absolute;
|
||||
padding: 11px;
|
||||
|
@ -119,9 +118,9 @@
|
|||
.graph--hasYLabel {
|
||||
|
||||
.dygraph-axis-label-y {
|
||||
padding: 0 1px 0 12px !important;
|
||||
padding: 0 1px 0 10px !important;
|
||||
}
|
||||
.dygraph-axis-label-y2 {
|
||||
padding: 0 12px 0 1px !important;
|
||||
padding: 0 10px 0 1px !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,15 @@
|
|||
&--purple-scrollbar {
|
||||
@include custom-scrollbar($g2-kevlar,$c-comet);
|
||||
}
|
||||
|
||||
&.presentation-mode {
|
||||
top: 0;
|
||||
height: 100%;
|
||||
|
||||
.dashboard {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.container-fluid {
|
||||
padding: ($chronograf-page-header-height / 2) $page-wrapper-padding ($chronograf-page-header-height / 2) $page-wrapper-padding;
|
||||
|
@ -457,4 +466,4 @@ table .monotype {
|
|||
margin-bottom: 75px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,9 +13,31 @@
|
|||
background: linear-gradient(to right, $startColor 0%,$endColor 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='$startColor', endColorstr='$endColor',GradientType=1 );
|
||||
}
|
||||
@mixin gradient-diag-up($startColor, $endColor) {
|
||||
background: $startColor;
|
||||
background: -moz-linear-gradient(45deg, $startColor 0%, $endColor 100%);
|
||||
background: -webkit-linear-gradient(45deg, $startColor 0%,$endColor 100%);
|
||||
background: linear-gradient(45deg, $startColor 0%,$endColor 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='$startColor', endColorstr='$endColor',GradientType=1 );
|
||||
}
|
||||
@mixin gradient-diag-down($startColor, $endColor) {
|
||||
background: $startColor;
|
||||
background: -moz-linear-gradient(135deg, $startColor 0%, $endColor 100%);
|
||||
background: -webkit-linear-gradient(135deg, $startColor 0%,$endColor 100%);
|
||||
background: linear-gradient(135deg, $startColor 0%,$endColor 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='$startColor', endColorstr='$endColor',GradientType=1 );
|
||||
}
|
||||
@mixin gradient-r($startColor, $endColor) {
|
||||
background: $startColor;
|
||||
background: -moz-radial-gradient(center, ellipse cover, $startColor 0%, $endColor 100%);
|
||||
background: -webkit-radial-gradient(center, ellipse cover, $startColor 0%,$endColor 100%);
|
||||
background: radial-gradient(ellipse at center, $startColor 0%,$endColor 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='$startColor', endColorstr='$endColor',GradientType=1 );
|
||||
}
|
||||
|
||||
// Custom Scrollbars (Chrome Only)
|
||||
$scrollbar-width: 16px;
|
||||
$scrollbar-offset: 3px;
|
||||
@mixin custom-scrollbar($trackColor, $handleColor) {
|
||||
&::-webkit-scrollbar {
|
||||
width: $scrollbar-width;
|
||||
|
@ -30,12 +52,12 @@ $scrollbar-width: 16px;
|
|||
}
|
||||
&-track-piece {
|
||||
background-color: $trackColor;
|
||||
border: 3px solid $trackColor;
|
||||
border: $scrollbar-offset solid $trackColor;
|
||||
border-radius: ($scrollbar-width / 2);
|
||||
}
|
||||
&-thumb {
|
||||
background-color: $handleColor;
|
||||
border: 3px solid $trackColor;
|
||||
border: $scrollbar-offset solid $trackColor;
|
||||
border-radius: ($scrollbar-width / 2);
|
||||
}
|
||||
&-corner {
|
||||
|
@ -45,4 +67,4 @@ $scrollbar-width: 16px;
|
|||
&::-webkit-resizer {
|
||||
background-color: $trackColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,229 @@
|
|||
/*
|
||||
Variables
|
||||
------------------------------------------------------
|
||||
*/
|
||||
$dash-graph-heading: 30px;
|
||||
|
||||
|
||||
/*
|
||||
Animations
|
||||
------------------------------------------------------
|
||||
*/
|
||||
@keyframes refreshingSpinnerA {
|
||||
0% { transform: translate(-50%,-50%) scale(1.75); background-color: $g7-graphite; }
|
||||
33% { transform: translate(-50%,-50%) scale(1,1); }
|
||||
66% { transform: translate(-50%,-50%) scale(1,1); }
|
||||
100% { transform: translate(-50%,-50%) scale(1,1); }
|
||||
}
|
||||
@keyframes refreshingSpinnerB {
|
||||
0% { transform: translate(-50%,-50%) scale(1,1); }
|
||||
33% { transform: translate(-50%,-50%) scale(1.75); background-color: $g7-graphite; }
|
||||
66% { transform: translate(-50%,-50%) scale(1,1); }
|
||||
100% { transform: translate(-50%,-50%) scale(1,1); }
|
||||
}
|
||||
@keyframes refreshingSpinnerC {
|
||||
0% { transform: translate(-50%,-50%) scale(1,1); }
|
||||
33% { transform: translate(-50%,-50%) scale(1,1); }
|
||||
66% { transform: translate(-50%,-50%) scale(1.75); background-color: $g7-graphite; }
|
||||
100% { transform: translate(-50%,-50%) scale(1,1); }
|
||||
}
|
||||
|
||||
/*
|
||||
Default Dashboard Mode
|
||||
------------------------------------------------------
|
||||
*/
|
||||
.dashboard {
|
||||
.react-grid-item {
|
||||
background-color: $g3-castle;
|
||||
border-radius: $radius;
|
||||
border: 2px solid $g3-castle;
|
||||
transition-property: left, top, border-color, background-color;
|
||||
}
|
||||
.graph-empty {
|
||||
background-color: transparent;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
.dash-graph--container {
|
||||
user-select: none !important;
|
||||
-o-user-select: none !important;
|
||||
-moz-user-select: none !important;
|
||||
-webkit-user-select: none !important;
|
||||
background-color: transparent;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: calc(100% - #{$dash-graph-heading});
|
||||
top: $dash-graph-heading;
|
||||
left: 0;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
cursor: crosshair;
|
||||
}
|
||||
& > div:not(.graph-empty) {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
& > div:not(.graph-panel__refreshing) {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
}
|
||||
.graph-panel__refreshing {
|
||||
top: (-$dash-graph-heading + 5px) !important;
|
||||
}
|
||||
}
|
||||
.dash-graph--heading {
|
||||
user-select: none !important;
|
||||
-o-user-select: none !important;
|
||||
-moz-user-select: none !important;
|
||||
-webkit-user-select: none !important;
|
||||
background-color: transparent;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: $dash-graph-heading;
|
||||
padding: 0 16px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: $radius;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: $g14-chromium;
|
||||
transition:
|
||||
color 0.25s ease,
|
||||
background-color 0.25s ease;
|
||||
&:hover {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
.graph-panel__refreshing {
|
||||
position: absolute;
|
||||
top: -18px !important;
|
||||
transform: translate(0,0);
|
||||
right: 16px !important;
|
||||
width: 16px;
|
||||
height: 18px;
|
||||
|
||||
> div {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background-color: $g6-smoke;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%,-50%);
|
||||
}
|
||||
|
||||
div:nth-child(1) {left: 0; animation: refreshingSpinnerA 0.8s cubic-bezier(0.645, 0.045, 0.355, 1) infinite; }
|
||||
div:nth-child(2) {left: 50%; animation: refreshingSpinnerB 0.8s cubic-bezier(0.645, 0.045, 0.355, 1) infinite; }
|
||||
div:nth-child(3) {left: 100%; animation: refreshingSpinnerC 0.8s cubic-bezier(0.645, 0.045, 0.355, 1) infinite;}
|
||||
}
|
||||
|
||||
/*
|
||||
Dashboard Edit Mode
|
||||
------------------------------------------------------
|
||||
*/
|
||||
.dashboard.dashboard-edit {
|
||||
.dash-graph--heading:hover {
|
||||
background-color: $g4-onyx;
|
||||
color: $g18-cloud;
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
cursor: grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: -webkit-grab;
|
||||
}
|
||||
.react-grid-placeholder {
|
||||
@include gradient-diag-down($c-pool,$c-comet);
|
||||
border: 0;
|
||||
opacity: 0.3;
|
||||
z-index: 2;
|
||||
}
|
||||
.react-grid-item {
|
||||
&.resizing {
|
||||
background-color: fade-out($g3-castle,0.09);
|
||||
border-color: $c-pool;
|
||||
border-image-slice: 3%;
|
||||
border-image-repeat: initial;
|
||||
border-image-outset: 0;
|
||||
border-image-width: 2px;
|
||||
border-image-source: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDY3IDc5LjE1Nzc0NywgMjAxNS8wMy8zMC0yMzo0MDo0MiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTg0NjVDRkVGMEVFMTFFNkE0QjVFRTJGNEI1ODc0RDMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTg0NjVDRkZGMEVFMTFFNkE0QjVFRTJGNEI1ODc0RDMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDoxODQ2NUNGQ0YwRUUxMUU2QTRCNUVFMkY0QjU4NzREMyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoxODQ2NUNGREYwRUUxMUU2QTRCNUVFMkY0QjU4NzREMyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PpeetfIAAAMnSURBVHja7N1PatwwFMfxJ5NlKT1DIfQKWZfSA/Q0hexDL9McoOQAPUKglwhp6dZ9Ho/HfyTZs6l+b/E1GDm27IH5oH9Pyji9//7XfLtNZt88/eT722TzlvrFseXHaXFmypuO8vd5nmW6nyeNefrKfZv7i9f75blU/NzafXvns2dV7tl8zqsnT55+9f3Xjf/xwQ9+evou+xLB+N8Ydi4AX3z/6PnvOj94AEOGMV/rB4P00J2rKTC0GNOTPne0GWEwhv1NB0YYjNPWgREHI00gYMTAOIGAEQdjuKcDIw7GXGWBEQJjrLLACIORrFBlgaHDsG2VBYYWY1VlgaHHSH3WqIOhxLB1ow6GGmPRqIMRAeMMAkYUDFuGTsDQYwxP6MCIg1Hp9oKhwih0e8FQYthuLAuM5hj1WBYYEoxUjGWBIcOwrFEHQ4qxLiFgyDFOvSww4mCM8yFghMEoDgzB0GGk2owhGBoMq5UQMDQYxRIChg4ja0PA0GLYMrgIhh7jUkLAiIExV1lghMA4GBiC0RrjNIULRhyMysAQDBVGYWAIhhJjM6cOhhpjUULAiIAxr1wEIwTGPDAEIwTGWGWBEQajHu0FQ4JRjvaCIcPIo71gSDHW0V4w5Bj5SB0MKUZxoRwYOoxsPgQMLcZqPgQMPUaxUQdDh2HVcQgYEoxUHIeAIcPIqywwpBjrKgsMOcb8f+pghMDIwu9gaDFWI3Uw9Bg2N+pgRMA497LAiIJRXf0OhgajuPodDB3G1dFeMNpgXBXtBaMdxmG0F4y2GLvRXjDaY2wGhmCoMawU7QVDh5G20V4wtBjzwBCMEBiXVx6BEQPjsJcFRluM3V4WGO0xqr0sMDQYVuplgaHDWL1YEgw9hi17WWDoMVJ1ChcMCYYVp3DBkGFUl5KCocGw6deAwIiBYUfBRTDaYmTdXjC0GFYLLoKhwSj+cAAYOgzbBhfB0GKsgotg6DGuWrkIRjuMudsLRgiMsQ0BIwzG5ZVHYMTAmKqsVzBiYPj2Z+j2PoERAmM4/2MoIfe+v4Ahx3jx5H4AefYLd37q0Y9/g9EcY/jOHz11A3v+J8AA9wisahRCWTQAAAAASUVORK5CYII=);
|
||||
z-index: 3;
|
||||
|
||||
& > .react-resizable-handle {
|
||||
&:before, &:after {
|
||||
background-color: $c-comet;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.react-draggable-dragging {
|
||||
background-color: fade-out($g3-castle,0.09);
|
||||
border-color: $c-pool;
|
||||
border-image-slice: 3%;
|
||||
border-image-repeat: initial;
|
||||
border-image-outset: 0;
|
||||
border-image-width: 2px;
|
||||
border-image-source: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDY3IDc5LjE1Nzc0NywgMjAxNS8wMy8zMC0yMzo0MDo0MiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTg0NjVDRkVGMEVFMTFFNkE0QjVFRTJGNEI1ODc0RDMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTg0NjVDRkZGMEVFMTFFNkE0QjVFRTJGNEI1ODc0RDMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDoxODQ2NUNGQ0YwRUUxMUU2QTRCNUVFMkY0QjU4NzREMyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoxODQ2NUNGREYwRUUxMUU2QTRCNUVFMkY0QjU4NzREMyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PpeetfIAAAMnSURBVHja7N1PatwwFMfxJ5NlKT1DIfQKWZfSA/Q0hexDL9McoOQAPUKglwhp6dZ9Ho/HfyTZs6l+b/E1GDm27IH5oH9Pyji9//7XfLtNZt88/eT722TzlvrFseXHaXFmypuO8vd5nmW6nyeNefrKfZv7i9f75blU/NzafXvns2dV7tl8zqsnT55+9f3Xjf/xwQ9+evou+xLB+N8Ydi4AX3z/6PnvOj94AEOGMV/rB4P00J2rKTC0GNOTPne0GWEwhv1NB0YYjNPWgREHI00gYMTAOIGAEQdjuKcDIw7GXGWBEQJjrLLACIORrFBlgaHDsG2VBYYWY1VlgaHHSH3WqIOhxLB1ow6GGmPRqIMRAeMMAkYUDFuGTsDQYwxP6MCIg1Hp9oKhwih0e8FQYthuLAuM5hj1WBYYEoxUjGWBIcOwrFEHQ4qxLiFgyDFOvSww4mCM8yFghMEoDgzB0GGk2owhGBoMq5UQMDQYxRIChg4ja0PA0GLYMrgIhh7jUkLAiIExV1lghMA4GBiC0RrjNIULRhyMysAQDBVGYWAIhhJjM6cOhhpjUULAiIAxr1wEIwTGPDAEIwTGWGWBEQajHu0FQ4JRjvaCIcPIo71gSDHW0V4w5Bj5SB0MKUZxoRwYOoxsPgQMLcZqPgQMPUaxUQdDh2HVcQgYEoxUHIeAIcPIqywwpBjrKgsMOcb8f+pghMDIwu9gaDFWI3Uw9Bg2N+pgRMA497LAiIJRXf0OhgajuPodDB3G1dFeMNpgXBXtBaMdxmG0F4y2GLvRXjDaY2wGhmCoMawU7QVDh5G20V4wtBjzwBCMEBiXVx6BEQPjsJcFRluM3V4WGO0xqr0sMDQYVuplgaHDWL1YEgw9hi17WWDoMVJ1ChcMCYYVp3DBkGFUl5KCocGw6deAwIiBYUfBRTDaYmTdXjC0GFYLLoKhwSj+cAAYOgzbBhfB0GKsgotg6DGuWrkIRjuMudsLRgiMsQ0BIwzG5ZVHYMTAmKqsVzBiYPj2Z+j2PoERAmM4/2MoIfe+v4Ahx3jx5H4AefYLd37q0Y9/g9EcY/jOHz11A3v+J8AA9wisahRCWTQAAAAASUVORK5CYII=);
|
||||
cursor: grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: -webkit-grabbing;
|
||||
&:hover {
|
||||
cursor: grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
|
||||
& > .dash-graph--heading,
|
||||
& > .dash-graph--heading:hover {
|
||||
background-color: $g4-onyx;
|
||||
color: $g18-cloud;
|
||||
cursor: grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
}
|
||||
&.cssTransforms {
|
||||
transition-property: transform, border-color, background-color;
|
||||
}
|
||||
& > .react-resizable-handle {
|
||||
background-image: none;
|
||||
cursor: nwse-resize;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: 2px;
|
||||
background-color: $g6-smoke;
|
||||
transition: background-color 0.25s ease;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
&:before {
|
||||
width: 20px;
|
||||
transform: translate(-50%,-50%) rotate(-45deg);
|
||||
}
|
||||
&:after {
|
||||
width: 12px;
|
||||
transform: translate(-3px,2px) rotate(-45deg);
|
||||
}
|
||||
&:hover {
|
||||
&:before, &:after {
|
||||
background-color: $c-comet;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,29 +3,13 @@
|
|||
----------------------------------------------
|
||||
*/
|
||||
|
||||
@keyframes refreshingSpinnerA {
|
||||
0% { transform: translate(-50%,-50%) scale(1.75); background-color: $g7-graphite; }
|
||||
33% { transform: translate(-50%,-50%) scale(1,1); }
|
||||
66% { transform: translate(-50%,-50%) scale(1,1); }
|
||||
100% { transform: translate(-50%,-50%) scale(1,1); }
|
||||
}
|
||||
@keyframes refreshingSpinnerB {
|
||||
0% { transform: translate(-50%,-50%) scale(1,1); }
|
||||
33% { transform: translate(-50%,-50%) scale(1.75); background-color: $g7-graphite; }
|
||||
66% { transform: translate(-50%,-50%) scale(1,1); }
|
||||
100% { transform: translate(-50%,-50%) scale(1,1); }
|
||||
}
|
||||
@keyframes refreshingSpinnerC {
|
||||
0% { transform: translate(-50%,-50%) scale(1,1); }
|
||||
33% { transform: translate(-50%,-50%) scale(1,1); }
|
||||
66% { transform: translate(-50%,-50%) scale(1.75); background-color: $g7-graphite; }
|
||||
100% { transform: translate(-50%,-50%) scale(1,1); }
|
||||
}
|
||||
.graph-container.hosts-graph {
|
||||
padding: 8px 16px;
|
||||
|
||||
.single-stat {
|
||||
font-size: 32px;
|
||||
font-size: 60px;
|
||||
font-weight: 300;
|
||||
color: $c-pool;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
@ -37,41 +21,8 @@
|
|||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.graph-panel__refreshing {
|
||||
position: absolute;
|
||||
top: -18px !important;
|
||||
transform: translate(0,0);
|
||||
right: 16px !important;
|
||||
width: 16px;
|
||||
height: 18px;
|
||||
|
||||
> div {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background-color: $g6-smoke;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%,-50%);
|
||||
}
|
||||
|
||||
div:nth-child(1) {left: 0; animation: refreshingSpinnerA 0.8s cubic-bezier(0.645, 0.045, 0.355, 1) infinite; }
|
||||
div:nth-child(2) {left: 50%; animation: refreshingSpinnerB 0.8s cubic-bezier(0.645, 0.045, 0.355, 1) infinite; }
|
||||
div:nth-child(3) {left: 100%; animation: refreshingSpinnerC 0.8s cubic-bezier(0.645, 0.045, 0.355, 1) infinite;}
|
||||
}
|
||||
}
|
||||
.hosts-graph-heading {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
background-color: $g3-castle;
|
||||
padding: 14px 16px 2px 16px;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: $g14-chromium;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
.host-list--active-source {
|
||||
text-transform: uppercase;
|
||||
font-size: 15px;
|
||||
|
@ -89,8 +40,6 @@
|
|||
}
|
||||
|
||||
/* Hacky way to ensure that legends cannot be obscured by neighboring graphs */
|
||||
.react-grid-item {
|
||||
&:hover {
|
||||
z-index: 8999;
|
||||
}
|
||||
div:not(.dashboard-edit) .react-grid-item:hover {
|
||||
z-index: 8999;
|
||||
}
|
||||
|
|
|
@ -74,13 +74,14 @@
|
|||
line-height: 30px !important;
|
||||
height: 30px !important;
|
||||
padding: 0 9px !important;
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
margin: 0 4px 0 0 ;
|
||||
}
|
||||
}
|
||||
.btn.btn-xs .icon {
|
||||
a.btn.btn-sm > span.icon,
|
||||
div.btn.btn-sm > span.icon,
|
||||
button.btn.btn-sm > span.icon {
|
||||
font-size: 16px;
|
||||
margin: 0 4px 0 0 ;
|
||||
}
|
||||
.btn.btn-xs > .icon {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue