Merge branch 'master' into feature/annotationz-pre-pl-with-master
commit
f2cc269a33
|
@ -1,5 +1,5 @@
|
|||
[bumpversion]
|
||||
current_version = 1.4.0.0
|
||||
current_version = 1.4.0.1
|
||||
files = README.md server/swagger.json
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\.(?P<release>\d+)
|
||||
serialize = {major}.{minor}.{patch}.{release}
|
||||
|
|
34
CHANGELOG.md
34
CHANGELOG.md
|
@ -1,13 +1,36 @@
|
|||
## v1.4.1.0 [unreleased]
|
||||
### Features
|
||||
1. [#2409](https://github.com/influxdata/chronograf/pull/2409): Allow adding multiple event handlers to a rule
|
||||
1. [#2709](https://github.com/influxdata/chronograf/pull/2709): Add "send test alert" button to test kapacitor alert configurations
|
||||
1. [#2708](https://github.com/influxdata/chronograf/pull/2708): Link to specified kapacitor config panel from rule builder alert handlers
|
||||
1. [#2722](https://github.com/influxdata/chronograf/pull/2722): Add auto refresh widget to hosts list page
|
||||
1. [#2765](https://github.com/influxdata/chronograf/pull/2765): Update to go 1.9.3 and node 6.12.3 for releases
|
||||
1. [#2784](https://github.com/influxdata/chronograf/pull/2784): Update to go 1.9.4
|
||||
1. [#2703](https://github.com/influxdata/chronograf/pull/2703): Add global users page visible only to super admins
|
||||
1. [#2777](https://github.com/influxdata/chronograf/pull/2777): Allow user to delete themselves
|
||||
|
||||
1. [#2703](https://github.com/influxdata/chronograf/pull/2703): Add global users page visible only to super admins
|
||||
1. [#2781](https://github.com/influxdata/chronograf/pull/2781): Add commands to users & create super admin
|
||||
### UI Improvements
|
||||
1. [#2698](https://github.com/influxdata/chronograf/pull/2698): Improve clarity of terminology surrounding InfluxDB & Kapacitor connections
|
||||
1. [#2746](https://github.com/influxdata/chronograf/pull/2746): Separate saving TICKscript from exiting editor page
|
||||
1. [#2774](https://github.com/influxdata/chronograf/pull/2774): Enable Save (⌘ + Enter) and Cancel (Escape) hotkeys in Cell Editor Overlay
|
||||
|
||||
### Bug Fixes
|
||||
1. [#2684](https://github.com/influxdata/chronograf/pull/2684): Fix TICKscript Sensu alerts when no group by tags selected
|
||||
1. [#2735](https://github.com/influxdata/chronograf/pull/2735): Remove cli options from systemd service file
|
||||
1. [#2757](https://github.com/influxdata/chronograf/pull/2757): Added "TO" field to kapacitor SMTP config, and improved error messages for config saving and testing
|
||||
1. [#2761](https://github.com/influxdata/chronograf/pull/2761): Remove cli options from sysvinit service file
|
||||
1. [#2780](https://github.com/influxdata/chronograf/pull/2780): Fix routing on alert save
|
||||
|
||||
## v1.4.0.1 [2017-1-9]
|
||||
### Features
|
||||
1. [#2690](https://github.com/influxdata/chronograf/pull/2690): Add separate CLI flag for canned sources, kapacitors, dashboards, and organizations
|
||||
1. [#2672](https://github.com/influxdata/chronograf/pull/2672): Add telegraf interval configuration
|
||||
|
||||
### Bug Fixes
|
||||
1. [#2689](https://github.com/influxdata/chronograf/pull/2689): Allow insecure (self-signed) certificates for kapacitor and influxdb
|
||||
|
||||
|
||||
## v1.4.0.1 [unreleased]
|
||||
### UI Improvements
|
||||
1. [#2690](https://github.com/influxdata/chronograf/pull/2690): Add separate CLI flag for canned sources, kapacitors, dashboards, and organizations
|
||||
1. [#2664](https://github.com/influxdata/chronograf/pull/2664): Fix positioning of custom time indicator
|
||||
|
||||
## v1.4.0.0 [2017-12-22]
|
||||
### UI Improvements
|
||||
|
@ -103,7 +126,6 @@
|
|||
1. [#2460](https://github.com/influxdata/chronograf/pull/2460): Update kapacitor alerts to cast to float before sending to influx
|
||||
1. [#2479](https://github.com/influxdata/chronograf/pull/2479): Support authentication for Enterprise Meta Nodes
|
||||
1. [#2477](https://github.com/influxdata/chronograf/pull/2477): Improve performance of hoverline rendering
|
||||
1. [#2409](https://github.com/influxdata/chronograf/pull/2409): Add multiple event handlers to rules
|
||||
|
||||
### UI Improvements
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
branch = "master"
|
||||
name = "github.com/dustin/go-humanize"
|
||||
packages = ["."]
|
||||
revision = "259d2a102b871d17f30e3cd9881a642961a1e486"
|
||||
revision = "bb3d318650d48840a39aa21a027c6630e198e626"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/elazarl/go-bindata-assetfs"
|
||||
|
@ -39,18 +39,53 @@
|
|||
|
||||
[[projects]]
|
||||
name = "github.com/gogo/protobuf"
|
||||
packages = ["gogoproto","jsonpb","plugin/compare","plugin/defaultcheck","plugin/description","plugin/embedcheck","plugin/enumstringer","plugin/equal","plugin/face","plugin/gostring","plugin/marshalto","plugin/oneofcheck","plugin/populate","plugin/size","plugin/stringer","plugin/testgen","plugin/union","plugin/unmarshal","proto","protoc-gen-gogo","protoc-gen-gogo/descriptor","protoc-gen-gogo/generator","protoc-gen-gogo/grpc","protoc-gen-gogo/plugin","vanity","vanity/command"]
|
||||
packages = [
|
||||
"gogoproto",
|
||||
"jsonpb",
|
||||
"plugin/compare",
|
||||
"plugin/defaultcheck",
|
||||
"plugin/description",
|
||||
"plugin/embedcheck",
|
||||
"plugin/enumstringer",
|
||||
"plugin/equal",
|
||||
"plugin/face",
|
||||
"plugin/gostring",
|
||||
"plugin/marshalto",
|
||||
"plugin/oneofcheck",
|
||||
"plugin/populate",
|
||||
"plugin/size",
|
||||
"plugin/stringer",
|
||||
"plugin/testgen",
|
||||
"plugin/union",
|
||||
"plugin/unmarshal",
|
||||
"proto",
|
||||
"protoc-gen-gogo",
|
||||
"protoc-gen-gogo/descriptor",
|
||||
"protoc-gen-gogo/generator",
|
||||
"protoc-gen-gogo/grpc",
|
||||
"protoc-gen-gogo/plugin",
|
||||
"vanity",
|
||||
"vanity/command"
|
||||
]
|
||||
revision = "6abcf94fd4c97dcb423fdafd42fe9f96ca7e421b"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/golang/protobuf"
|
||||
packages = ["proto"]
|
||||
revision = "8ee79997227bf9b34611aee7946ae64735e6fd93"
|
||||
revision = "925541529c1fa6821df4e44ce2723319eb2be768"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/google/go-cmp"
|
||||
packages = ["cmp","cmp/cmpopts"]
|
||||
revision = "79b2d888f100ec053545168aa94bcfb322e8bfc8"
|
||||
packages = [
|
||||
"cmp",
|
||||
"cmp/cmpopts",
|
||||
"cmp/internal/diff",
|
||||
"cmp/internal/function",
|
||||
"cmp/internal/value"
|
||||
]
|
||||
revision = "8099a9787ce5dc5984ed879a3bda47dc730a8e97"
|
||||
version = "v0.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/google/go-github"
|
||||
|
@ -58,19 +93,35 @@
|
|||
revision = "1bc362c7737e51014af7299e016444b654095ad9"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/google/go-querystring"
|
||||
packages = ["query"]
|
||||
revision = "9235644dd9e52eeae6fa48efd539fdc351a0af53"
|
||||
revision = "53e6ce116135b80d037921a7fdd5138cf32d7a8a"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/influxdata/influxdb"
|
||||
packages = ["influxql","influxql/internal","influxql/neldermead","models","pkg/escape"]
|
||||
packages = [
|
||||
"influxql",
|
||||
"influxql/internal",
|
||||
"influxql/neldermead",
|
||||
"models",
|
||||
"pkg/escape"
|
||||
]
|
||||
revision = "cd9363b52cac452113b95554d98a6be51beda24e"
|
||||
version = "v1.1.5"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/influxdata/kapacitor"
|
||||
packages = ["client/v1","pipeline","pipeline/tick","services/k8s/client","tick","tick/ast","tick/stateful","udf/agent"]
|
||||
packages = [
|
||||
"client/v1",
|
||||
"pipeline",
|
||||
"pipeline/tick",
|
||||
"services/k8s/client",
|
||||
"tick",
|
||||
"tick/ast",
|
||||
"tick/stateful",
|
||||
"udf/agent"
|
||||
]
|
||||
revision = "6de30070b39afde111fea5e041281126fe8aae31"
|
||||
|
||||
[[projects]]
|
||||
|
@ -84,15 +135,15 @@
|
|||
revision = "4cc2832a6e6d1d3b815e2b9d544b2a4dfb3ce8fa"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/jteeuwen/go-bindata"
|
||||
name = "github.com/kevinburke/go-bindata"
|
||||
packages = ["."]
|
||||
revision = "a0ff2567cfb70903282db057e799fd826784d41d"
|
||||
revision = "46eb4c183bfc1ebb527d9d19bcded39476302eb8"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
revision = "ff09b135c25aae272398c51a07235b90a75aa4f0"
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/satori/go.uuid"
|
||||
|
@ -107,39 +158,60 @@
|
|||
[[projects]]
|
||||
name = "github.com/tylerb/graceful"
|
||||
packages = ["."]
|
||||
revision = "50a48b6e73fcc75b45e22c05b79629a67c79e938"
|
||||
version = "v1.2.13"
|
||||
revision = "4654dfbb6ad53cb5e27f37d99b02e16c1872fbbb"
|
||||
version = "v1.2.15"
|
||||
|
||||
[[projects]]
|
||||
name = "golang.org/x/net"
|
||||
packages = ["context","context/ctxhttp"]
|
||||
packages = [
|
||||
"context",
|
||||
"context/ctxhttp"
|
||||
]
|
||||
revision = "749a502dd1eaf3e5bfd4f8956748c502357c0bbe"
|
||||
|
||||
[[projects]]
|
||||
name = "golang.org/x/oauth2"
|
||||
packages = [".","github","heroku","internal"]
|
||||
packages = [
|
||||
".",
|
||||
"github",
|
||||
"heroku",
|
||||
"internal"
|
||||
]
|
||||
revision = "1e695b1c8febf17aad3bfa7bf0a819ef94b98ad5"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix"]
|
||||
revision = "f3918c30c5c2cb527c0b071a27c35120a6c0719a"
|
||||
revision = "37707fdb30a5b38865cfb95e5aab41707daec7fd"
|
||||
|
||||
[[projects]]
|
||||
name = "google.golang.org/api"
|
||||
packages = ["gensupport","googleapi","googleapi/internal/uritemplates","oauth2/v2"]
|
||||
packages = [
|
||||
"gensupport",
|
||||
"googleapi",
|
||||
"googleapi/internal/uritemplates",
|
||||
"oauth2/v2"
|
||||
]
|
||||
revision = "bc20c61134e1d25265dd60049f5735381e79b631"
|
||||
|
||||
[[projects]]
|
||||
name = "google.golang.org/appengine"
|
||||
packages = ["internal","internal/base","internal/datastore","internal/log","internal/remote_api","internal/urlfetch","urlfetch"]
|
||||
packages = [
|
||||
"internal",
|
||||
"internal/base",
|
||||
"internal/datastore",
|
||||
"internal/log",
|
||||
"internal/remote_api",
|
||||
"internal/urlfetch",
|
||||
"urlfetch"
|
||||
]
|
||||
revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
|
||||
version = "v1.0.0"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "a5bd1aa82919723ff8ec5dd9d520329862de8181ca9dba75c6acb3a34df5f1a4"
|
||||
inputs-digest = "11df631364d11bc05c8f71af1aa735360b5a40a793d32d47d1f1d8c694a55f6f"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
required = ["github.com/jteeuwen/go-bindata","github.com/gogo/protobuf/proto","github.com/gogo/protobuf/jsonpb","github.com/gogo/protobuf/protoc-gen-gogo","github.com/gogo/protobuf/gogoproto"]
|
||||
required = ["github.com/kevinburke/go-bindata","github.com/gogo/protobuf/proto","github.com/gogo/protobuf/jsonpb","github.com/gogo/protobuf/protoc-gen-gogo","github.com/gogo/protobuf/gogoproto"]
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/NYTimes/gziphandler"
|
||||
|
@ -41,8 +41,8 @@ required = ["github.com/jteeuwen/go-bindata","github.com/gogo/protobuf/proto","g
|
|||
revision = "4cc2832a6e6d1d3b815e2b9d544b2a4dfb3ce8fa"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/jteeuwen/go-bindata"
|
||||
revision = "a0ff2567cfb70903282db057e799fd826784d41d"
|
||||
name = "github.com/kevinburke/go-bindata"
|
||||
revision = "46eb4c183bfc1ebb527d9d19bcded39476302eb8"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/satori/go.uuid"
|
||||
|
|
4
Makefile
4
Makefile
|
@ -2,7 +2,7 @@
|
|||
|
||||
VERSION ?= $(shell git describe --always --tags)
|
||||
COMMIT ?= $(shell git rev-parse --short=8 HEAD)
|
||||
GOBINDATA := $(shell go list -f {{.Root}} github.com/jteeuwen/go-bindata 2> /dev/null)
|
||||
GOBINDATA := $(shell go list -f {{.Root}} github.com/kevinburke/go-bindata 2> /dev/null)
|
||||
YARN := $(shell command -v yarn 2> /dev/null)
|
||||
|
||||
SOURCES := $(shell find . -name '*.go' ! -name '*_gen.go' -not -path "./vendor/*" )
|
||||
|
@ -73,7 +73,7 @@ dep: .jsdep .godep
|
|||
.godep:
|
||||
ifndef GOBINDATA
|
||||
@echo "Installing go-bindata"
|
||||
go get -u github.com/jteeuwen/go-bindata/...
|
||||
go get -u github.com/kevinburke/go-bindata/...
|
||||
endif
|
||||
@touch .godep
|
||||
|
||||
|
|
10
README.md
10
README.md
|
@ -136,7 +136,7 @@ option.
|
|||
## Versions
|
||||
|
||||
The most recent version of Chronograf is
|
||||
[v1.4.0.0](https://www.influxdata.com/downloads/).
|
||||
[v1.4.0.1](https://www.influxdata.com/downloads/).
|
||||
|
||||
Spotted a bug or have a feature request? Please open
|
||||
[an issue](https://github.com/influxdata/chronograf/issues/new)!
|
||||
|
@ -156,7 +156,7 @@ The Chronograf team has identified and is working on the following issues:
|
|||
## Installation
|
||||
|
||||
Check out the
|
||||
[INSTALLATION](https://docs.influxdata.com/chronograf/v1.3/introduction/installation/)
|
||||
[INSTALLATION](https://docs.influxdata.com/chronograf/v1.4/introduction/installation/)
|
||||
guide to get up and running with Chronograf with as little configuration and
|
||||
code as possible.
|
||||
|
||||
|
@ -178,7 +178,7 @@ By default, chronograf runs on port `8888`.
|
|||
To get started right away with Docker, you can pull down our latest release:
|
||||
|
||||
```sh
|
||||
docker pull chronograf:1.4.0.0
|
||||
docker pull chronograf:1.4.0.1
|
||||
```
|
||||
|
||||
### From Source
|
||||
|
@ -198,10 +198,10 @@ docker pull chronograf:1.4.0.0
|
|||
|
||||
## Documentation
|
||||
|
||||
[Getting Started](https://docs.influxdata.com/chronograf/v1.3/introduction/getting-started/)
|
||||
[Getting Started](https://docs.influxdata.com/chronograf/v1.4/introduction/getting-started/)
|
||||
will get you up and running with Chronograf with as little configuration and
|
||||
code as possible. See our
|
||||
[guides](https://docs.influxdata.com/chronograf/v1.3/guides/) to get familiar
|
||||
[guides](https://docs.influxdata.com/chronograf/v1.4/guides/) to get familiar
|
||||
with Chronograf's main features.
|
||||
|
||||
Documentation for Telegraf, InfluxDB, and Kapacitor are available at
|
||||
|
|
|
@ -34,7 +34,7 @@ func (s *ConfigStore) Migrate(ctx context.Context) error {
|
|||
func (s *ConfigStore) Initialize(ctx context.Context) error {
|
||||
cfg := chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: true,
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
}
|
||||
return s.Update(ctx, &cfg)
|
||||
|
|
|
@ -22,7 +22,7 @@ func TestConfig_Get(t *testing.T) {
|
|||
wants: wants{
|
||||
config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: true,
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -274,6 +274,10 @@ func MarshalDashboard(d chronograf.Dashboard) ([]byte, error) {
|
|||
Type: c.Type,
|
||||
Axes: axes,
|
||||
Colors: colors,
|
||||
Legend: &Legend{
|
||||
Type: c.Legend.Type,
|
||||
Orientation: c.Legend.Orientation,
|
||||
},
|
||||
}
|
||||
}
|
||||
templates := make([]*Template, len(d.Templates))
|
||||
|
@ -394,6 +398,12 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error {
|
|||
}
|
||||
}
|
||||
|
||||
legend := chronograf.Legend{}
|
||||
if c.Legend != nil {
|
||||
legend.Type = c.Legend.Type
|
||||
legend.Orientation = c.Legend.Orientation
|
||||
}
|
||||
|
||||
cells[i] = chronograf.DashboardCell{
|
||||
ID: c.ID,
|
||||
X: c.X,
|
||||
|
@ -405,6 +415,7 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error {
|
|||
Type: c.Type,
|
||||
Axes: axes,
|
||||
CellColors: colors,
|
||||
Legend: legend,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -570,10 +581,7 @@ func UnmarshalRole(data []byte, r *chronograf.Role) error {
|
|||
|
||||
// UnmarshalRolePB decodes a role from binary protobuf data.
|
||||
func UnmarshalRolePB(data []byte, r *Role) error {
|
||||
if err := proto.Unmarshal(data, r); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return proto.Unmarshal(data, r)
|
||||
}
|
||||
|
||||
// MarshalOrganization encodes a organization to binary protobuf format.
|
||||
|
@ -607,10 +615,7 @@ func UnmarshalOrganization(data []byte, o *chronograf.Organization) error {
|
|||
|
||||
// UnmarshalOrganizationPB decodes a organization from binary protobuf data.
|
||||
func UnmarshalOrganizationPB(data []byte, o *Organization) error {
|
||||
if err := proto.Unmarshal(data, o); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return proto.Unmarshal(data, o)
|
||||
}
|
||||
|
||||
// MarshalConfig encodes a config to binary protobuf format.
|
||||
|
@ -643,8 +648,5 @@ func UnmarshalConfig(data []byte, c *chronograf.Config) error {
|
|||
|
||||
// UnmarshalConfigPB decodes a config from binary protobuf data.
|
||||
func UnmarshalConfigPB(data []byte, c *Config) error {
|
||||
if err := proto.Unmarshal(data, c); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return proto.Unmarshal(data, c)
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ It has these top-level messages:
|
|||
Dashboard
|
||||
DashboardCell
|
||||
Color
|
||||
Legend
|
||||
Axis
|
||||
Template
|
||||
TemplateValue
|
||||
|
@ -219,6 +220,7 @@ type DashboardCell struct {
|
|||
ID string `protobuf:"bytes,8,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
Axes map[string]*Axis `protobuf:"bytes,9,rep,name=axes" json:"axes,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value"`
|
||||
Colors []*Color `protobuf:"bytes,10,rep,name=colors" json:"colors,omitempty"`
|
||||
Legend *Legend `protobuf:"bytes,11,opt,name=legend" json:"legend,omitempty"`
|
||||
}
|
||||
|
||||
func (m *DashboardCell) Reset() { *m = DashboardCell{} }
|
||||
|
@ -296,6 +298,13 @@ func (m *DashboardCell) GetColors() []*Color {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *DashboardCell) GetLegend() *Legend {
|
||||
if m != nil {
|
||||
return m.Legend
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Color struct {
|
||||
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
Type string `protobuf:"bytes,2,opt,name=Type,proto3" json:"Type,omitempty"`
|
||||
|
@ -344,6 +353,30 @@ func (m *Color) GetValue() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
type Legend struct {
|
||||
Type string `protobuf:"bytes,1,opt,name=Type,proto3" json:"Type,omitempty"`
|
||||
Orientation string `protobuf:"bytes,2,opt,name=Orientation,proto3" json:"Orientation,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Legend) Reset() { *m = Legend{} }
|
||||
func (m *Legend) String() string { return proto.CompactTextString(m) }
|
||||
func (*Legend) ProtoMessage() {}
|
||||
func (*Legend) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{4} }
|
||||
|
||||
func (m *Legend) GetType() string {
|
||||
if m != nil {
|
||||
return m.Type
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Legend) GetOrientation() string {
|
||||
if m != nil {
|
||||
return m.Orientation
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type Axis struct {
|
||||
LegacyBounds []int64 `protobuf:"varint,1,rep,packed,name=legacyBounds" json:"legacyBounds,omitempty"`
|
||||
Bounds []string `protobuf:"bytes,2,rep,name=bounds" json:"bounds,omitempty"`
|
||||
|
@ -357,7 +390,7 @@ type Axis struct {
|
|||
func (m *Axis) Reset() { *m = Axis{} }
|
||||
func (m *Axis) String() string { return proto.CompactTextString(m) }
|
||||
func (*Axis) ProtoMessage() {}
|
||||
func (*Axis) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{4} }
|
||||
func (*Axis) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{5} }
|
||||
|
||||
func (m *Axis) GetLegacyBounds() []int64 {
|
||||
if m != nil {
|
||||
|
@ -420,7 +453,7 @@ type Template struct {
|
|||
func (m *Template) Reset() { *m = Template{} }
|
||||
func (m *Template) String() string { return proto.CompactTextString(m) }
|
||||
func (*Template) ProtoMessage() {}
|
||||
func (*Template) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{5} }
|
||||
func (*Template) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{6} }
|
||||
|
||||
func (m *Template) GetID() string {
|
||||
if m != nil {
|
||||
|
@ -473,7 +506,7 @@ type TemplateValue struct {
|
|||
func (m *TemplateValue) Reset() { *m = TemplateValue{} }
|
||||
func (m *TemplateValue) String() string { return proto.CompactTextString(m) }
|
||||
func (*TemplateValue) ProtoMessage() {}
|
||||
func (*TemplateValue) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{6} }
|
||||
func (*TemplateValue) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{7} }
|
||||
|
||||
func (m *TemplateValue) GetType() string {
|
||||
if m != nil {
|
||||
|
@ -508,7 +541,7 @@ type TemplateQuery struct {
|
|||
func (m *TemplateQuery) Reset() { *m = TemplateQuery{} }
|
||||
func (m *TemplateQuery) String() string { return proto.CompactTextString(m) }
|
||||
func (*TemplateQuery) ProtoMessage() {}
|
||||
func (*TemplateQuery) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{7} }
|
||||
func (*TemplateQuery) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{8} }
|
||||
|
||||
func (m *TemplateQuery) GetCommand() string {
|
||||
if m != nil {
|
||||
|
@ -566,7 +599,7 @@ type Server struct {
|
|||
func (m *Server) Reset() { *m = Server{} }
|
||||
func (m *Server) String() string { return proto.CompactTextString(m) }
|
||||
func (*Server) ProtoMessage() {}
|
||||
func (*Server) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{8} }
|
||||
func (*Server) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{9} }
|
||||
|
||||
func (m *Server) GetID() int64 {
|
||||
if m != nil {
|
||||
|
@ -635,7 +668,7 @@ type Layout struct {
|
|||
func (m *Layout) Reset() { *m = Layout{} }
|
||||
func (m *Layout) String() string { return proto.CompactTextString(m) }
|
||||
func (*Layout) ProtoMessage() {}
|
||||
func (*Layout) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{9} }
|
||||
func (*Layout) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{10} }
|
||||
|
||||
func (m *Layout) GetID() string {
|
||||
if m != nil {
|
||||
|
@ -689,7 +722,7 @@ type Cell struct {
|
|||
func (m *Cell) Reset() { *m = Cell{} }
|
||||
func (m *Cell) String() string { return proto.CompactTextString(m) }
|
||||
func (*Cell) ProtoMessage() {}
|
||||
func (*Cell) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{10} }
|
||||
func (*Cell) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{11} }
|
||||
|
||||
func (m *Cell) GetX() int32 {
|
||||
if m != nil {
|
||||
|
@ -783,7 +816,7 @@ type Query struct {
|
|||
func (m *Query) Reset() { *m = Query{} }
|
||||
func (m *Query) String() string { return proto.CompactTextString(m) }
|
||||
func (*Query) ProtoMessage() {}
|
||||
func (*Query) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{11} }
|
||||
func (*Query) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{12} }
|
||||
|
||||
func (m *Query) GetCommand() string {
|
||||
if m != nil {
|
||||
|
@ -857,7 +890,7 @@ type TimeShift struct {
|
|||
func (m *TimeShift) Reset() { *m = TimeShift{} }
|
||||
func (m *TimeShift) String() string { return proto.CompactTextString(m) }
|
||||
func (*TimeShift) ProtoMessage() {}
|
||||
func (*TimeShift) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{12} }
|
||||
func (*TimeShift) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{13} }
|
||||
|
||||
func (m *TimeShift) GetLabel() string {
|
||||
if m != nil {
|
||||
|
@ -888,7 +921,7 @@ type Range struct {
|
|||
func (m *Range) Reset() { *m = Range{} }
|
||||
func (m *Range) String() string { return proto.CompactTextString(m) }
|
||||
func (*Range) ProtoMessage() {}
|
||||
func (*Range) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{13} }
|
||||
func (*Range) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{14} }
|
||||
|
||||
func (m *Range) GetUpper() int64 {
|
||||
if m != nil {
|
||||
|
@ -914,7 +947,7 @@ type AlertRule struct {
|
|||
func (m *AlertRule) Reset() { *m = AlertRule{} }
|
||||
func (m *AlertRule) String() string { return proto.CompactTextString(m) }
|
||||
func (*AlertRule) ProtoMessage() {}
|
||||
func (*AlertRule) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{14} }
|
||||
func (*AlertRule) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{15} }
|
||||
|
||||
func (m *AlertRule) GetID() string {
|
||||
if m != nil {
|
||||
|
@ -956,7 +989,7 @@ type User struct {
|
|||
func (m *User) Reset() { *m = User{} }
|
||||
func (m *User) String() string { return proto.CompactTextString(m) }
|
||||
func (*User) ProtoMessage() {}
|
||||
func (*User) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{15} }
|
||||
func (*User) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{16} }
|
||||
|
||||
func (m *User) GetID() uint64 {
|
||||
if m != nil {
|
||||
|
@ -1008,7 +1041,7 @@ type Role struct {
|
|||
func (m *Role) Reset() { *m = Role{} }
|
||||
func (m *Role) String() string { return proto.CompactTextString(m) }
|
||||
func (*Role) ProtoMessage() {}
|
||||
func (*Role) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{16} }
|
||||
func (*Role) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{17} }
|
||||
|
||||
func (m *Role) GetOrganization() string {
|
||||
if m != nil {
|
||||
|
@ -1034,7 +1067,7 @@ type Organization struct {
|
|||
func (m *Organization) Reset() { *m = Organization{} }
|
||||
func (m *Organization) String() string { return proto.CompactTextString(m) }
|
||||
func (*Organization) ProtoMessage() {}
|
||||
func (*Organization) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{17} }
|
||||
func (*Organization) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{18} }
|
||||
|
||||
func (m *Organization) GetID() string {
|
||||
if m != nil {
|
||||
|
@ -1071,7 +1104,7 @@ type Config struct {
|
|||
func (m *Config) Reset() { *m = Config{} }
|
||||
func (m *Config) String() string { return proto.CompactTextString(m) }
|
||||
func (*Config) ProtoMessage() {}
|
||||
func (*Config) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{18} }
|
||||
func (*Config) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{19} }
|
||||
|
||||
func (m *Config) GetAuth() *AuthConfig {
|
||||
if m != nil {
|
||||
|
@ -1087,7 +1120,7 @@ type AuthConfig struct {
|
|||
func (m *AuthConfig) Reset() { *m = AuthConfig{} }
|
||||
func (m *AuthConfig) String() string { return proto.CompactTextString(m) }
|
||||
func (*AuthConfig) ProtoMessage() {}
|
||||
func (*AuthConfig) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{19} }
|
||||
func (*AuthConfig) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{20} }
|
||||
|
||||
func (m *AuthConfig) GetSuperAdminNewUsers() bool {
|
||||
if m != nil {
|
||||
|
@ -1104,7 +1137,7 @@ type BuildInfo struct {
|
|||
func (m *BuildInfo) Reset() { *m = BuildInfo{} }
|
||||
func (m *BuildInfo) String() string { return proto.CompactTextString(m) }
|
||||
func (*BuildInfo) ProtoMessage() {}
|
||||
func (*BuildInfo) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{20} }
|
||||
func (*BuildInfo) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{21} }
|
||||
|
||||
func (m *BuildInfo) GetVersion() string {
|
||||
if m != nil {
|
||||
|
@ -1125,6 +1158,7 @@ func init() {
|
|||
proto.RegisterType((*Dashboard)(nil), "internal.Dashboard")
|
||||
proto.RegisterType((*DashboardCell)(nil), "internal.DashboardCell")
|
||||
proto.RegisterType((*Color)(nil), "internal.Color")
|
||||
proto.RegisterType((*Legend)(nil), "internal.Legend")
|
||||
proto.RegisterType((*Axis)(nil), "internal.Axis")
|
||||
proto.RegisterType((*Template)(nil), "internal.Template")
|
||||
proto.RegisterType((*TemplateValue)(nil), "internal.TemplateValue")
|
||||
|
@ -1147,89 +1181,92 @@ func init() {
|
|||
func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) }
|
||||
|
||||
var fileDescriptorInternal = []byte{
|
||||
// 1342 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x57, 0xdd, 0x8e, 0xe3, 0xc4,
|
||||
0x12, 0x96, 0xe3, 0x38, 0xb1, 0x2b, 0xb3, 0x7b, 0x56, 0x3e, 0xab, 0xb3, 0x3e, 0x7b, 0xa4, 0xa3,
|
||||
0x60, 0x81, 0x08, 0x82, 0x1d, 0xd0, 0xac, 0x90, 0x10, 0x02, 0xa4, 0xcc, 0x04, 0x2d, 0xc3, 0xfe,
|
||||
0xcd, 0x76, 0x76, 0x86, 0x2b, 0xb4, 0xea, 0x38, 0x95, 0xc4, 0x5a, 0xc7, 0x36, 0x6d, 0x7b, 0x26,
|
||||
0xe6, 0x61, 0x90, 0x90, 0x78, 0x02, 0xc4, 0x3d, 0xb7, 0x88, 0x5b, 0xde, 0x81, 0x57, 0xe0, 0x16,
|
||||
0x55, 0x77, 0xfb, 0x27, 0x93, 0xb0, 0xda, 0x0b, 0xc4, 0x5d, 0x7f, 0x55, 0xed, 0xea, 0xfa, 0xf9,
|
||||
0xaa, 0xba, 0x0d, 0x37, 0xc3, 0x38, 0x47, 0x11, 0xf3, 0xe8, 0x30, 0x15, 0x49, 0x9e, 0xb8, 0x76,
|
||||
0x85, 0xfd, 0xdf, 0x3b, 0xd0, 0x9b, 0x26, 0x85, 0x08, 0xd0, 0xbd, 0x09, 0x9d, 0xd3, 0x89, 0x67,
|
||||
0x0c, 0x8d, 0x91, 0xc9, 0x3a, 0xa7, 0x13, 0xd7, 0x85, 0xee, 0x13, 0xbe, 0x46, 0xaf, 0x33, 0x34,
|
||||
0x46, 0x0e, 0x93, 0x6b, 0x92, 0x3d, 0x2f, 0x53, 0xf4, 0x4c, 0x25, 0xa3, 0xb5, 0x7b, 0x17, 0xec,
|
||||
0xf3, 0x8c, 0xac, 0xad, 0xd1, 0xeb, 0x4a, 0x79, 0x8d, 0x49, 0x77, 0xc6, 0xb3, 0xec, 0x2a, 0x11,
|
||||
0x73, 0xcf, 0x52, 0xba, 0x0a, 0xbb, 0xb7, 0xc0, 0x3c, 0x67, 0x8f, 0xbc, 0x9e, 0x14, 0xd3, 0xd2,
|
||||
0xf5, 0xa0, 0x3f, 0xc1, 0x05, 0x2f, 0xa2, 0xdc, 0xeb, 0x0f, 0x8d, 0x91, 0xcd, 0x2a, 0x48, 0x76,
|
||||
0x9e, 0x63, 0x84, 0x4b, 0xc1, 0x17, 0x9e, 0xad, 0xec, 0x54, 0xd8, 0x3d, 0x04, 0xf7, 0x34, 0xce,
|
||||
0x30, 0x28, 0x04, 0x4e, 0x5f, 0x86, 0xe9, 0x05, 0x8a, 0x70, 0x51, 0x7a, 0x8e, 0x34, 0xb0, 0x47,
|
||||
0x43, 0xa7, 0x3c, 0xc6, 0x9c, 0xd3, 0xd9, 0x20, 0x4d, 0x55, 0xd0, 0xf5, 0xe1, 0x60, 0xba, 0xe2,
|
||||
0x02, 0xe7, 0x53, 0x0c, 0x04, 0xe6, 0xde, 0x40, 0xaa, 0xb7, 0x64, 0xb4, 0xe7, 0xa9, 0x58, 0xf2,
|
||||
0x38, 0xfc, 0x96, 0xe7, 0x61, 0x12, 0x7b, 0x07, 0x6a, 0x4f, 0x5b, 0x46, 0x59, 0x62, 0x49, 0x84,
|
||||
0xde, 0x0d, 0x95, 0x25, 0x5a, 0xfb, 0x3f, 0x19, 0xe0, 0x4c, 0x78, 0xb6, 0x9a, 0x25, 0x5c, 0xcc,
|
||||
0x5f, 0x2b, 0xd7, 0xf7, 0xc0, 0x0a, 0x30, 0x8a, 0x32, 0xcf, 0x1c, 0x9a, 0xa3, 0xc1, 0xd1, 0x9d,
|
||||
0xc3, 0xba, 0x88, 0xb5, 0x9d, 0x13, 0x8c, 0x22, 0xa6, 0x76, 0xb9, 0x1f, 0x80, 0x93, 0xe3, 0x3a,
|
||||
0x8d, 0x78, 0x8e, 0x99, 0xd7, 0x95, 0x9f, 0xb8, 0xcd, 0x27, 0xcf, 0xb5, 0x8a, 0x35, 0x9b, 0x76,
|
||||
0x42, 0xb1, 0x76, 0x43, 0xf1, 0x7f, 0xeb, 0xc0, 0x8d, 0xad, 0xe3, 0xdc, 0x03, 0x30, 0x36, 0xd2,
|
||||
0x73, 0x8b, 0x19, 0x1b, 0x42, 0xa5, 0xf4, 0xda, 0x62, 0x46, 0x49, 0xe8, 0x4a, 0x72, 0xc3, 0x62,
|
||||
0xc6, 0x15, 0xa1, 0x95, 0x64, 0x84, 0xc5, 0x8c, 0x95, 0xfb, 0x0e, 0xf4, 0xbf, 0x29, 0x50, 0x84,
|
||||
0x98, 0x79, 0x96, 0xf4, 0xee, 0x5f, 0x8d, 0x77, 0xcf, 0x0a, 0x14, 0x25, 0xab, 0xf4, 0x94, 0x0d,
|
||||
0xc9, 0x26, 0x45, 0x0d, 0xb9, 0x26, 0x59, 0x4e, 0xcc, 0xeb, 0x2b, 0x19, 0xad, 0x75, 0x16, 0x15,
|
||||
0x1f, 0x28, 0x8b, 0x1f, 0x42, 0x97, 0x6f, 0x30, 0xf3, 0x1c, 0x69, 0xff, 0x8d, 0xbf, 0x48, 0xd8,
|
||||
0xe1, 0x78, 0x83, 0xd9, 0xe7, 0x71, 0x2e, 0x4a, 0x26, 0xb7, 0xbb, 0x6f, 0x43, 0x2f, 0x48, 0xa2,
|
||||
0x44, 0x64, 0x1e, 0x5c, 0x77, 0xec, 0x84, 0xe4, 0x4c, 0xab, 0xef, 0x3e, 0x00, 0xa7, 0xfe, 0x96,
|
||||
0xe8, 0xfb, 0x12, 0x4b, 0x99, 0x09, 0x87, 0xd1, 0xd2, 0x7d, 0x13, 0xac, 0x4b, 0x1e, 0x15, 0xaa,
|
||||
0x8a, 0x83, 0xa3, 0x9b, 0x8d, 0x99, 0xf1, 0x26, 0xcc, 0x98, 0x52, 0x7e, 0xdc, 0xf9, 0xc8, 0xf0,
|
||||
0x97, 0x60, 0x49, 0xcb, 0x2d, 0x1e, 0x38, 0x15, 0x0f, 0x64, 0x7f, 0x75, 0x5a, 0xfd, 0x75, 0x0b,
|
||||
0xcc, 0x2f, 0x70, 0xa3, 0x5b, 0x8e, 0x96, 0x35, 0x5b, 0xba, 0x2d, 0xb6, 0xdc, 0x06, 0xeb, 0x42,
|
||||
0x1e, 0xae, 0xaa, 0xa8, 0x80, 0xff, 0xa3, 0x01, 0x5d, 0x3a, 0x9c, 0x6a, 0x1d, 0xe1, 0x92, 0x07,
|
||||
0xe5, 0x71, 0x52, 0xc4, 0xf3, 0xcc, 0x33, 0x86, 0xe6, 0xc8, 0x64, 0x5b, 0x32, 0xf7, 0x3f, 0xd0,
|
||||
0x9b, 0x29, 0x6d, 0x67, 0x68, 0x8e, 0x1c, 0xa6, 0x11, 0x99, 0x8e, 0xf8, 0x0c, 0x23, 0xed, 0x82,
|
||||
0x02, 0xb4, 0x3b, 0x15, 0xb8, 0x08, 0x37, 0xda, 0x0d, 0x8d, 0x48, 0x9e, 0x15, 0x0b, 0x92, 0x2b,
|
||||
0x4f, 0x34, 0x22, 0xa7, 0x67, 0x3c, 0xab, 0x8b, 0x4a, 0x6b, 0xb2, 0x9c, 0x05, 0x3c, 0xaa, 0xaa,
|
||||
0xaa, 0x80, 0xff, 0xb3, 0x41, 0xdd, 0xae, 0x58, 0xba, 0x93, 0xa1, 0xff, 0x82, 0x4d, 0x0c, 0x7e,
|
||||
0x71, 0xc9, 0x85, 0xce, 0x52, 0x9f, 0xf0, 0x05, 0x17, 0xee, 0xfb, 0xd0, 0x93, 0x29, 0xde, 0xd3,
|
||||
0x31, 0x95, 0x39, 0x99, 0x15, 0xa6, 0xb7, 0xd5, 0x9c, 0xea, 0xb6, 0x38, 0x55, 0x07, 0x6b, 0xb5,
|
||||
0x83, 0xbd, 0x07, 0x16, 0x91, 0xb3, 0x94, 0xde, 0xef, 0xb5, 0xac, 0x28, 0xac, 0x76, 0xf9, 0xe7,
|
||||
0x70, 0x63, 0xeb, 0xc4, 0xfa, 0x24, 0x63, 0xfb, 0xa4, 0x86, 0x2e, 0x8e, 0xa6, 0x07, 0x4d, 0xba,
|
||||
0x0c, 0x23, 0x0c, 0x72, 0x9c, 0xcb, 0x7c, 0xdb, 0xac, 0xc6, 0xfe, 0xf7, 0x46, 0x63, 0x57, 0x9e,
|
||||
0x47, 0xb3, 0x2c, 0x48, 0xd6, 0x6b, 0x1e, 0xcf, 0xb5, 0xe9, 0x0a, 0x52, 0xde, 0xe6, 0x33, 0x6d,
|
||||
0xba, 0x33, 0x9f, 0x11, 0x16, 0xa9, 0xae, 0x60, 0x47, 0xa4, 0xee, 0x10, 0x06, 0x6b, 0xe4, 0x59,
|
||||
0x21, 0x70, 0x8d, 0x71, 0xae, 0x53, 0xd0, 0x16, 0xb9, 0x77, 0xa0, 0x9f, 0xf3, 0xe5, 0x0b, 0x22,
|
||||
0xb9, 0xae, 0x64, 0xce, 0x97, 0x0f, 0xb1, 0x74, 0xff, 0x07, 0xce, 0x22, 0xc4, 0x68, 0x2e, 0x55,
|
||||
0xaa, 0x9c, 0xb6, 0x14, 0x3c, 0xc4, 0xd2, 0xff, 0xc5, 0x80, 0xde, 0x14, 0xc5, 0x25, 0x8a, 0xd7,
|
||||
0x1a, 0x72, 0xed, 0xcb, 0xc3, 0x7c, 0xc5, 0xe5, 0xd1, 0xdd, 0x7f, 0x79, 0x58, 0xcd, 0xe5, 0x71,
|
||||
0x1b, 0xac, 0xa9, 0x08, 0x4e, 0x27, 0xd2, 0x23, 0x93, 0x29, 0x40, 0x6c, 0x1c, 0x07, 0x79, 0x78,
|
||||
0x89, 0xfa, 0x46, 0xd1, 0x68, 0x67, 0xf6, 0xd9, 0x7b, 0x66, 0xdf, 0x77, 0x06, 0xf4, 0x1e, 0xf1,
|
||||
0x32, 0x29, 0xf2, 0x1d, 0x16, 0x0e, 0x61, 0x30, 0x4e, 0xd3, 0x28, 0x0c, 0xd4, 0xd7, 0x2a, 0xa2,
|
||||
0xb6, 0x88, 0x76, 0x3c, 0x6e, 0xe5, 0x57, 0xc5, 0xd6, 0x16, 0xd1, 0xb8, 0x38, 0x91, 0xf3, 0x5d,
|
||||
0x0d, 0xeb, 0xd6, 0xb8, 0x50, 0x63, 0x5d, 0x2a, 0x29, 0x09, 0xe3, 0x22, 0x4f, 0x16, 0x51, 0x72,
|
||||
0x25, 0xa3, 0xb5, 0x59, 0x8d, 0xfd, 0x5f, 0x3b, 0xd0, 0xfd, 0xa7, 0x66, 0xf2, 0x01, 0x18, 0xa1,
|
||||
0x2e, 0xb6, 0x11, 0xd6, 0x13, 0xba, 0xdf, 0x9a, 0xd0, 0x1e, 0xf4, 0x4b, 0xc1, 0xe3, 0x25, 0x66,
|
||||
0x9e, 0x2d, 0xa7, 0x4b, 0x05, 0xa5, 0x46, 0xf6, 0x91, 0x1a, 0xcd, 0x0e, 0xab, 0x60, 0xdd, 0x17,
|
||||
0xd0, 0xea, 0x8b, 0xf7, 0xf4, 0x14, 0x1f, 0x48, 0x8f, 0xbc, 0xed, 0xb4, 0x5c, 0x1f, 0xde, 0x7f,
|
||||
0xdf, 0x4c, 0xfe, 0xc3, 0x00, 0xab, 0x6e, 0xaa, 0x93, 0xed, 0xa6, 0x3a, 0x69, 0x9a, 0x6a, 0x72,
|
||||
0x5c, 0x35, 0xd5, 0xe4, 0x98, 0x30, 0x3b, 0xab, 0x9a, 0x8a, 0x9d, 0x51, 0xb1, 0x1e, 0x88, 0xa4,
|
||||
0x48, 0x8f, 0x4b, 0x55, 0x55, 0x87, 0xd5, 0x98, 0x98, 0xf8, 0xd5, 0x0a, 0x85, 0x4e, 0xb5, 0xc3,
|
||||
0x34, 0x22, 0xde, 0x3e, 0x92, 0x03, 0x47, 0x25, 0x57, 0x01, 0xf7, 0x2d, 0xb0, 0x18, 0x25, 0x4f,
|
||||
0x66, 0x78, 0xab, 0x2e, 0x52, 0xcc, 0x94, 0x96, 0x8c, 0xaa, 0xd7, 0x9b, 0x26, 0x70, 0xf5, 0x96,
|
||||
0x7b, 0x17, 0x7a, 0xd3, 0x55, 0xb8, 0xc8, 0xab, 0xbb, 0xf0, 0xdf, 0xad, 0x81, 0x15, 0xae, 0x51,
|
||||
0xea, 0x98, 0xde, 0xe2, 0x3f, 0x03, 0xa7, 0x16, 0x36, 0xee, 0x18, 0x6d, 0x77, 0x5c, 0xe8, 0x9e,
|
||||
0xc7, 0x61, 0x5e, 0xb5, 0x2e, 0xad, 0x29, 0xd8, 0x67, 0x05, 0x8f, 0xf3, 0x30, 0x2f, 0xab, 0xd6,
|
||||
0xad, 0xb0, 0x7f, 0x5f, 0xbb, 0x4f, 0xe6, 0xce, 0xd3, 0x14, 0x85, 0x1e, 0x03, 0x0a, 0xc8, 0x43,
|
||||
0x92, 0x2b, 0x54, 0x13, 0xdc, 0x64, 0x0a, 0xf8, 0x5f, 0x83, 0x33, 0x8e, 0x50, 0xe4, 0xac, 0x88,
|
||||
0x70, 0xdf, 0xcd, 0xf8, 0xe5, 0xf4, 0xe9, 0x93, 0xca, 0x03, 0x5a, 0x37, 0x2d, 0x6f, 0x5e, 0x6b,
|
||||
0xf9, 0x87, 0x3c, 0xe5, 0xa7, 0x13, 0xc9, 0x73, 0x93, 0x69, 0xe4, 0xff, 0x60, 0x40, 0x97, 0x66,
|
||||
0x4b, 0xcb, 0x74, 0xf7, 0x55, 0x73, 0xe9, 0x4c, 0x24, 0x97, 0xe1, 0x1c, 0x45, 0x15, 0x5c, 0x85,
|
||||
0x65, 0xd2, 0x83, 0x15, 0xd6, 0x17, 0xb0, 0x46, 0xc4, 0x35, 0x7a, 0xea, 0x55, 0xbd, 0xd4, 0xe2,
|
||||
0x1a, 0x89, 0x99, 0x52, 0xba, 0xff, 0x07, 0x98, 0x16, 0x29, 0x8a, 0xf1, 0x7c, 0x1d, 0xc6, 0xb2,
|
||||
0xe8, 0x36, 0x6b, 0x49, 0xfc, 0xcf, 0xd4, 0xe3, 0x71, 0x67, 0x42, 0x19, 0xfb, 0x1f, 0x9a, 0xd7,
|
||||
0x3d, 0xf7, 0xa3, 0xed, 0xef, 0xf6, 0x25, 0x72, 0x27, 0xda, 0x21, 0x0c, 0xf4, 0x4b, 0x5b, 0xbe,
|
||||
0x5b, 0xf5, 0xb0, 0x6a, 0x89, 0x28, 0xe6, 0xb3, 0x62, 0x16, 0x85, 0x81, 0x8c, 0xd9, 0x66, 0x1a,
|
||||
0xf9, 0x47, 0xd0, 0x3b, 0x49, 0xe2, 0x45, 0xb8, 0x74, 0x47, 0xd0, 0x1d, 0x17, 0xf9, 0x4a, 0x9e,
|
||||
0x34, 0x38, 0xba, 0xdd, 0x6a, 0xb4, 0x22, 0x5f, 0xa9, 0x3d, 0x4c, 0xee, 0xf0, 0x3f, 0x01, 0x68,
|
||||
0x64, 0xf4, 0x7c, 0x6f, 0xa2, 0x7f, 0x82, 0x57, 0x54, 0xa2, 0x4c, 0x5a, 0xb1, 0xd9, 0x1e, 0x8d,
|
||||
0xff, 0x29, 0x38, 0xc7, 0x45, 0x18, 0xcd, 0x4f, 0xe3, 0x45, 0x42, 0xad, 0x7a, 0x81, 0x22, 0x6b,
|
||||
0xf2, 0x53, 0x41, 0x72, 0x98, 0xba, 0xb6, 0xe6, 0xac, 0x46, 0xb3, 0x9e, 0xfc, 0x03, 0xba, 0xff,
|
||||
0x67, 0x00, 0x00, 0x00, 0xff, 0xff, 0xbe, 0x23, 0x76, 0x8f, 0x13, 0x0d, 0x00, 0x00,
|
||||
// 1379 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x57, 0x5f, 0x8f, 0xdb, 0x44,
|
||||
0x10, 0x97, 0xe3, 0x38, 0x89, 0x27, 0xd7, 0x52, 0x99, 0x8a, 0x9a, 0x22, 0xa1, 0x60, 0x81, 0x08,
|
||||
0x82, 0x1e, 0xe8, 0x2a, 0x24, 0x84, 0xa0, 0x52, 0xee, 0x82, 0xca, 0xd1, 0x6b, 0x7b, 0xdd, 0xdc,
|
||||
0x1d, 0x4f, 0xa8, 0xda, 0x38, 0x93, 0xc4, 0xaa, 0x63, 0x9b, 0xb5, 0x7d, 0x17, 0xf3, 0x61, 0x90,
|
||||
0x90, 0xf8, 0x04, 0x88, 0x77, 0x5e, 0x11, 0x1f, 0x84, 0xaf, 0xc0, 0x13, 0x12, 0x9a, 0xdd, 0xf5,
|
||||
0x9f, 0x5c, 0x42, 0xd5, 0x07, 0xc4, 0xdb, 0xfe, 0x66, 0x36, 0xb3, 0xf3, 0xe7, 0x37, 0x33, 0x0e,
|
||||
0xdc, 0x0c, 0xa2, 0x0c, 0x45, 0xc4, 0xc3, 0xfd, 0x44, 0xc4, 0x59, 0xec, 0xf4, 0x4a, 0xec, 0xfd,
|
||||
0xd9, 0x82, 0xce, 0x24, 0xce, 0x85, 0x8f, 0xce, 0x4d, 0x68, 0x1d, 0x8f, 0x5d, 0x63, 0x60, 0x0c,
|
||||
0x4d, 0xd6, 0x3a, 0x1e, 0x3b, 0x0e, 0xb4, 0x9f, 0xf0, 0x15, 0xba, 0xad, 0x81, 0x31, 0xb4, 0x99,
|
||||
0x3c, 0x93, 0xec, 0xac, 0x48, 0xd0, 0x35, 0x95, 0x8c, 0xce, 0xce, 0x5d, 0xe8, 0x9d, 0xa7, 0x64,
|
||||
0x6d, 0x85, 0x6e, 0x5b, 0xca, 0x2b, 0x4c, 0xba, 0x53, 0x9e, 0xa6, 0x57, 0xb1, 0x98, 0xb9, 0x96,
|
||||
0xd2, 0x95, 0xd8, 0xb9, 0x05, 0xe6, 0x39, 0x3b, 0x71, 0x3b, 0x52, 0x4c, 0x47, 0xc7, 0x85, 0xee,
|
||||
0x18, 0xe7, 0x3c, 0x0f, 0x33, 0xb7, 0x3b, 0x30, 0x86, 0x3d, 0x56, 0x42, 0xb2, 0x73, 0x86, 0x21,
|
||||
0x2e, 0x04, 0x9f, 0xbb, 0x3d, 0x65, 0xa7, 0xc4, 0xce, 0x3e, 0x38, 0xc7, 0x51, 0x8a, 0x7e, 0x2e,
|
||||
0x70, 0xf2, 0x22, 0x48, 0x2e, 0x50, 0x04, 0xf3, 0xc2, 0xb5, 0xa5, 0x81, 0x1d, 0x1a, 0x7a, 0xe5,
|
||||
0x31, 0x66, 0x9c, 0xde, 0x06, 0x69, 0xaa, 0x84, 0x8e, 0x07, 0x7b, 0x93, 0x25, 0x17, 0x38, 0x9b,
|
||||
0xa0, 0x2f, 0x30, 0x73, 0xfb, 0x52, 0xbd, 0x21, 0xa3, 0x3b, 0x4f, 0xc5, 0x82, 0x47, 0xc1, 0x0f,
|
||||
0x3c, 0x0b, 0xe2, 0xc8, 0xdd, 0x53, 0x77, 0x9a, 0x32, 0xca, 0x12, 0x8b, 0x43, 0x74, 0x6f, 0xa8,
|
||||
0x2c, 0xd1, 0xd9, 0xfb, 0xd5, 0x00, 0x7b, 0xcc, 0xd3, 0xe5, 0x34, 0xe6, 0x62, 0xf6, 0x4a, 0xb9,
|
||||
0xbe, 0x07, 0x96, 0x8f, 0x61, 0x98, 0xba, 0xe6, 0xc0, 0x1c, 0xf6, 0x0f, 0xee, 0xec, 0x57, 0x45,
|
||||
0xac, 0xec, 0x1c, 0x61, 0x18, 0x32, 0x75, 0xcb, 0xf9, 0x04, 0xec, 0x0c, 0x57, 0x49, 0xc8, 0x33,
|
||||
0x4c, 0xdd, 0xb6, 0xfc, 0x89, 0x53, 0xff, 0xe4, 0x4c, 0xab, 0x58, 0x7d, 0x69, 0x2b, 0x14, 0x6b,
|
||||
0x3b, 0x14, 0xef, 0xef, 0x16, 0xdc, 0xd8, 0x78, 0xce, 0xd9, 0x03, 0x63, 0x2d, 0x3d, 0xb7, 0x98,
|
||||
0xb1, 0x26, 0x54, 0x48, 0xaf, 0x2d, 0x66, 0x14, 0x84, 0xae, 0x24, 0x37, 0x2c, 0x66, 0x5c, 0x11,
|
||||
0x5a, 0x4a, 0x46, 0x58, 0xcc, 0x58, 0x3a, 0x1f, 0x40, 0xf7, 0xfb, 0x1c, 0x45, 0x80, 0xa9, 0x6b,
|
||||
0x49, 0xef, 0x5e, 0xab, 0xbd, 0x7b, 0x96, 0xa3, 0x28, 0x58, 0xa9, 0xa7, 0x6c, 0x48, 0x36, 0x29,
|
||||
0x6a, 0xc8, 0x33, 0xc9, 0x32, 0x62, 0x5e, 0x57, 0xc9, 0xe8, 0xac, 0xb3, 0xa8, 0xf8, 0x40, 0x59,
|
||||
0xfc, 0x14, 0xda, 0x7c, 0x8d, 0xa9, 0x6b, 0x4b, 0xfb, 0xef, 0xfc, 0x4b, 0xc2, 0xf6, 0x47, 0x6b,
|
||||
0x4c, 0xbf, 0x8a, 0x32, 0x51, 0x30, 0x79, 0xdd, 0x79, 0x1f, 0x3a, 0x7e, 0x1c, 0xc6, 0x22, 0x75,
|
||||
0xe1, 0xba, 0x63, 0x47, 0x24, 0x67, 0x5a, 0xed, 0x0c, 0xa1, 0x13, 0xe2, 0x02, 0xa3, 0x99, 0x64,
|
||||
0x46, 0xff, 0xe0, 0x56, 0x7d, 0xf1, 0x44, 0xca, 0x99, 0xd6, 0xdf, 0x7d, 0x08, 0x76, 0xf5, 0x0a,
|
||||
0x11, 0xfd, 0x05, 0x16, 0x32, 0x67, 0x36, 0xa3, 0xa3, 0xf3, 0x2e, 0x58, 0x97, 0x3c, 0xcc, 0x55,
|
||||
0xbd, 0xfb, 0x07, 0x37, 0x6b, 0x3b, 0xa3, 0x75, 0x90, 0x32, 0xa5, 0xfc, 0xbc, 0xf5, 0x99, 0xe1,
|
||||
0x2d, 0xc0, 0x92, 0x3e, 0x34, 0x18, 0x63, 0x97, 0x8c, 0x91, 0x9d, 0xd8, 0x6a, 0x74, 0xe2, 0x2d,
|
||||
0x30, 0xbf, 0xc6, 0xb5, 0x6e, 0x4e, 0x3a, 0x56, 0xbc, 0x6a, 0x37, 0x78, 0x75, 0x1b, 0xac, 0x0b,
|
||||
0xf9, 0xb8, 0xaa, 0xb7, 0x02, 0xde, 0x03, 0xe8, 0xa8, 0x18, 0x2a, 0xcb, 0x46, 0xc3, 0xf2, 0x00,
|
||||
0xfa, 0x4f, 0x45, 0x80, 0x51, 0xa6, 0x98, 0xa2, 0x1e, 0x6d, 0x8a, 0xbc, 0x5f, 0x0c, 0x68, 0x93,
|
||||
0xf3, 0xc4, 0xaa, 0x10, 0x17, 0xdc, 0x2f, 0x0e, 0xe3, 0x3c, 0x9a, 0xa5, 0xae, 0x31, 0x30, 0x87,
|
||||
0x26, 0xdb, 0x90, 0x39, 0x6f, 0x40, 0x67, 0xaa, 0xb4, 0xad, 0x81, 0x39, 0xb4, 0x99, 0x46, 0xe4,
|
||||
0x5a, 0xc8, 0xa7, 0x18, 0xea, 0x10, 0x14, 0xa0, 0xdb, 0x89, 0xc0, 0x79, 0xb0, 0xd6, 0x61, 0x68,
|
||||
0x44, 0xf2, 0x34, 0x9f, 0x93, 0x5c, 0x45, 0xa2, 0x11, 0x05, 0x30, 0xe5, 0x69, 0x45, 0x1f, 0x3a,
|
||||
0x93, 0xe5, 0xd4, 0xe7, 0x61, 0xc9, 0x1f, 0x05, 0xbc, 0xdf, 0x0c, 0x9a, 0x2b, 0xaa, 0x1f, 0xb6,
|
||||
0x32, 0xfc, 0x26, 0xf4, 0xa8, 0x57, 0x9e, 0x5f, 0x72, 0xa1, 0x03, 0xee, 0x12, 0xbe, 0xe0, 0xc2,
|
||||
0xf9, 0x18, 0x3a, 0xb2, 0x44, 0x3b, 0x7a, 0xb3, 0x34, 0x27, 0xb3, 0xca, 0xf4, 0xb5, 0x8a, 0xbd,
|
||||
0xed, 0x06, 0x7b, 0xab, 0x60, 0xad, 0x66, 0xb0, 0xf7, 0xc0, 0xa2, 0x36, 0x28, 0xa4, 0xf7, 0x3b,
|
||||
0x2d, 0xab, 0x66, 0x51, 0xb7, 0xbc, 0x73, 0xb8, 0xb1, 0xf1, 0x62, 0xf5, 0x92, 0xb1, 0xf9, 0x52,
|
||||
0x4d, 0x37, 0x5b, 0xd3, 0x8b, 0x66, 0x6a, 0x8a, 0x21, 0xfa, 0x19, 0xce, 0x64, 0xbe, 0x7b, 0xac,
|
||||
0xc2, 0xde, 0x4f, 0x46, 0x6d, 0x57, 0xbe, 0x47, 0x53, 0xd3, 0x8f, 0x57, 0x2b, 0x1e, 0xcd, 0xb4,
|
||||
0xe9, 0x12, 0x52, 0xde, 0x66, 0x53, 0x6d, 0xba, 0x35, 0x9b, 0x12, 0x16, 0x89, 0xae, 0x60, 0x4b,
|
||||
0x24, 0xc4, 0x9d, 0x15, 0xf2, 0x34, 0x17, 0xb8, 0xc2, 0x28, 0xd3, 0x29, 0x68, 0x8a, 0x9c, 0x3b,
|
||||
0xd0, 0xcd, 0xf8, 0xe2, 0x39, 0x35, 0x89, 0xae, 0x64, 0xc6, 0x17, 0x8f, 0xb0, 0x70, 0xde, 0x02,
|
||||
0x7b, 0x1e, 0x60, 0x38, 0x93, 0x2a, 0x55, 0xce, 0x9e, 0x14, 0x3c, 0xc2, 0xc2, 0xfb, 0xdd, 0x80,
|
||||
0xce, 0x04, 0xc5, 0x25, 0x8a, 0x57, 0x1a, 0xa7, 0xcd, 0x35, 0x65, 0xbe, 0x64, 0x4d, 0xb5, 0x77,
|
||||
0xaf, 0x29, 0xab, 0x5e, 0x53, 0xb7, 0xc1, 0x9a, 0x08, 0xff, 0x78, 0x2c, 0x3d, 0x32, 0x99, 0x02,
|
||||
0xc4, 0xc6, 0x91, 0x9f, 0x05, 0x97, 0xa8, 0x77, 0x97, 0x46, 0x5b, 0x53, 0xb6, 0xb7, 0x63, 0xca,
|
||||
0xfe, 0x68, 0x40, 0xe7, 0x84, 0x17, 0x71, 0x9e, 0x6d, 0xb1, 0x70, 0x00, 0xfd, 0x51, 0x92, 0x84,
|
||||
0x81, 0xbf, 0xd1, 0x79, 0x0d, 0x11, 0xdd, 0x78, 0xdc, 0xc8, 0xaf, 0x8a, 0xad, 0x29, 0xa2, 0x71,
|
||||
0x73, 0x24, 0x37, 0x89, 0x5a, 0x0b, 0x8d, 0x71, 0xa3, 0x16, 0x88, 0x54, 0x52, 0x12, 0x46, 0x79,
|
||||
0x16, 0xcf, 0xc3, 0xf8, 0x4a, 0x46, 0xdb, 0x63, 0x15, 0xf6, 0xfe, 0x68, 0x41, 0xfb, 0xff, 0x9a,
|
||||
0xfe, 0x7b, 0x60, 0x04, 0xba, 0xd8, 0x46, 0x50, 0xed, 0x82, 0x6e, 0x63, 0x17, 0xb8, 0xd0, 0x2d,
|
||||
0x04, 0x8f, 0x16, 0x98, 0xba, 0x3d, 0x39, 0x5d, 0x4a, 0x28, 0x35, 0xb2, 0x8f, 0xd4, 0x12, 0xb0,
|
||||
0x59, 0x09, 0xab, 0xbe, 0x80, 0x46, 0x5f, 0x7c, 0xa4, 0xf7, 0x45, 0x5f, 0x7a, 0xe4, 0x6e, 0xa6,
|
||||
0xe5, 0xfa, 0x9a, 0xf8, 0xef, 0x66, 0xfa, 0x5f, 0x06, 0x58, 0x55, 0x53, 0x1d, 0x6d, 0x36, 0xd5,
|
||||
0x51, 0xdd, 0x54, 0xe3, 0xc3, 0xb2, 0xa9, 0xc6, 0x87, 0x84, 0xd9, 0x69, 0xd9, 0x54, 0xec, 0x94,
|
||||
0x8a, 0xf5, 0x50, 0xc4, 0x79, 0x72, 0x58, 0xa8, 0xaa, 0xda, 0xac, 0xc2, 0xc4, 0xc4, 0x6f, 0x97,
|
||||
0x28, 0x74, 0xaa, 0x6d, 0xa6, 0x11, 0xf1, 0xf6, 0x44, 0x0e, 0x1c, 0x95, 0x5c, 0x05, 0x9c, 0xf7,
|
||||
0xc0, 0x62, 0x94, 0x3c, 0x99, 0xe1, 0x8d, 0xba, 0x48, 0x31, 0x53, 0x5a, 0x32, 0xaa, 0xbe, 0x13,
|
||||
0x35, 0x81, 0xcb, 0xaf, 0xc6, 0x0f, 0xa1, 0x33, 0x59, 0x06, 0xf3, 0xac, 0xdc, 0xba, 0xaf, 0x37,
|
||||
0x06, 0x56, 0xb0, 0x42, 0xa9, 0x63, 0xfa, 0x8a, 0xf7, 0x0c, 0xec, 0x4a, 0x58, 0xbb, 0x63, 0x34,
|
||||
0xdd, 0x71, 0xa0, 0x7d, 0x1e, 0x05, 0x59, 0xd9, 0xba, 0x74, 0xa6, 0x60, 0x9f, 0xe5, 0x3c, 0xca,
|
||||
0x82, 0xac, 0x28, 0x5b, 0xb7, 0xc4, 0xde, 0x7d, 0xed, 0x3e, 0x99, 0x3b, 0x4f, 0x12, 0x14, 0x7a,
|
||||
0x0c, 0x28, 0x20, 0x1f, 0x89, 0xaf, 0x50, 0x4d, 0x70, 0x93, 0x29, 0xe0, 0x7d, 0x07, 0xf6, 0x28,
|
||||
0x44, 0x91, 0xb1, 0x3c, 0xc4, 0x5d, 0x9b, 0xf5, 0x9b, 0xc9, 0xd3, 0x27, 0xa5, 0x07, 0x74, 0xae,
|
||||
0x5b, 0xde, 0xbc, 0xd6, 0xf2, 0x8f, 0x78, 0xc2, 0x8f, 0xc7, 0x92, 0xe7, 0x26, 0xd3, 0xc8, 0xfb,
|
||||
0xd9, 0x80, 0x36, 0xcd, 0x96, 0x86, 0xe9, 0xf6, 0xcb, 0xe6, 0xd2, 0xa9, 0x88, 0x2f, 0x83, 0x19,
|
||||
0x8a, 0x32, 0xb8, 0x12, 0xcb, 0xa4, 0xfb, 0x4b, 0xac, 0x16, 0xb8, 0x46, 0xc4, 0x35, 0xfa, 0xa8,
|
||||
0x2c, 0x7b, 0xa9, 0xc1, 0x35, 0x12, 0x33, 0xa5, 0x74, 0xde, 0x06, 0x98, 0xe4, 0x09, 0x8a, 0xd1,
|
||||
0x6c, 0x15, 0x44, 0xb2, 0xe8, 0x3d, 0xd6, 0x90, 0x78, 0x0f, 0xd4, 0x67, 0xea, 0xd6, 0x84, 0x32,
|
||||
0x76, 0x7f, 0xd2, 0x5e, 0xf7, 0xdc, 0x0b, 0x37, 0x7f, 0xb7, 0x2b, 0x91, 0x5b, 0xd1, 0x0e, 0xa0,
|
||||
0xaf, 0xbf, 0xe9, 0xe5, 0x17, 0xb2, 0x1e, 0x56, 0x0d, 0x11, 0xc5, 0x7c, 0x9a, 0x4f, 0xc3, 0xc0,
|
||||
0x97, 0x31, 0xf7, 0x98, 0x46, 0xde, 0x01, 0x74, 0x8e, 0xe2, 0x68, 0x1e, 0x2c, 0x9c, 0x21, 0xb4,
|
||||
0x47, 0x79, 0xb6, 0x94, 0x2f, 0xf5, 0x0f, 0x6e, 0x37, 0x1a, 0x2d, 0xcf, 0x96, 0xea, 0x0e, 0x93,
|
||||
0x37, 0xbc, 0x2f, 0x00, 0x6a, 0x19, 0xfd, 0x51, 0xa8, 0xa3, 0x7f, 0x82, 0x57, 0x54, 0xa2, 0x54,
|
||||
0x5a, 0xe9, 0xb1, 0x1d, 0x1a, 0xef, 0x4b, 0xb0, 0x0f, 0xf3, 0x20, 0x9c, 0x1d, 0x47, 0xf3, 0x98,
|
||||
0x5a, 0xf5, 0x02, 0x45, 0x5a, 0xe7, 0xa7, 0x84, 0xe4, 0x30, 0x75, 0x6d, 0xc5, 0x59, 0x8d, 0xa6,
|
||||
0x1d, 0xf9, 0x5f, 0xeb, 0xfe, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x0f, 0x64, 0xa4, 0xff, 0x7d,
|
||||
0x0d, 0x00, 0x00,
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ message DashboardCell {
|
|||
string ID = 8; // id is the unique id of the dashboard. MIGRATED FIELD added in 1.2.0-beta6
|
||||
map<string, Axis> axes = 9; // Axes represent the graphical viewport for a cell's visualizations
|
||||
repeated Color colors = 10; // Colors represent encoding data values to color
|
||||
Legend legend = 11; // Legend is summary information for a cell
|
||||
}
|
||||
|
||||
message Color {
|
||||
|
@ -46,6 +47,11 @@ message Color {
|
|||
string Value = 5; // Value is the data value mapped to this color
|
||||
}
|
||||
|
||||
message Legend {
|
||||
string Type = 1; // Type is how the legend is used
|
||||
string Orientation = 2; // Orientation is the location of the legend on the cell
|
||||
}
|
||||
|
||||
message Axis {
|
||||
repeated int64 legacyBounds = 1; // legacyBounds are an ordered 2-tuple consisting of lower and upper axis extents, respectively
|
||||
repeated string bounds = 2; // bounds are an arbitrary list of client-defined bounds.
|
||||
|
|
|
@ -251,6 +251,10 @@ func Test_MarshalDashboard_WithLegacyBounds(t *testing.T) {
|
|||
Value: "100",
|
||||
},
|
||||
},
|
||||
Legend: chronograf.Legend{
|
||||
Type: "static",
|
||||
Orientation: "bottom",
|
||||
},
|
||||
Type: "line",
|
||||
},
|
||||
},
|
||||
|
@ -301,6 +305,10 @@ func Test_MarshalDashboard_WithLegacyBounds(t *testing.T) {
|
|||
Value: "100",
|
||||
},
|
||||
},
|
||||
Legend: chronograf.Legend{
|
||||
Type: "static",
|
||||
Orientation: "bottom",
|
||||
},
|
||||
Type: "line",
|
||||
},
|
||||
},
|
||||
|
|
|
@ -25,6 +25,9 @@ const (
|
|||
ErrInvalidAxis = Error("Unexpected axis in cell. Valid axes are 'x', 'y', and 'y2'")
|
||||
ErrInvalidColorType = Error("Invalid color type. Valid color types are 'min', 'max', 'threshold', 'text', and 'background'")
|
||||
ErrInvalidColor = Error("Invalid color. Accepted color format is #RRGGBB")
|
||||
ErrInvalidLegend = Error("Invalid legend. Both type and orientation must be set")
|
||||
ErrInvalidLegendType = Error("Invalid legend type. Valid legend type is 'static'")
|
||||
ErrInvalidLegendOrient = Error("Invalid orientation type. Valid orientation types are 'top', 'bottom', 'right', 'left'")
|
||||
ErrUserAlreadyExists = Error("user already exists")
|
||||
ErrOrganizationNotFound = Error("organization not found")
|
||||
ErrOrganizationAlreadyExists = Error("organization already exists")
|
||||
|
@ -531,6 +534,12 @@ type CellColor struct {
|
|||
Value string `json:"value"` // Value is the data value mapped to this color
|
||||
}
|
||||
|
||||
// Legend represents the encoding of data into a legend
|
||||
type Legend struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Orientation string `json:"orientation,omitempty"`
|
||||
}
|
||||
|
||||
// DashboardCell holds visual and query information for a cell
|
||||
type DashboardCell struct {
|
||||
ID string `json:"i"`
|
||||
|
@ -543,6 +552,7 @@ type DashboardCell struct {
|
|||
Axes map[string]Axis `json:"axes"`
|
||||
Type string `json:"type"`
|
||||
CellColors []CellColor `json:"colors"`
|
||||
Legend Legend `json:"legend"`
|
||||
}
|
||||
|
||||
// DashboardsStore is the storage and retrieval of dashboards
|
||||
|
|
|
@ -3,7 +3,7 @@ machine:
|
|||
services:
|
||||
- docker
|
||||
environment:
|
||||
DOCKER_TAG: chronograf-20171027
|
||||
DOCKER_TAG: chronograf-20180207
|
||||
|
||||
dependencies:
|
||||
override:
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
type AddCommand struct {
|
||||
BoltPath string `short:"b" long:"bolt-path" description:"Full path to boltDB file (e.g. './chronograf-v1.db')" env:"BOLT_PATH" default:"chronograf-v1.db"`
|
||||
ID *uint64 `short:"i" long:"id" description:"Users ID. Must be id for existing user"`
|
||||
Username string `short:"n" long:"name" description:"Users name. Must be Oauth-able email address or username"`
|
||||
Provider string `short:"p" long:"provider" description:"Name of the Auth provider (e.g. google, github, auth0, or generic)"`
|
||||
Scheme string `short:"s" long:"scheme" description:"Authentication scheme that matches auth provider (e.g. oauth or ldap)"`
|
||||
//Organizations string `short:"o" long:"orgs" description:"A comma separated list of organizations that the user should be added to"`
|
||||
}
|
||||
|
||||
var addCommand AddCommand
|
||||
|
||||
func (l *AddCommand) Execute(args []string) error {
|
||||
c, err := NewBoltClient(l.BoltPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
q := chronograf.UserQuery{
|
||||
Name: &l.Username,
|
||||
Provider: &l.Provider,
|
||||
Scheme: &l.Scheme,
|
||||
}
|
||||
|
||||
if l.ID != nil {
|
||||
q.ID = l.ID
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
user, err := c.UsersStore.Get(ctx, q)
|
||||
if err != nil && err != chronograf.ErrUserNotFound {
|
||||
return err
|
||||
} else if err == chronograf.ErrUserNotFound {
|
||||
user = &chronograf.User{
|
||||
Name: l.Username,
|
||||
Provider: l.Provider,
|
||||
Scheme: l.Scheme,
|
||||
SuperAdmin: true,
|
||||
}
|
||||
|
||||
user, err = c.UsersStore.Add(ctx, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
user.SuperAdmin = true
|
||||
if err = c.UsersStore.Update(ctx, user); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(desa): Apply mapping to user and update their roles
|
||||
// TODO(desa): Add a flag that allows the user to specify an organization to join
|
||||
|
||||
w := NewTabWriter()
|
||||
WriteHeaders(w)
|
||||
WriteUser(w, user)
|
||||
w.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
parser.AddCommand("add-superadmin",
|
||||
"Creates a new superadmin user",
|
||||
"The add-user command will create a new user with superadmin status",
|
||||
&addCommand)
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type ListCommand struct {
|
||||
BoltPath string `short:"b" long:"bolt-path" description:"Full path to boltDB file (e.g. './chronograf-v1.db')" env:"BOLT_PATH" default:"chronograf-v1.db"`
|
||||
}
|
||||
|
||||
var listCommand ListCommand
|
||||
|
||||
func (l *ListCommand) Execute(args []string) error {
|
||||
c, err := NewBoltClient(l.BoltPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
users, err := c.UsersStore.All(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w := NewTabWriter()
|
||||
WriteHeaders(w)
|
||||
for _, user := range users {
|
||||
WriteUser(w, &user)
|
||||
}
|
||||
w.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
parser.AddCommand("list-users",
|
||||
"Lists users",
|
||||
"The list-users command will list all users in the chronograf boltdb instance",
|
||||
&listCommand)
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/jessevdk/go-flags"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
}
|
||||
|
||||
var options Options
|
||||
|
||||
var parser = flags.NewParser(&options, flags.Default)
|
||||
|
||||
func main() {
|
||||
if _, err := parser.Parse(); err != nil {
|
||||
if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp {
|
||||
os.Exit(0)
|
||||
} else {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/bolt"
|
||||
"github.com/influxdata/chronograf/mocks"
|
||||
)
|
||||
|
||||
func NewBoltClient(path string) (*bolt.Client, error) {
|
||||
c := bolt.NewClient()
|
||||
c.Path = path
|
||||
|
||||
ctx := context.Background()
|
||||
logger := mocks.NewLogger()
|
||||
var bi chronograf.BuildInfo
|
||||
if err := c.Open(ctx, logger, bi); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func NewTabWriter() *tabwriter.Writer {
|
||||
return tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
|
||||
}
|
||||
|
||||
func WriteHeaders(w io.Writer) {
|
||||
fmt.Fprintln(w, "ID\tName\tProvider\tScheme\tSuperAdmin\tOrganization(s)")
|
||||
}
|
||||
|
||||
func WriteUser(w io.Writer, user *chronograf.User) {
|
||||
orgs := []string{}
|
||||
for _, role := range user.Roles {
|
||||
orgs = append(orgs, role.Organization)
|
||||
}
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%t\t%s\n", user.ID, user.Name, user.Provider, user.Scheme, user.SuperAdmin, strings.Join(orgs, ","))
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
VERSION ?= $(shell git describe --always --tags)
|
||||
COMMIT ?= $(shell git rev-parse --short=8 HEAD)
|
||||
GDM := $(shell command -v gdm 2> /dev/null)
|
||||
GOBINDATA := $(shell go list -f {{.Root}} github.com/jteeuwen/go-bindata 2> /dev/null)
|
||||
GOBINDATA := $(shell go list -f {{.Root}} github.com/kevinburke/go-bindata 2> /dev/null)
|
||||
YARN := $(shell command -v yarn 2> /dev/null)
|
||||
|
||||
SOURCES := $(shell find . -name '*.go' ! -name '*_gen.go')
|
||||
|
@ -63,7 +63,7 @@ ifndef GDM
|
|||
endif
|
||||
ifndef GOBINDATA
|
||||
@echo "Installing go-bindata"
|
||||
go get -u github.com/jteeuwen/go-bindata/...
|
||||
go get -u github.com/kevinburke/go-bindata/...
|
||||
endif
|
||||
gdm restore
|
||||
@touch .godep
|
||||
|
|
|
@ -12,13 +12,15 @@ RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y \
|
|||
ruby-dev \
|
||||
rpm \
|
||||
zip \
|
||||
python-pip
|
||||
python-pip \
|
||||
autoconf \
|
||||
libtool
|
||||
|
||||
RUN pip install boto requests python-jose --upgrade
|
||||
RUN gem install fpm
|
||||
|
||||
# Install node
|
||||
ENV NODE_VERSION v6.11.5
|
||||
ENV NODE_VERSION v6.12.3
|
||||
RUN wget -q https://nodejs.org/dist/latest-v6.x/node-${NODE_VERSION}-linux-x64.tar.gz; \
|
||||
tar -xvf node-${NODE_VERSION}-linux-x64.tar.gz -C / --strip-components=1; \
|
||||
rm -f node-${NODE_VERSION}-linux-x64.tar.gz
|
||||
|
@ -35,7 +37,7 @@ RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
|
|||
|
||||
# Install go
|
||||
ENV GOPATH /root/go
|
||||
ENV GO_VERSION 1.9.2
|
||||
ENV GO_VERSION 1.9.4
|
||||
ENV GO_ARCH amd64
|
||||
RUN wget https://storage.googleapis.com/golang/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz; \
|
||||
tar -C /usr/local/ -xf /go${GO_VERSION}.linux-${GO_ARCH}.tar.gz ; \
|
||||
|
|
|
@ -10,3 +10,6 @@ After updating the Dockerfile_build run
|
|||
|
||||
and push to quay with:
|
||||
`docker push quay.io/influxdb/builder:chronograf-$(date "+%Y%m%d")`
|
||||
|
||||
### Update circle
|
||||
Update DOCKER_TAG in circle.yml to the new container.
|
||||
|
|
|
@ -24,6 +24,7 @@ DATA_DIR = "/var/lib/chronograf"
|
|||
SCRIPT_DIR = "/usr/lib/chronograf/scripts"
|
||||
LOGROTATE_DIR = "/etc/logrotate.d"
|
||||
CANNED_DIR = "/usr/share/chronograf/canned"
|
||||
RESOURCES_DIR = "/usr/share/chronograf/resources"
|
||||
|
||||
INIT_SCRIPT = "etc/scripts/init.sh"
|
||||
SYSTEMD_SCRIPT = "etc/scripts/chronograf.service"
|
||||
|
@ -115,7 +116,8 @@ def create_package_fs(build_root):
|
|||
DATA_DIR[1:],
|
||||
SCRIPT_DIR[1:],
|
||||
LOGROTATE_DIR[1:],
|
||||
CANNED_DIR[1:]
|
||||
CANNED_DIR[1:],
|
||||
RESOURCES_DIR[1:]
|
||||
]
|
||||
for d in dirs:
|
||||
os.makedirs(os.path.join(build_root, d))
|
||||
|
|
|
@ -8,8 +8,12 @@ After=network-online.target
|
|||
[Service]
|
||||
User=chronograf
|
||||
Group=chronograf
|
||||
Environment="HOST=0.0.0.0"
|
||||
Environment="PORT=8888"
|
||||
Environment="BOLT_PATH=/var/lib/chronograf/chronograf-v1.db"
|
||||
Environment="CANNED_PATH=/usr/share/chronograf/canned"
|
||||
EnvironmentFile=-/etc/default/chronograf
|
||||
ExecStart=/usr/bin/chronograf --host 0.0.0.0 --port 8888 -b /var/lib/chronograf/chronograf-v1.db -c /usr/share/chronograf/canned $CHRONOGRAF_OPTS
|
||||
ExecStart=/usr/bin/chronograf $CHRONOGRAF_OPTS
|
||||
KillMode=control-group
|
||||
Restart=on-failure
|
||||
|
||||
|
|
|
@ -12,9 +12,13 @@
|
|||
|
||||
# Script to execute when starting
|
||||
SCRIPT="/usr/bin/chronograf"
|
||||
export HOST="0.0.0.0"
|
||||
export PORT="8888"
|
||||
export BOLT_PATH="/var/lib/chronograf/chronograf-v1.db"
|
||||
export CANNED_PATH="/usr/share/chronograf/canned"
|
||||
# Options to pass to the script on startup
|
||||
. /etc/default/chronograf
|
||||
SCRIPT_OPTS="--host 0.0.0.0 --port 8888 -b /var/lib/chronograf/chronograf-v1.db -c /usr/share/chronograf/canned ${CHRONOGRAF_OPTS}"
|
||||
SCRIPT_OPTS="${CHRONOGRAF_OPTS}"
|
||||
|
||||
# User to run the process under
|
||||
RUNAS=chronograf
|
||||
|
|
|
@ -539,6 +539,10 @@ func TestServer(t *testing.T) {
|
|||
"value": "100"
|
||||
}
|
||||
],
|
||||
"legend":{
|
||||
"type": "static",
|
||||
"orientation": "bottom"
|
||||
},
|
||||
"links": {
|
||||
"self": "/chronograf/v1/dashboards/1000/cells/8f61c619-dd9b-4761-8aa8-577f27247093"
|
||||
}
|
||||
|
@ -778,6 +782,10 @@ func TestServer(t *testing.T) {
|
|||
"value": "100"
|
||||
}
|
||||
],
|
||||
"legend":{
|
||||
"type": "static",
|
||||
"orientation": "bottom"
|
||||
},
|
||||
"links": {
|
||||
"self": "/chronograf/v1/dashboards/1000/cells/8f61c619-dd9b-4761-8aa8-577f27247093"
|
||||
}
|
||||
|
@ -901,7 +909,7 @@ func TestServer(t *testing.T) {
|
|||
GithubClientSecret: "not empty",
|
||||
},
|
||||
method: "GET",
|
||||
path: "/chronograf/v1/users",
|
||||
path: "/chronograf/v1/organizations/default/users",
|
||||
principal: oauth2.Principal{
|
||||
Organization: "default",
|
||||
Subject: "billibob",
|
||||
|
@ -933,6 +941,284 @@ func TestServer(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
server: &server.Server{
|
||||
GithubClientID: "not empty",
|
||||
GithubClientSecret: "not empty",
|
||||
},
|
||||
method: "GET",
|
||||
path: "/chronograf/v1/organizations/default/users",
|
||||
principal: oauth2.Principal{
|
||||
Organization: "default",
|
||||
Subject: "billibob",
|
||||
Issuer: "github",
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: 200,
|
||||
body: `
|
||||
{
|
||||
"links": {
|
||||
"self": "/chronograf/v1/organizations/default/users"
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"links": {
|
||||
"self": "/chronograf/v1/organizations/default/users/1"
|
||||
},
|
||||
"id": "1",
|
||||
"name": "billibob",
|
||||
"provider": "github",
|
||||
"scheme": "oauth2",
|
||||
"superAdmin": true,
|
||||
"roles": [
|
||||
{
|
||||
"name": "admin",
|
||||
"organization": "default"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GET /users",
|
||||
subName: "Two users in two organizations; user making request is as SuperAdmin with out raw query param",
|
||||
fields: fields{
|
||||
Users: []chronograf.User{
|
||||
{
|
||||
ID: 1, // This is artificial, but should be reflective of the users actual ID
|
||||
Name: "billibob",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: "admin",
|
||||
Organization: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 2, // This is artificial, but should be reflective of the users actual ID
|
||||
Name: "billietta",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: "admin",
|
||||
Organization: "cool",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
server: &server.Server{
|
||||
GithubClientID: "not empty",
|
||||
GithubClientSecret: "not empty",
|
||||
},
|
||||
method: "GET",
|
||||
path: "/chronograf/v1/organizations/default/users",
|
||||
principal: oauth2.Principal{
|
||||
Organization: "default",
|
||||
Subject: "billibob",
|
||||
Issuer: "github",
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: 200,
|
||||
body: `
|
||||
{
|
||||
"links": {
|
||||
"self": "/chronograf/v1/organizations/default/users"
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"links": {
|
||||
"self": "/chronograf/v1/organizations/default/users/1"
|
||||
},
|
||||
"id": "1",
|
||||
"name": "billibob",
|
||||
"provider": "github",
|
||||
"scheme": "oauth2",
|
||||
"superAdmin": true,
|
||||
"roles": [
|
||||
{
|
||||
"name": "admin",
|
||||
"organization": "default"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "POST /users",
|
||||
subName: "User making request is as SuperAdmin with raw query param; being created has wildcard role",
|
||||
fields: fields{
|
||||
Users: []chronograf.User{
|
||||
{
|
||||
ID: 1, // This is artificial, but should be reflective of the users actual ID
|
||||
Name: "billibob",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: "admin",
|
||||
Organization: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
server: &server.Server{
|
||||
GithubClientID: "not empty",
|
||||
GithubClientSecret: "not empty",
|
||||
},
|
||||
payload: &chronograf.User{
|
||||
Name: "user",
|
||||
Provider: "provider",
|
||||
Scheme: "oauth2",
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: "*",
|
||||
Organization: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
method: "POST",
|
||||
path: "/chronograf/v1/users",
|
||||
principal: oauth2.Principal{
|
||||
Organization: "default",
|
||||
Subject: "billibob",
|
||||
Issuer: "github",
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: 201,
|
||||
body: `
|
||||
{
|
||||
"links": {
|
||||
"self": "/chronograf/v1/users/2"
|
||||
},
|
||||
"id": "2",
|
||||
"name": "user",
|
||||
"provider": "provider",
|
||||
"scheme": "oauth2",
|
||||
"superAdmin": false,
|
||||
"roles": [
|
||||
{
|
||||
"name": "member",
|
||||
"organization": "default"
|
||||
}
|
||||
]
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "POST /users",
|
||||
subName: "User making request is as SuperAdmin with raw query param; being created has no roles",
|
||||
fields: fields{
|
||||
Users: []chronograf.User{
|
||||
{
|
||||
ID: 1, // This is artificial, but should be reflective of the users actual ID
|
||||
Name: "billibob",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: "admin",
|
||||
Organization: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
server: &server.Server{
|
||||
GithubClientID: "not empty",
|
||||
GithubClientSecret: "not empty",
|
||||
},
|
||||
payload: &chronograf.User{
|
||||
Name: "user",
|
||||
Provider: "provider",
|
||||
Scheme: "oauth2",
|
||||
Roles: []chronograf.Role{},
|
||||
},
|
||||
method: "POST",
|
||||
path: "/chronograf/v1/users",
|
||||
principal: oauth2.Principal{
|
||||
Organization: "default",
|
||||
Subject: "billibob",
|
||||
Issuer: "github",
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: 201,
|
||||
body: `
|
||||
{
|
||||
"links": {
|
||||
"self": "/chronograf/v1/users/2"
|
||||
},
|
||||
"id": "2",
|
||||
"name": "user",
|
||||
"provider": "provider",
|
||||
"scheme": "oauth2",
|
||||
"superAdmin": false,
|
||||
"roles": []
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GET /users",
|
||||
subName: "Two users in two organizations; user making request is as SuperAdmin with raw query param",
|
||||
fields: fields{
|
||||
Organizations: []chronograf.Organization{
|
||||
{
|
||||
ID: "1",
|
||||
Name: "cool",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
},
|
||||
},
|
||||
Users: []chronograf.User{
|
||||
{
|
||||
ID: 1, // This is artificial, but should be reflective of the users actual ID
|
||||
Name: "billibob",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: "admin",
|
||||
Organization: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 2, // This is artificial, but should be reflective of the users actual ID
|
||||
Name: "billietta",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: "admin",
|
||||
Organization: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
server: &server.Server{
|
||||
GithubClientID: "not empty",
|
||||
|
@ -969,9 +1255,93 @@ func TestServer(t *testing.T) {
|
|||
"organization": "default"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"links": {
|
||||
"self": "/chronograf/v1/users/2"
|
||||
},
|
||||
"id": "2",
|
||||
"name": "billietta",
|
||||
"provider": "github",
|
||||
"scheme": "oauth2",
|
||||
"superAdmin": true,
|
||||
"roles": [
|
||||
{
|
||||
"name": "admin",
|
||||
"organization": "1"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`,
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GET /users",
|
||||
subName: "Two users in two organizations; user making request is as not SuperAdmin with raw query param",
|
||||
fields: fields{
|
||||
Organizations: []chronograf.Organization{
|
||||
{
|
||||
ID: "1",
|
||||
Name: "cool",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
},
|
||||
},
|
||||
Users: []chronograf.User{
|
||||
{
|
||||
ID: 1, // This is artificial, but should be reflective of the users actual ID
|
||||
Name: "billibob",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: "admin",
|
||||
Organization: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 2, // This is artificial, but should be reflective of the users actual ID
|
||||
Name: "billietta",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: false,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: "admin",
|
||||
Organization: "default",
|
||||
},
|
||||
{
|
||||
Name: "admin",
|
||||
Organization: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
server: &server.Server{
|
||||
GithubClientID: "not empty",
|
||||
GithubClientSecret: "not empty",
|
||||
},
|
||||
method: "GET",
|
||||
path: "/chronograf/v1/users",
|
||||
principal: oauth2.Principal{
|
||||
Organization: "default",
|
||||
Subject: "billieta",
|
||||
Issuer: "github",
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: 403,
|
||||
body: `
|
||||
{
|
||||
"code": 403,
|
||||
"message": "User is not authorized"
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -1005,7 +1375,7 @@ func TestServer(t *testing.T) {
|
|||
GithubClientSecret: "not empty",
|
||||
},
|
||||
method: "POST",
|
||||
path: "/chronograf/v1/users",
|
||||
path: "/chronograf/v1/organizations/default/users",
|
||||
payload: &chronograf.User{
|
||||
Name: "user",
|
||||
Provider: "provider",
|
||||
|
@ -1028,7 +1398,7 @@ func TestServer(t *testing.T) {
|
|||
body: `
|
||||
{
|
||||
"links": {
|
||||
"self": "/chronograf/v1/users/2"
|
||||
"self": "/chronograf/v1/organizations/default/users/2"
|
||||
},
|
||||
"id": "2",
|
||||
"name": "user",
|
||||
|
@ -1075,7 +1445,7 @@ func TestServer(t *testing.T) {
|
|||
GithubClientSecret: "not empty",
|
||||
},
|
||||
method: "POST",
|
||||
path: "/chronograf/v1/users",
|
||||
path: "/chronograf/v1/organizations/default/users",
|
||||
payload: &chronograf.User{
|
||||
Name: "user",
|
||||
Provider: "provider",
|
||||
|
@ -1098,7 +1468,7 @@ func TestServer(t *testing.T) {
|
|||
body: `
|
||||
{
|
||||
"links": {
|
||||
"self": "/chronograf/v1/users/2"
|
||||
"self": "/chronograf/v1/organizations/default/users/2"
|
||||
},
|
||||
"id": "2",
|
||||
"name": "user",
|
||||
|
@ -1145,7 +1515,7 @@ func TestServer(t *testing.T) {
|
|||
GithubClientSecret: "not empty",
|
||||
},
|
||||
method: "POST",
|
||||
path: "/chronograf/v1/users",
|
||||
path: "/chronograf/v1/organizations/default/users",
|
||||
payload: &chronograf.User{
|
||||
Name: "user",
|
||||
Provider: "provider",
|
||||
|
@ -1168,7 +1538,7 @@ func TestServer(t *testing.T) {
|
|||
body: `
|
||||
{
|
||||
"links": {
|
||||
"self": "/chronograf/v1/users/2"
|
||||
"self": "/chronograf/v1/organizations/default/users/2"
|
||||
},
|
||||
"id": "2",
|
||||
"name": "user",
|
||||
|
@ -1215,7 +1585,7 @@ func TestServer(t *testing.T) {
|
|||
GithubClientSecret: "not empty",
|
||||
},
|
||||
method: "POST",
|
||||
path: "/chronograf/v1/users",
|
||||
path: "/chronograf/v1/organizations/default/users",
|
||||
payload: &chronograf.User{
|
||||
Name: "user",
|
||||
Provider: "provider",
|
||||
|
@ -1240,6 +1610,246 @@ func TestServer(t *testing.T) {
|
|||
{
|
||||
"code": 401,
|
||||
"message": "User does not have authorization required to set SuperAdmin status. See https://github.com/influxdata/chronograf/issues/2601 for more information."
|
||||
}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "POST /users",
|
||||
subName: "Create a New User with in multiple organizations; User on Principal is a SuperAdmin with raw query param",
|
||||
fields: fields{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: true,
|
||||
},
|
||||
},
|
||||
Organizations: []chronograf.Organization{
|
||||
{
|
||||
ID: "1",
|
||||
Name: "cool",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
},
|
||||
},
|
||||
Users: []chronograf.User{
|
||||
{
|
||||
ID: 1, // This is artificial, but should be reflective of the users actual ID
|
||||
Name: "billibob",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: "admin",
|
||||
Organization: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
server: &server.Server{
|
||||
GithubClientID: "not empty",
|
||||
GithubClientSecret: "not empty",
|
||||
},
|
||||
method: "POST",
|
||||
path: "/chronograf/v1/users",
|
||||
payload: &chronograf.User{
|
||||
Name: "user",
|
||||
Provider: "provider",
|
||||
Scheme: "oauth2",
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: roles.EditorRoleName,
|
||||
Organization: "default",
|
||||
},
|
||||
{
|
||||
Name: roles.EditorRoleName,
|
||||
Organization: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
principal: oauth2.Principal{
|
||||
Organization: "default",
|
||||
Subject: "billibob",
|
||||
Issuer: "github",
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: 201,
|
||||
body: `
|
||||
{
|
||||
"links": {
|
||||
"self": "/chronograf/v1/users/2"
|
||||
},
|
||||
"id": "2",
|
||||
"name": "user",
|
||||
"provider": "provider",
|
||||
"scheme": "oauth2",
|
||||
"superAdmin": true,
|
||||
"roles": [
|
||||
{
|
||||
"name": "editor",
|
||||
"organization": "default"
|
||||
},
|
||||
{
|
||||
"name": "editor",
|
||||
"organization": "1"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "PATCH /users",
|
||||
subName: "Update user to have no roles",
|
||||
fields: fields{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: true,
|
||||
},
|
||||
},
|
||||
Organizations: []chronograf.Organization{
|
||||
{
|
||||
ID: "1",
|
||||
Name: "cool",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
},
|
||||
},
|
||||
Users: []chronograf.User{
|
||||
{
|
||||
ID: 1, // This is artificial, but should be reflective of the users actual ID
|
||||
Name: "billibob",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: "admin",
|
||||
Organization: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
server: &server.Server{
|
||||
GithubClientID: "not empty",
|
||||
GithubClientSecret: "not empty",
|
||||
},
|
||||
method: "PATCH",
|
||||
path: "/chronograf/v1/users/1",
|
||||
payload: map[string]interface{}{
|
||||
"name": "billibob",
|
||||
"provider": "github",
|
||||
"scheme": "oauth2",
|
||||
"superAdmin": true,
|
||||
"roles": []chronograf.Role{},
|
||||
},
|
||||
principal: oauth2.Principal{
|
||||
Organization: "default",
|
||||
Subject: "billibob",
|
||||
Issuer: "github",
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: 200,
|
||||
body: `
|
||||
{
|
||||
"links": {
|
||||
"self": "/chronograf/v1/users/1"
|
||||
},
|
||||
"id": "1",
|
||||
"name": "billibob",
|
||||
"provider": "github",
|
||||
"scheme": "oauth2",
|
||||
"superAdmin": true,
|
||||
"roles": [
|
||||
]
|
||||
}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "PATCH /users",
|
||||
subName: "Update user roles with wildcard",
|
||||
fields: fields{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: true,
|
||||
},
|
||||
},
|
||||
Organizations: []chronograf.Organization{
|
||||
{
|
||||
ID: "1",
|
||||
Name: "cool",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
},
|
||||
},
|
||||
Users: []chronograf.User{
|
||||
{
|
||||
ID: 1, // This is artificial, but should be reflective of the users actual ID
|
||||
Name: "billibob",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: "admin",
|
||||
Organization: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
server: &server.Server{
|
||||
GithubClientID: "not empty",
|
||||
GithubClientSecret: "not empty",
|
||||
},
|
||||
method: "PATCH",
|
||||
path: "/chronograf/v1/users/1",
|
||||
payload: &chronograf.User{
|
||||
Name: "billibob",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: roles.AdminRoleName,
|
||||
Organization: "default",
|
||||
},
|
||||
{
|
||||
Name: roles.WildcardRoleName,
|
||||
Organization: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
principal: oauth2.Principal{
|
||||
Organization: "default",
|
||||
Subject: "billibob",
|
||||
Issuer: "github",
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: 200,
|
||||
body: `
|
||||
{
|
||||
"links": {
|
||||
"self": "/chronograf/v1/users/1"
|
||||
},
|
||||
"id": "1",
|
||||
"name": "billibob",
|
||||
"provider": "github",
|
||||
"scheme": "oauth2",
|
||||
"superAdmin": true,
|
||||
"roles": [
|
||||
{
|
||||
"name": "admin",
|
||||
"organization": "default"
|
||||
},
|
||||
{
|
||||
"name": "viewer",
|
||||
"organization": "1"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
},
|
||||
},
|
||||
|
@ -1269,7 +1879,7 @@ func TestServer(t *testing.T) {
|
|||
GithubClientSecret: "not empty",
|
||||
},
|
||||
method: "PATCH",
|
||||
path: "/chronograf/v1/users/1",
|
||||
path: "/chronograf/v1/organizations/default/users/1",
|
||||
payload: map[string]interface{}{
|
||||
"id": "1",
|
||||
"superAdmin": false,
|
||||
|
@ -1293,6 +1903,74 @@ func TestServer(t *testing.T) {
|
|||
"code": 401,
|
||||
"message": "user cannot modify their own SuperAdmin status"
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GET /organization/default/users",
|
||||
subName: "Organization not set explicitly on principal",
|
||||
fields: fields{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
Organizations: []chronograf.Organization{},
|
||||
Users: []chronograf.User{
|
||||
{
|
||||
ID: 1, // This is artificial, but should be reflective of the users actual ID
|
||||
Name: "billibob",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: "admin",
|
||||
Organization: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
server: &server.Server{
|
||||
GithubClientID: "not empty",
|
||||
GithubClientSecret: "not empty",
|
||||
},
|
||||
method: "GET",
|
||||
path: "/chronograf/v1/organizations/default/users",
|
||||
principal: oauth2.Principal{
|
||||
Organization: "",
|
||||
Subject: "billibob",
|
||||
Issuer: "github",
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: 200,
|
||||
body: `
|
||||
{
|
||||
"links": {
|
||||
"self": "/chronograf/v1/organizations/default/users"
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"links": {
|
||||
"self": "/chronograf/v1/organizations/default/users/1"
|
||||
},
|
||||
"id": "1",
|
||||
"name": "billibob",
|
||||
"provider": "github",
|
||||
"scheme": "oauth2",
|
||||
"superAdmin": true,
|
||||
"roles": [
|
||||
{
|
||||
"name": "admin",
|
||||
"organization": "default"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
|
@ -1364,7 +2042,7 @@ func TestServer(t *testing.T) {
|
|||
"scheme": "oauth2",
|
||||
"superAdmin": true,
|
||||
"links": {
|
||||
"self": "/chronograf/v1/users/1"
|
||||
"self": "/chronograf/v1/organizations/1/users/1"
|
||||
},
|
||||
"organizations": [
|
||||
{
|
||||
|
@ -1446,6 +2124,168 @@ func TestServer(t *testing.T) {
|
|||
}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GET /",
|
||||
subName: "signed into default org",
|
||||
fields: fields{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: true,
|
||||
},
|
||||
},
|
||||
Organizations: []chronograf.Organization{
|
||||
{
|
||||
ID: "1",
|
||||
Name: "cool",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
},
|
||||
},
|
||||
Users: []chronograf.User{
|
||||
{
|
||||
ID: 1, // This is artificial, but should be reflective of the users actual ID
|
||||
Name: "billibob",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: "admin",
|
||||
Organization: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
server: &server.Server{
|
||||
GithubClientID: "not empty",
|
||||
GithubClientSecret: "not empty",
|
||||
},
|
||||
method: "GET",
|
||||
path: "/chronograf/v1/",
|
||||
principal: oauth2.Principal{
|
||||
Organization: "default",
|
||||
Subject: "billibob",
|
||||
Issuer: "github",
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: 200,
|
||||
body: `
|
||||
{
|
||||
"layouts": "/chronograf/v1/layouts",
|
||||
"users": "/chronograf/v1/organizations/default/users",
|
||||
"allUsers": "/chronograf/v1/users",
|
||||
"organizations": "/chronograf/v1/organizations",
|
||||
"mappings": "/chronograf/v1/mappings",
|
||||
"sources": "/chronograf/v1/sources",
|
||||
"me": "/chronograf/v1/me",
|
||||
"environment": "/chronograf/v1/env",
|
||||
"dashboards": "/chronograf/v1/dashboards",
|
||||
"config": {
|
||||
"self": "/chronograf/v1/config",
|
||||
"auth": "/chronograf/v1/config/auth"
|
||||
},
|
||||
"auth": [
|
||||
{
|
||||
"name": "github",
|
||||
"label": "Github",
|
||||
"login": "/oauth/github/login",
|
||||
"logout": "/oauth/github/logout",
|
||||
"callback": "/oauth/github/callback"
|
||||
}
|
||||
],
|
||||
"logout": "/oauth/logout",
|
||||
"external": {
|
||||
"statusFeed": ""
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GET /",
|
||||
subName: "signed into org 1",
|
||||
fields: fields{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: true,
|
||||
},
|
||||
},
|
||||
Organizations: []chronograf.Organization{
|
||||
{
|
||||
ID: "1",
|
||||
Name: "cool",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
},
|
||||
},
|
||||
Users: []chronograf.User{
|
||||
{
|
||||
ID: 1, // This is artificial, but should be reflective of the users actual ID
|
||||
Name: "billibob",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: false,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: "admin",
|
||||
Organization: "default",
|
||||
},
|
||||
{
|
||||
Name: "member",
|
||||
Organization: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
server: &server.Server{
|
||||
GithubClientID: "not empty",
|
||||
GithubClientSecret: "not empty",
|
||||
},
|
||||
method: "GET",
|
||||
path: "/chronograf/v1/",
|
||||
principal: oauth2.Principal{
|
||||
Organization: "1",
|
||||
Subject: "billibob",
|
||||
Issuer: "github",
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: 200,
|
||||
body: `
|
||||
{
|
||||
"layouts": "/chronograf/v1/layouts",
|
||||
"users": "/chronograf/v1/organizations/1/users",
|
||||
"allUsers": "/chronograf/v1/users",
|
||||
"organizations": "/chronograf/v1/organizations",
|
||||
"mappings": "/chronograf/v1/mappings",
|
||||
"sources": "/chronograf/v1/sources",
|
||||
"me": "/chronograf/v1/me",
|
||||
"environment": "/chronograf/v1/env",
|
||||
"dashboards": "/chronograf/v1/dashboards",
|
||||
"config": {
|
||||
"self": "/chronograf/v1/config",
|
||||
"auth": "/chronograf/v1/config/auth"
|
||||
},
|
||||
"auth": [
|
||||
{
|
||||
"name": "github",
|
||||
"label": "Github",
|
||||
"login": "/oauth/github/login",
|
||||
"logout": "/oauth/github/logout",
|
||||
"callback": "/oauth/github/callback"
|
||||
}
|
||||
],
|
||||
"logout": "/oauth/logout",
|
||||
"external": {
|
||||
"statusFeed": ""
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
|
@ -86,7 +86,11 @@
|
|||
"name": "comet",
|
||||
"value": "100"
|
||||
}
|
||||
]
|
||||
],
|
||||
"legend": {
|
||||
"type": "static",
|
||||
"orientation": "bottom"
|
||||
}
|
||||
}
|
||||
],
|
||||
"templates": [
|
||||
|
|
|
@ -1293,7 +1293,157 @@ func TestClient_Create(t *testing.T) {
|
|||
createTaskOptions *client.CreateTaskOptions
|
||||
}{
|
||||
{
|
||||
name: "create alert rule",
|
||||
name: "create alert rule with tags",
|
||||
fields: fields{
|
||||
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
|
||||
return kapa, nil
|
||||
},
|
||||
Ticker: &Alert{},
|
||||
ID: &MockID{
|
||||
ID: "howdy",
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
rule: chronograf.AlertRule{
|
||||
ID: "howdy",
|
||||
Name: "myname's",
|
||||
Query: &chronograf.QueryConfig{
|
||||
Database: "db",
|
||||
RetentionPolicy: "rp",
|
||||
Measurement: "meas",
|
||||
GroupBy: chronograf.GroupBy{
|
||||
Tags: []string{
|
||||
"tag1",
|
||||
"tag2",
|
||||
},
|
||||
},
|
||||
},
|
||||
Trigger: Deadman,
|
||||
TriggerValues: chronograf.TriggerValues{
|
||||
Period: "1d",
|
||||
},
|
||||
},
|
||||
},
|
||||
resTask: client.Task{
|
||||
ID: "chronograf-v1-howdy",
|
||||
Status: client.Enabled,
|
||||
Type: client.StreamTask,
|
||||
DBRPs: []client.DBRP{
|
||||
{
|
||||
Database: "db",
|
||||
RetentionPolicy: "rp",
|
||||
},
|
||||
},
|
||||
Link: client.Link{
|
||||
Href: "/kapacitor/v1/tasks/chronograf-v1-howdy",
|
||||
},
|
||||
},
|
||||
createTaskOptions: &client.CreateTaskOptions{
|
||||
TICKscript: `var db = 'db'
|
||||
|
||||
var rp = 'rp'
|
||||
|
||||
var measurement = 'meas'
|
||||
|
||||
var groupBy = ['tag1', 'tag2']
|
||||
|
||||
var whereFilter = lambda: TRUE
|
||||
|
||||
var period = 1d
|
||||
|
||||
var name = 'myname\'s'
|
||||
|
||||
var idVar = name + ':{{.Group}}'
|
||||
|
||||
var message = ''
|
||||
|
||||
var idTag = 'alertID'
|
||||
|
||||
var levelTag = 'level'
|
||||
|
||||
var messageField = 'message'
|
||||
|
||||
var durationField = 'duration'
|
||||
|
||||
var outputDB = 'chronograf'
|
||||
|
||||
var outputRP = 'autogen'
|
||||
|
||||
var outputMeasurement = 'alerts'
|
||||
|
||||
var triggerType = 'deadman'
|
||||
|
||||
var threshold = 0.0
|
||||
|
||||
var data = stream
|
||||
|from()
|
||||
.database(db)
|
||||
.retentionPolicy(rp)
|
||||
.measurement(measurement)
|
||||
.groupBy(groupBy)
|
||||
.where(whereFilter)
|
||||
|
||||
var trigger = data
|
||||
|deadman(threshold, period)
|
||||
.stateChangesOnly()
|
||||
.message(message)
|
||||
.id(idVar)
|
||||
.idTag(idTag)
|
||||
.levelTag(levelTag)
|
||||
.messageField(messageField)
|
||||
.durationField(durationField)
|
||||
|
||||
trigger
|
||||
|eval(lambda: "emitted")
|
||||
.as('value')
|
||||
.keep('value', messageField, durationField)
|
||||
|eval(lambda: float("value"))
|
||||
.as('value')
|
||||
.keep()
|
||||
|influxDBOut()
|
||||
.create()
|
||||
.database(outputDB)
|
||||
.retentionPolicy(outputRP)
|
||||
.measurement(outputMeasurement)
|
||||
.tag('alertName', name)
|
||||
.tag('triggerType', triggerType)
|
||||
|
||||
trigger
|
||||
|httpOut('output')
|
||||
`,
|
||||
|
||||
ID: "chronograf-v1-howdy",
|
||||
Type: client.StreamTask,
|
||||
Status: client.Enabled,
|
||||
DBRPs: []client.DBRP{
|
||||
{
|
||||
Database: "db",
|
||||
RetentionPolicy: "rp",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &Task{
|
||||
ID: "chronograf-v1-howdy",
|
||||
Href: "/kapacitor/v1/tasks/chronograf-v1-howdy",
|
||||
HrefOutput: "/kapacitor/v1/tasks/chronograf-v1-howdy/output",
|
||||
Rule: chronograf.AlertRule{
|
||||
Type: "stream",
|
||||
DBRPs: []chronograf.DBRP{
|
||||
{
|
||||
|
||||
DB: "db",
|
||||
RP: "rp",
|
||||
},
|
||||
},
|
||||
Status: "enabled",
|
||||
ID: "chronograf-v1-howdy",
|
||||
Name: "chronograf-v1-howdy",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create alert rule with no tags",
|
||||
fields: fields{
|
||||
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
|
||||
return kapa, nil
|
||||
|
@ -1348,7 +1498,7 @@ var period = 1d
|
|||
|
||||
var name = 'myname\'s'
|
||||
|
||||
var idVar = name + ':{{.Group}}'
|
||||
var idVar = name
|
||||
|
||||
var message = ''
|
||||
|
||||
|
|
|
@ -123,7 +123,7 @@ func commonVars(rule chronograf.AlertRule) (string, error) {
|
|||
%s
|
||||
|
||||
var name = '%s'
|
||||
var idVar = name + ':{{.Group}}'
|
||||
var idVar = %s
|
||||
var message = '%s'
|
||||
var idTag = '%s'
|
||||
var levelTag = '%s'
|
||||
|
@ -143,6 +143,7 @@ func commonVars(rule chronograf.AlertRule) (string, error) {
|
|||
whereFilter(rule.Query),
|
||||
wind,
|
||||
Escape(rule.Name),
|
||||
idVar(rule.Query),
|
||||
Escape(rule.Message),
|
||||
IDTag,
|
||||
LevelTag,
|
||||
|
@ -197,6 +198,13 @@ func groupBy(q *chronograf.QueryConfig) string {
|
|||
return "[" + strings.Join(groups, ",") + "]"
|
||||
}
|
||||
|
||||
func idVar(q *chronograf.QueryConfig) string {
|
||||
if len(q.GroupBy.Tags) > 0 {
|
||||
return `name + ':{{.Group}}'`
|
||||
}
|
||||
return "name"
|
||||
}
|
||||
|
||||
func field(q *chronograf.QueryConfig) (string, error) {
|
||||
if q == nil {
|
||||
return "", fmt.Errorf("No fields set in query")
|
||||
|
|
|
@ -38,6 +38,9 @@ const (
|
|||
EditorRoleName = "editor"
|
||||
AdminRoleName = "admin"
|
||||
SuperAdminStatus = "superadmin"
|
||||
|
||||
// Indicatior that the server should retrieve the default role for the organization.
|
||||
WildcardRoleName = "*"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -11,6 +11,13 @@ import (
|
|||
"github.com/influxdata/chronograf/roles"
|
||||
)
|
||||
|
||||
// HasAuthorizedToken extracts the token from a request and validates it using the authenticator.
|
||||
// It is used by routes that need access to the token to populate links request.
|
||||
func HasAuthorizedToken(auth oauth2.Authenticator, r *http.Request) (oauth2.Principal, error) {
|
||||
ctx := r.Context()
|
||||
return auth.Validate(ctx, r)
|
||||
}
|
||||
|
||||
// AuthorizedToken extracts the token and validates; if valid the next handler
|
||||
// will be run. The principal will be sent to the next handler via the request's
|
||||
// Context. It is up to the next handler to determine if the principal has access.
|
||||
|
@ -49,6 +56,33 @@ func AuthorizedToken(auth oauth2.Authenticator, logger chronograf.Logger, next h
|
|||
})
|
||||
}
|
||||
|
||||
// RawStoreAccess gives a super admin access to the data store without a facade.
|
||||
func RawStoreAccess(logger chronograf.Logger, next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if isServer := hasServerContext(ctx); isServer {
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
log := logger.
|
||||
WithField("component", "raw_store").
|
||||
WithField("remote_addr", r.RemoteAddr).
|
||||
WithField("method", r.Method).
|
||||
WithField("url", r.URL)
|
||||
|
||||
if isSuperAdmin := hasSuperAdminContext(ctx); isSuperAdmin {
|
||||
r = r.WithContext(serverContext(ctx))
|
||||
} else {
|
||||
log.Error("User making request is not a SuperAdmin")
|
||||
Error(w, http.StatusForbidden, "User is not authorized", logger)
|
||||
return
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// AuthorizedUser extracts the user name and provider from context. If the
|
||||
// user and provider can be found on the context, we look up the user by their
|
||||
// name and provider. If the user is found, we verify that the user has at at
|
||||
|
@ -181,6 +215,13 @@ func hasAuthorizedRole(u *chronograf.User, role string) bool {
|
|||
}
|
||||
|
||||
switch role {
|
||||
case roles.MemberRoleName:
|
||||
for _, r := range u.Roles {
|
||||
switch r.Name {
|
||||
case roles.MemberRoleName, roles.ViewerRoleName, roles.EditorRoleName, roles.AdminRoleName:
|
||||
return true
|
||||
}
|
||||
}
|
||||
case roles.ViewerRoleName:
|
||||
for _, r := range u.Roles {
|
||||
switch r.Name {
|
||||
|
|
|
@ -108,6 +108,230 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
hasServerContext: true,
|
||||
authorized: true,
|
||||
},
|
||||
{
|
||||
name: "User with member role is member authorized",
|
||||
fields: fields{
|
||||
UsersStore: &mocks.UsersStore{
|
||||
GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
|
||||
if q.Name == nil || q.Provider == nil || q.Scheme == nil {
|
||||
return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme")
|
||||
}
|
||||
return &chronograf.User{
|
||||
ID: 1337,
|
||||
Name: "billysteve",
|
||||
Provider: "google",
|
||||
Scheme: "oauth2",
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: roles.MemberRoleName,
|
||||
Organization: "1337",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
if q.ID == nil {
|
||||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
Logger: clog.New(clog.DebugLevel),
|
||||
},
|
||||
args: args{
|
||||
principal: &oauth2.Principal{
|
||||
Subject: "billysteve",
|
||||
Issuer: "google",
|
||||
Organization: "1337",
|
||||
},
|
||||
scheme: "oauth2",
|
||||
role: "member",
|
||||
useAuth: true,
|
||||
},
|
||||
authorized: true,
|
||||
hasOrganizationContext: true,
|
||||
hasSuperAdminContext: false,
|
||||
hasRoleContext: true,
|
||||
hasServerContext: false,
|
||||
},
|
||||
{
|
||||
name: "User with viewer role is member authorized",
|
||||
fields: fields{
|
||||
UsersStore: &mocks.UsersStore{
|
||||
GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
|
||||
if q.Name == nil || q.Provider == nil || q.Scheme == nil {
|
||||
return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme")
|
||||
}
|
||||
return &chronograf.User{
|
||||
ID: 1337,
|
||||
Name: "billysteve",
|
||||
Provider: "google",
|
||||
Scheme: "oauth2",
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: roles.ViewerRoleName,
|
||||
Organization: "1337",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
if q.ID == nil {
|
||||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
Logger: clog.New(clog.DebugLevel),
|
||||
},
|
||||
args: args{
|
||||
principal: &oauth2.Principal{
|
||||
Subject: "billysteve",
|
||||
Issuer: "google",
|
||||
Organization: "1337",
|
||||
},
|
||||
scheme: "oauth2",
|
||||
role: "member",
|
||||
useAuth: true,
|
||||
},
|
||||
authorized: true,
|
||||
hasOrganizationContext: true,
|
||||
hasSuperAdminContext: false,
|
||||
hasRoleContext: true,
|
||||
hasServerContext: false,
|
||||
},
|
||||
{
|
||||
name: "User with editor role is member authorized",
|
||||
fields: fields{
|
||||
UsersStore: &mocks.UsersStore{
|
||||
GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
|
||||
if q.Name == nil || q.Provider == nil || q.Scheme == nil {
|
||||
return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme")
|
||||
}
|
||||
return &chronograf.User{
|
||||
ID: 1337,
|
||||
Name: "billysteve",
|
||||
Provider: "google",
|
||||
Scheme: "oauth2",
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: roles.EditorRoleName,
|
||||
Organization: "1337",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
if q.ID == nil {
|
||||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
Logger: clog.New(clog.DebugLevel),
|
||||
},
|
||||
args: args{
|
||||
principal: &oauth2.Principal{
|
||||
Subject: "billysteve",
|
||||
Issuer: "google",
|
||||
Organization: "1337",
|
||||
},
|
||||
scheme: "oauth2",
|
||||
role: "member",
|
||||
useAuth: true,
|
||||
},
|
||||
authorized: true,
|
||||
hasOrganizationContext: true,
|
||||
hasSuperAdminContext: false,
|
||||
hasRoleContext: true,
|
||||
hasServerContext: false,
|
||||
},
|
||||
{
|
||||
name: "User with admin role is member authorized",
|
||||
fields: fields{
|
||||
UsersStore: &mocks.UsersStore{
|
||||
GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
|
||||
if q.Name == nil || q.Provider == nil || q.Scheme == nil {
|
||||
return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme")
|
||||
}
|
||||
return &chronograf.User{
|
||||
ID: 1337,
|
||||
Name: "billysteve",
|
||||
Provider: "google",
|
||||
Scheme: "oauth2",
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: roles.AdminRoleName,
|
||||
Organization: "1337",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
if q.ID == nil {
|
||||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
Logger: clog.New(clog.DebugLevel),
|
||||
},
|
||||
args: args{
|
||||
principal: &oauth2.Principal{
|
||||
Subject: "billysteve",
|
||||
Issuer: "google",
|
||||
Organization: "1337",
|
||||
},
|
||||
scheme: "oauth2",
|
||||
role: "member",
|
||||
useAuth: true,
|
||||
},
|
||||
authorized: true,
|
||||
hasOrganizationContext: true,
|
||||
hasSuperAdminContext: false,
|
||||
hasRoleContext: true,
|
||||
hasServerContext: false,
|
||||
},
|
||||
{
|
||||
name: "User with viewer role is viewer authorized",
|
||||
fields: fields{
|
||||
|
@ -1606,3 +1830,118 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawStoreAccess(t *testing.T) {
|
||||
type fields struct {
|
||||
Logger chronograf.Logger
|
||||
}
|
||||
type args struct {
|
||||
principal *oauth2.Principal
|
||||
serverContext bool
|
||||
user *chronograf.User
|
||||
}
|
||||
type wants struct {
|
||||
authorized bool
|
||||
hasServerContext bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "middleware already has server context",
|
||||
fields: fields{
|
||||
Logger: clog.New(clog.DebugLevel),
|
||||
},
|
||||
args: args{
|
||||
serverContext: true,
|
||||
},
|
||||
wants: wants{
|
||||
authorized: true,
|
||||
hasServerContext: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user on context is a SuperAdmin",
|
||||
fields: fields{
|
||||
Logger: clog.New(clog.DebugLevel),
|
||||
},
|
||||
args: args{
|
||||
user: &chronograf.User{
|
||||
SuperAdmin: true,
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
authorized: true,
|
||||
hasServerContext: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user on context is a not SuperAdmin",
|
||||
fields: fields{
|
||||
Logger: clog.New(clog.DebugLevel),
|
||||
},
|
||||
args: args{
|
||||
user: &chronograf.User{
|
||||
SuperAdmin: false,
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
authorized: false,
|
||||
hasServerContext: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var authorized bool
|
||||
var hasServerCtx bool
|
||||
next := func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
hasServerCtx = hasServerContext(ctx)
|
||||
authorized = true
|
||||
}
|
||||
fn := RawStoreAccess(
|
||||
tt.fields.Logger,
|
||||
next,
|
||||
)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
url := "http://any.url"
|
||||
r := httptest.NewRequest(
|
||||
"GET",
|
||||
url,
|
||||
nil,
|
||||
)
|
||||
if tt.args.principal == nil {
|
||||
r = r.WithContext(context.WithValue(r.Context(), oauth2.PrincipalKey, nil))
|
||||
} else {
|
||||
r = r.WithContext(context.WithValue(r.Context(), oauth2.PrincipalKey, *tt.args.principal))
|
||||
}
|
||||
|
||||
if tt.args.serverContext {
|
||||
r = r.WithContext(serverContext(r.Context()))
|
||||
}
|
||||
if tt.args.user != nil {
|
||||
r = r.WithContext(context.WithValue(r.Context(), UserContextKey, tt.args.user))
|
||||
}
|
||||
fn(w, r)
|
||||
|
||||
if authorized != tt.wants.authorized {
|
||||
t.Errorf("%q. RawStoreAccess() = %v, expected %v", tt.name, authorized, tt.wants.authorized)
|
||||
}
|
||||
|
||||
if !authorized && w.Code != http.StatusForbidden {
|
||||
t.Errorf("%q. RawStoreAccess() Status Code = %v, expected %v", tt.name, w.Code, http.StatusForbidden)
|
||||
}
|
||||
|
||||
if hasServerCtx != tt.wants.hasServerContext {
|
||||
t.Errorf("%q. RawStoreAccess().Context().Server = %v, expected %v", tt.name, hasServerCtx, tt.wants.hasServerContext)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,37 +28,31 @@ type dashboardCellResponse struct {
|
|||
|
||||
func newCellResponse(dID chronograf.DashboardID, cell chronograf.DashboardCell) dashboardCellResponse {
|
||||
base := "/chronograf/v1/dashboards"
|
||||
newCell := chronograf.DashboardCell{}
|
||||
newCell.Queries = make([]chronograf.DashboardQuery, len(cell.Queries))
|
||||
copy(newCell.Queries, cell.Queries)
|
||||
|
||||
newCell.CellColors = make([]chronograf.CellColor, len(cell.CellColors))
|
||||
copy(newCell.CellColors, cell.CellColors)
|
||||
|
||||
// ensure x, y, and y2 axes always returned
|
||||
labels := []string{"x", "y", "y2"}
|
||||
newCell.Axes = make(map[string]chronograf.Axis, len(labels))
|
||||
|
||||
newCell.X = cell.X
|
||||
newCell.Y = cell.Y
|
||||
newCell.W = cell.W
|
||||
newCell.H = cell.H
|
||||
newCell.Name = cell.Name
|
||||
newCell.ID = cell.ID
|
||||
newCell.Type = cell.Type
|
||||
|
||||
for _, lbl := range labels {
|
||||
if axis, found := cell.Axes[lbl]; !found {
|
||||
newCell.Axes[lbl] = chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
}
|
||||
} else {
|
||||
newCell.Axes[lbl] = axis
|
||||
}
|
||||
if cell.Queries == nil {
|
||||
cell.Queries = []chronograf.DashboardQuery{}
|
||||
}
|
||||
if cell.CellColors == nil {
|
||||
cell.CellColors = []chronograf.CellColor{}
|
||||
}
|
||||
|
||||
// Copy to handle race condition
|
||||
newAxes := make(map[string]chronograf.Axis, len(cell.Axes))
|
||||
for k, v := range cell.Axes {
|
||||
newAxes[k] = v
|
||||
}
|
||||
|
||||
// ensure x, y, and y2 axes always returned
|
||||
for _, lbl := range []string{"x", "y", "y2"} {
|
||||
if _, found := newAxes[lbl]; !found {
|
||||
newAxes[lbl] = chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
}
|
||||
}
|
||||
}
|
||||
cell.Axes = newAxes
|
||||
|
||||
return dashboardCellResponse{
|
||||
DashboardCell: newCell,
|
||||
DashboardCell: cell,
|
||||
Links: dashboardCellLinks{
|
||||
Self: fmt.Sprintf("%s/%d/cells/%s", base, dID, cell.ID),
|
||||
},
|
||||
|
@ -91,7 +85,10 @@ func ValidDashboardCellRequest(c *chronograf.DashboardCell) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return HasCorrectColors(c)
|
||||
if err = HasCorrectColors(c); err != nil {
|
||||
return err
|
||||
}
|
||||
return HasCorrectLegend(c)
|
||||
}
|
||||
|
||||
// HasCorrectAxes verifies that only permitted axes exist within a DashboardCell
|
||||
|
@ -126,6 +123,27 @@ func HasCorrectColors(c *chronograf.DashboardCell) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// HasCorrectLegend verifies that the format of the legend is correct
|
||||
func HasCorrectLegend(c *chronograf.DashboardCell) error {
|
||||
// No legend set
|
||||
if c.Legend.Type == "" && c.Legend.Orientation == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.Legend.Type == "" || c.Legend.Orientation == "" {
|
||||
return chronograf.ErrInvalidLegend
|
||||
}
|
||||
if !oneOf(c.Legend.Orientation, "top", "bottom", "right", "left") {
|
||||
return chronograf.ErrInvalidLegendOrient
|
||||
}
|
||||
|
||||
// Remember! if we add other types, update ErrInvalidLegendType
|
||||
if !oneOf(c.Legend.Type, "static") {
|
||||
return chronograf.ErrInvalidLegendType
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// oneOf reports whether a provided string is a member of a variadic list of
|
||||
// valid options
|
||||
func oneOf(prop string, validOpts ...string) bool {
|
||||
|
|
|
@ -532,7 +532,7 @@ func TestService_ReplaceDashboardCell(t *testing.T) {
|
|||
}
|
||||
}
|
||||
`))),
|
||||
want: `{"i":"3c5c4102-fa40-4585-a8f9-917c77e37192","x":0,"y":0,"w":4,"h":4,"name":"Untitled Cell","queries":[{"query":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","queryConfig":{"id":"3cd3eaa4-a4b8-44b3-b69e-0c7bf6b91d9e","database":"telegraf","measurement":"cpu","retentionPolicy":"autogen","fields":[{"value":"mean","type":"func","alias":"mean_usage_user","args":[{"value":"usage_user","type":"field","alias":""}]}],"tags":{"cpu":["ChristohersMBP2.lan"]},"groupBy":{"time":"2s","tags":[]},"areTagsAccepted":true,"fill":"null","rawText":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","range":{"upper":"","lower":"now() - 15m"},"shifts":[]},"source":""}],"axes":{"x":{"bounds":[],"label":"","prefix":"","suffix":"","base":"","scale":""},"y":{"bounds":[],"label":"","prefix":"","suffix":"","base":"","scale":""},"y2":{"bounds":[],"label":"","prefix":"","suffix":"","base":"","scale":""}},"type":"line","colors":[{"id":"0","type":"min","hex":"#00C9FF","name":"laser","value":"0"},{"id":"1","type":"max","hex":"#9394FF","name":"comet","value":"100"}],"links":{"self":"/chronograf/v1/dashboards/1/cells/3c5c4102-fa40-4585-a8f9-917c77e37192"}}
|
||||
want: `{"i":"3c5c4102-fa40-4585-a8f9-917c77e37192","x":0,"y":0,"w":4,"h":4,"name":"Untitled Cell","queries":[{"query":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","queryConfig":{"id":"3cd3eaa4-a4b8-44b3-b69e-0c7bf6b91d9e","database":"telegraf","measurement":"cpu","retentionPolicy":"autogen","fields":[{"value":"mean","type":"func","alias":"mean_usage_user","args":[{"value":"usage_user","type":"field","alias":""}]}],"tags":{"cpu":["ChristohersMBP2.lan"]},"groupBy":{"time":"2s","tags":[]},"areTagsAccepted":true,"fill":"null","rawText":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","range":{"upper":"","lower":"now() - 15m"},"shifts":[]},"source":""}],"axes":{"x":{"bounds":[],"label":"","prefix":"","suffix":"","base":"","scale":""},"y":{"bounds":[],"label":"","prefix":"","suffix":"","base":"","scale":""},"y2":{"bounds":[],"label":"","prefix":"","suffix":"","base":"","scale":""}},"type":"line","colors":[{"id":"0","type":"min","hex":"#00C9FF","name":"laser","value":"0"},{"id":"1","type":"max","hex":"#9394FF","name":"comet","value":"100"}],"legend":{},"links":{"self":"/chronograf/v1/dashboards/1/cells/3c5c4102-fa40-4585-a8f9-917c77e37192"}}
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@ -695,7 +695,7 @@ func Test_newCellResponses(t *testing.T) {
|
|||
want []dashboardCellResponse
|
||||
}{
|
||||
{
|
||||
name: "foo",
|
||||
name: "all fields set",
|
||||
dID: chronograf.DashboardID(1),
|
||||
dcells: []chronograf.DashboardCell{
|
||||
chronograf.DashboardCell{
|
||||
|
@ -752,6 +752,10 @@ func Test_newCellResponses(t *testing.T) {
|
|||
chronograf.CellColor{ID: "0", Type: "min", Hex: "#00C9FF", Name: "laser", Value: "0"},
|
||||
chronograf.CellColor{ID: "1", Type: "max", Hex: "#9394FF", Name: "comet", Value: "100"},
|
||||
},
|
||||
Legend: chronograf.Legend{
|
||||
Type: "static",
|
||||
Orientation: "bottom",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []dashboardCellResponse{
|
||||
|
@ -817,6 +821,50 @@ func Test_newCellResponses(t *testing.T) {
|
|||
Value: "100",
|
||||
},
|
||||
},
|
||||
Legend: chronograf.Legend{
|
||||
Type: "static",
|
||||
Orientation: "bottom",
|
||||
},
|
||||
},
|
||||
Links: dashboardCellLinks{
|
||||
Self: "/chronograf/v1/dashboards/1/cells/445f8dc0-4d73-4168-8477-f628690d18a3"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nothing set",
|
||||
dID: chronograf.DashboardID(1),
|
||||
dcells: []chronograf.DashboardCell{
|
||||
chronograf.DashboardCell{
|
||||
ID: "445f8dc0-4d73-4168-8477-f628690d18a3",
|
||||
X: 0,
|
||||
Y: 0,
|
||||
W: 4,
|
||||
H: 4,
|
||||
Name: "Untitled Cell",
|
||||
},
|
||||
},
|
||||
want: []dashboardCellResponse{
|
||||
{
|
||||
DashboardCell: chronograf.DashboardCell{
|
||||
ID: "445f8dc0-4d73-4168-8477-f628690d18a3",
|
||||
W: 4,
|
||||
H: 4,
|
||||
Name: "Untitled Cell",
|
||||
Queries: []chronograf.DashboardQuery{},
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"x": chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
},
|
||||
"y": chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
},
|
||||
"y2": chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
},
|
||||
},
|
||||
CellColors: []chronograf.CellColor{},
|
||||
Legend: chronograf.Legend{},
|
||||
},
|
||||
Links: dashboardCellLinks{
|
||||
Self: "/chronograf/v1/dashboards/1/cells/445f8dc0-4d73-4168-8477-f628690d18a3"},
|
||||
|
@ -832,3 +880,97 @@ func Test_newCellResponses(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasCorrectLegend(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
c *chronograf.DashboardCell
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty legend is ok",
|
||||
c: &chronograf.DashboardCell{},
|
||||
},
|
||||
{
|
||||
name: "must have both an orientation and type",
|
||||
c: &chronograf.DashboardCell{
|
||||
Legend: chronograf.Legend{
|
||||
Type: "static",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "must have both a type and orientation",
|
||||
c: &chronograf.DashboardCell{
|
||||
Legend: chronograf.Legend{
|
||||
Orientation: "bottom",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid types",
|
||||
c: &chronograf.DashboardCell{
|
||||
Legend: chronograf.Legend{
|
||||
Type: "no such type",
|
||||
Orientation: "bottom",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid orientation",
|
||||
c: &chronograf.DashboardCell{
|
||||
Legend: chronograf.Legend{
|
||||
Type: "static",
|
||||
Orientation: "no such orientation",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "orientation bottom valid",
|
||||
c: &chronograf.DashboardCell{
|
||||
Legend: chronograf.Legend{
|
||||
Type: "static",
|
||||
Orientation: "bottom",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "orientation top valid",
|
||||
c: &chronograf.DashboardCell{
|
||||
Legend: chronograf.Legend{
|
||||
Type: "static",
|
||||
Orientation: "top",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "orientation right valid",
|
||||
c: &chronograf.DashboardCell{
|
||||
Legend: chronograf.Legend{
|
||||
Type: "static",
|
||||
Orientation: "right",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "orientation left valid",
|
||||
c: &chronograf.DashboardCell{
|
||||
Legend: chronograf.Legend{
|
||||
Type: "static",
|
||||
Orientation: "left",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := HasCorrectLegend(tt.c); (err != nil) != tt.wantErr {
|
||||
t.Errorf("HasCorrectLegend() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
11
server/me.go
11
server/me.go
|
@ -26,10 +26,11 @@ type meResponse struct {
|
|||
|
||||
// If new user response is nil, return an empty meResponse because it
|
||||
// indicates authentication is not needed
|
||||
func newMeResponse(usr *chronograf.User) meResponse {
|
||||
base := "/chronograf/v1/users"
|
||||
func newMeResponse(usr *chronograf.User, org string) meResponse {
|
||||
base := "/chronograf/v1"
|
||||
name := "me"
|
||||
if usr != nil {
|
||||
base = fmt.Sprintf("/chronograf/v1/organizations/%s/users", org)
|
||||
name = PathEscape(fmt.Sprintf("%d", usr.ID))
|
||||
}
|
||||
|
||||
|
@ -181,7 +182,7 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
|
|||
ctx := r.Context()
|
||||
if !s.UseAuth {
|
||||
// If there's no authentication, return an empty user
|
||||
res := newMeResponse(nil)
|
||||
res := newMeResponse(nil, "")
|
||||
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||
return
|
||||
}
|
||||
|
@ -264,7 +265,7 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
|
|||
unknownErrorWithMessage(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
res := newMeResponse(usr)
|
||||
res := newMeResponse(usr, currentOrg.ID)
|
||||
res.Organizations = orgs
|
||||
res.CurrentOrganization = currentOrg
|
||||
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||
|
@ -314,7 +315,7 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
|
|||
unknownErrorWithMessage(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
res := newMeResponse(newUser)
|
||||
res := newMeResponse(newUser, currentOrg.ID)
|
||||
res.Organizations = orgs
|
||||
res.CurrentOrganization = currentOrg
|
||||
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||
|
|
|
@ -176,7 +176,7 @@ func TestService_Me(t *testing.T) {
|
|||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"name":"me","roles":[{"name":"viewer","organization":"0"}],"provider":"github","scheme":"oauth2","superAdmin":true,"links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"viewer","public":true}],"currentOrganization":{"id":"0","name":"Default","defaultRole":"viewer","public":true}}`,
|
||||
wantBody: `{"name":"me","roles":[{"name":"viewer","organization":"0"}],"provider":"github","scheme":"oauth2","superAdmin":true,"links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"viewer","public":true}],"currentOrganization":{"id":"0","name":"Default","defaultRole":"viewer","public":true}}`,
|
||||
},
|
||||
{
|
||||
name: "Existing user - private default org",
|
||||
|
@ -306,7 +306,7 @@ func TestService_Me(t *testing.T) {
|
|||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"name":"me","roles":[{"name":"viewer","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"viewer","public":true}],"currentOrganization":{"id":"0","name":"Default","defaultRole":"viewer","public":true}}`,
|
||||
wantBody: `{"name":"me","roles":[{"name":"viewer","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"viewer","public":true}],"currentOrganization":{"id":"0","name":"Default","defaultRole":"viewer","public":true}}`,
|
||||
},
|
||||
{
|
||||
name: "Existing user - organization doesn't exist",
|
||||
|
@ -423,7 +423,7 @@ func TestService_Me(t *testing.T) {
|
|||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"name":"secret","superAdmin":true,"roles":[{"name":"viewer","organization":"0"}],"provider":"auth0","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"The Gnarly Default","defaultRole":"viewer","public":true}],"currentOrganization":{"id":"0","name":"The Gnarly Default","defaultRole":"viewer","public":true}}
|
||||
wantBody: `{"name":"secret","superAdmin":true,"roles":[{"name":"viewer","organization":"0"}],"provider":"auth0","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"The Gnarly Default","defaultRole":"viewer","public":true}],"currentOrganization":{"id":"0","name":"The Gnarly Default","defaultRole":"viewer","public":true}}
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@ -485,7 +485,7 @@ func TestService_Me(t *testing.T) {
|
|||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"name":"secret","roles":[{"name":"viewer","organization":"0"}],"provider":"auth0","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"The Gnarly Default","public":true,"defaultRole":"viewer"}],"currentOrganization":{"id":"0","name":"The Gnarly Default","public":true,"defaultRole":"viewer"}}
|
||||
wantBody: `{"name":"secret","roles":[{"name":"viewer","organization":"0"}],"provider":"auth0","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"The Gnarly Default","public":true,"defaultRole":"viewer"}],"currentOrganization":{"id":"0","name":"The Gnarly Default","public":true,"defaultRole":"viewer"}}
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@ -547,7 +547,7 @@ func TestService_Me(t *testing.T) {
|
|||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"name":"secret","superAdmin":true,"roles":[{"name":"viewer","organization":"0"}],"provider":"auth0","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"The Gnarly Default","public":true,"defaultRole":"viewer"}],"currentOrganization":{"id":"0","name":"The Gnarly Default","public":true,"defaultRole":"viewer"}}
|
||||
wantBody: `{"name":"secret","superAdmin":true,"roles":[{"name":"viewer","organization":"0"}],"provider":"auth0","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"The Gnarly Default","public":true,"defaultRole":"viewer"}],"currentOrganization":{"id":"0","name":"The Gnarly Default","public":true,"defaultRole":"viewer"}}
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@ -624,7 +624,7 @@ func TestService_Me(t *testing.T) {
|
|||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"links":{"self":"/chronograf/v1/users/me"}}
|
||||
wantBody: `{"links":{"self":"/chronograf/v1/me"}}
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@ -824,7 +824,7 @@ func TestService_UpdateMe(t *testing.T) {
|
|||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"name":"me","roles":[{"name":"admin","organization":"1337"},{"name":"admin","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"admin","public":true},{"id":"1337","name":"The ShillBillThrilliettas","public":true}],"currentOrganization":{"id":"1337","name":"The ShillBillThrilliettas","public":true}}`,
|
||||
wantBody: `{"name":"me","roles":[{"name":"admin","organization":"1337"},{"name":"admin","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/1337/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"admin","public":true},{"id":"1337","name":"The ShillBillThrilliettas","public":true}],"currentOrganization":{"id":"1337","name":"The ShillBillThrilliettas","public":true}}`,
|
||||
},
|
||||
{
|
||||
name: "Change the current User's organization",
|
||||
|
@ -899,7 +899,7 @@ func TestService_UpdateMe(t *testing.T) {
|
|||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"name":"me","roles":[{"name":"admin","organization":"1337"},{"name":"editor","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"editor","public":true},{"id":"1337","name":"The ThrillShilliettos","public":false}],"currentOrganization":{"id":"1337","name":"The ThrillShilliettos","public":false}}`,
|
||||
wantBody: `{"name":"me","roles":[{"name":"admin","organization":"1337"},{"name":"editor","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/1337/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"editor","public":true},{"id":"1337","name":"The ThrillShilliettos","public":false}],"currentOrganization":{"id":"1337","name":"The ThrillShilliettos","public":false}}`,
|
||||
},
|
||||
{
|
||||
name: "Unable to find requested user in valid organization",
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/bouk/httprouter"
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
// RouteMatchesPrincipal checks that the organization on context matches the organization
|
||||
// in the route.
|
||||
func RouteMatchesPrincipal(
|
||||
store DataStore,
|
||||
useAuth bool,
|
||||
logger chronograf.Logger,
|
||||
next http.HandlerFunc,
|
||||
) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if !useAuth {
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
log := logger.
|
||||
WithField("component", "org_match").
|
||||
WithField("remote_addr", r.RemoteAddr).
|
||||
WithField("method", r.Method).
|
||||
WithField("url", r.URL)
|
||||
|
||||
orgID := httprouter.GetParamFromContext(ctx, "oid")
|
||||
p, err := getValidPrincipal(ctx)
|
||||
if err != nil {
|
||||
log.Error("Failed to retrieve principal from context")
|
||||
Error(w, http.StatusForbidden, "User is not authorized", logger)
|
||||
return
|
||||
}
|
||||
|
||||
if p.Organization == "" {
|
||||
defaultOrg, err := store.Organizations(ctx).DefaultOrganization(ctx)
|
||||
if err != nil {
|
||||
log.Error("Failed to look up default organization")
|
||||
Error(w, http.StatusForbidden, "User is not authorized", logger)
|
||||
return
|
||||
}
|
||||
p.Organization = defaultOrg.ID
|
||||
}
|
||||
|
||||
if orgID != p.Organization {
|
||||
log.Error("Route organization does not match the organization on principal")
|
||||
Error(w, http.StatusForbidden, "User is not authorized", logger)
|
||||
return
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/bouk/httprouter"
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/log"
|
||||
"github.com/influxdata/chronograf/mocks"
|
||||
"github.com/influxdata/chronograf/oauth2"
|
||||
)
|
||||
|
||||
func TestRouteMatchesPrincipal(t *testing.T) {
|
||||
type fields struct {
|
||||
OrganizationsStore chronograf.OrganizationsStore
|
||||
Logger chronograf.Logger
|
||||
}
|
||||
type args struct {
|
||||
useAuth bool
|
||||
principal *oauth2.Principal
|
||||
routerParams *httprouter.Params
|
||||
}
|
||||
type wants struct {
|
||||
matches bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "route matches request params",
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: "default",
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
useAuth: true,
|
||||
principal: &oauth2.Principal{
|
||||
Subject: "user",
|
||||
Issuer: "github",
|
||||
Organization: "default",
|
||||
},
|
||||
routerParams: &httprouter.Params{
|
||||
{
|
||||
Key: "oid",
|
||||
Value: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
matches: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "route does not match request params",
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: "default",
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
useAuth: true,
|
||||
principal: &oauth2.Principal{
|
||||
Subject: "user",
|
||||
Issuer: "github",
|
||||
Organization: "default",
|
||||
},
|
||||
routerParams: &httprouter.Params{
|
||||
{
|
||||
Key: "oid",
|
||||
Value: "other",
|
||||
},
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
matches: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing principal",
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: "default",
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
useAuth: true,
|
||||
principal: nil,
|
||||
routerParams: &httprouter.Params{
|
||||
{
|
||||
Key: "oid",
|
||||
Value: "other",
|
||||
},
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
matches: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not using auth",
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: "default",
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
useAuth: false,
|
||||
principal: &oauth2.Principal{
|
||||
Subject: "user",
|
||||
Issuer: "github",
|
||||
Organization: "default",
|
||||
},
|
||||
routerParams: &httprouter.Params{
|
||||
{
|
||||
Key: "oid",
|
||||
Value: "other",
|
||||
},
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
matches: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
store := &mocks.Store{
|
||||
OrganizationsStore: tt.fields.OrganizationsStore,
|
||||
}
|
||||
var matches bool
|
||||
next := func(w http.ResponseWriter, r *http.Request) {
|
||||
matches = true
|
||||
}
|
||||
fn := RouteMatchesPrincipal(
|
||||
store,
|
||||
tt.args.useAuth,
|
||||
tt.fields.Logger,
|
||||
next,
|
||||
)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
url := "http://any.url"
|
||||
r := httptest.NewRequest(
|
||||
"GET",
|
||||
url,
|
||||
nil,
|
||||
)
|
||||
if tt.args.routerParams != nil {
|
||||
r = r.WithContext(httprouter.WithParams(r.Context(), *tt.args.routerParams))
|
||||
}
|
||||
if tt.args.principal == nil {
|
||||
r = r.WithContext(context.WithValue(r.Context(), oauth2.PrincipalKey, nil))
|
||||
} else {
|
||||
r = r.WithContext(context.WithValue(r.Context(), oauth2.PrincipalKey, *tt.args.principal))
|
||||
}
|
||||
fn(w, r)
|
||||
|
||||
if matches != tt.wants.matches {
|
||||
t.Errorf("%q. RouteMatchesPrincipal() = %v, expected %v", tt.name, matches, tt.wants.matches)
|
||||
}
|
||||
|
||||
if !matches && w.Code != http.StatusForbidden {
|
||||
t.Errorf("%q. RouteMatchesPrincipal() Status Code = %v, expected %v", tt.name, w.Code, http.StatusForbidden)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
|
@ -68,6 +68,16 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
|
|||
hr.NotFound = http.StripPrefix(opts.Basepath, hr.NotFound)
|
||||
}
|
||||
|
||||
EnsureMember := func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return AuthorizedUser(
|
||||
service.Store,
|
||||
opts.UseAuth,
|
||||
roles.MemberRoleName,
|
||||
opts.Logger,
|
||||
next,
|
||||
)
|
||||
}
|
||||
_ = EnsureMember
|
||||
EnsureViewer := func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return AuthorizedUser(
|
||||
service.Store,
|
||||
|
@ -105,6 +115,19 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
|
|||
)
|
||||
}
|
||||
|
||||
rawStoreAccess := func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return RawStoreAccess(opts.Logger, next)
|
||||
}
|
||||
|
||||
ensureOrgMatches := func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return RouteMatchesPrincipal(
|
||||
service.Store,
|
||||
opts.UseAuth,
|
||||
opts.Logger,
|
||||
next,
|
||||
)
|
||||
}
|
||||
|
||||
/* Documentation */
|
||||
router.GET("/swagger.json", Spec())
|
||||
router.GET("/docs", Redoc("/swagger.json"))
|
||||
|
@ -114,9 +137,9 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
|
|||
router.GET("/chronograf/v1/organizations", EnsureAdmin(service.Organizations))
|
||||
router.POST("/chronograf/v1/organizations", EnsureSuperAdmin(service.NewOrganization))
|
||||
|
||||
router.GET("/chronograf/v1/organizations/:id", EnsureAdmin(service.OrganizationID))
|
||||
router.PATCH("/chronograf/v1/organizations/:id", EnsureSuperAdmin(service.UpdateOrganization))
|
||||
router.DELETE("/chronograf/v1/organizations/:id", EnsureSuperAdmin(service.RemoveOrganization))
|
||||
router.GET("/chronograf/v1/organizations/:oid", EnsureAdmin(service.OrganizationID))
|
||||
router.PATCH("/chronograf/v1/organizations/:oid", EnsureSuperAdmin(service.UpdateOrganization))
|
||||
router.DELETE("/chronograf/v1/organizations/:oid", EnsureSuperAdmin(service.RemoveOrganization))
|
||||
|
||||
// Sources
|
||||
router.GET("/chronograf/v1/sources", EnsureViewer(service.Sources))
|
||||
|
@ -201,12 +224,19 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
|
|||
router.PUT("/chronograf/v1/me", service.UpdateMe(opts.Auth))
|
||||
|
||||
// TODO(desa): what to do about admin's being able to set superadmin
|
||||
router.GET("/chronograf/v1/users", EnsureAdmin(service.Users))
|
||||
router.POST("/chronograf/v1/users", EnsureAdmin(service.NewUser))
|
||||
router.GET("/chronograf/v1/organizations/:oid/users", EnsureAdmin(ensureOrgMatches(service.Users)))
|
||||
router.POST("/chronograf/v1/organizations/:oid/users", EnsureAdmin(ensureOrgMatches(service.NewUser)))
|
||||
|
||||
router.GET("/chronograf/v1/users/:id", EnsureAdmin(service.UserID))
|
||||
router.DELETE("/chronograf/v1/users/:id", EnsureAdmin(service.RemoveUser))
|
||||
router.PATCH("/chronograf/v1/users/:id", EnsureAdmin(service.UpdateUser))
|
||||
router.GET("/chronograf/v1/organizations/:oid/users/:id", EnsureAdmin(ensureOrgMatches(service.UserID)))
|
||||
router.DELETE("/chronograf/v1/organizations/:oid/users/:id", EnsureAdmin(ensureOrgMatches(service.RemoveUser)))
|
||||
router.PATCH("/chronograf/v1/organizations/:oid/users/:id", EnsureAdmin(ensureOrgMatches(service.UpdateUser)))
|
||||
|
||||
router.GET("/chronograf/v1/users", EnsureSuperAdmin(rawStoreAccess(service.Users)))
|
||||
router.POST("/chronograf/v1/users", EnsureSuperAdmin(rawStoreAccess(service.NewUser)))
|
||||
|
||||
router.GET("/chronograf/v1/users/:id", EnsureSuperAdmin(rawStoreAccess(service.UserID)))
|
||||
router.DELETE("/chronograf/v1/users/:id", EnsureSuperAdmin(rawStoreAccess(service.RemoveUser)))
|
||||
router.PATCH("/chronograf/v1/users/:id", EnsureSuperAdmin(rawStoreAccess(service.UpdateUser)))
|
||||
|
||||
// Dashboards
|
||||
router.GET("/chronograf/v1/dashboards", EnsureViewer(service.Dashboards))
|
||||
|
@ -257,6 +287,11 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
|
|||
CustomLinks: opts.CustomLinks,
|
||||
}
|
||||
|
||||
getPrincipal := func(r *http.Request) oauth2.Principal {
|
||||
p, _ := HasAuthorizedToken(opts.Auth, r)
|
||||
return p
|
||||
}
|
||||
allRoutes.GetPrincipal = getPrincipal
|
||||
router.Handler("GET", "/chronograf/v1/", allRoutes)
|
||||
|
||||
var out http.Handler
|
||||
|
|
|
@ -165,7 +165,7 @@ func (s *Service) NewOrganization(w http.ResponseWriter, r *http.Request) {
|
|||
func (s *Service) OrganizationID(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
id := httprouter.GetParamFromContext(ctx, "id")
|
||||
id := httprouter.GetParamFromContext(ctx, "oid")
|
||||
|
||||
org, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &id})
|
||||
if err != nil {
|
||||
|
@ -191,7 +191,7 @@ func (s *Service) UpdateOrganization(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
ctx := r.Context()
|
||||
id := httprouter.GetParamFromContext(ctx, "id")
|
||||
id := httprouter.GetParamFromContext(ctx, "oid")
|
||||
|
||||
org, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &id})
|
||||
if err != nil {
|
||||
|
@ -226,7 +226,7 @@ func (s *Service) UpdateOrganization(w http.ResponseWriter, r *http.Request) {
|
|||
// RemoveOrganization removes an organization in the organizations store
|
||||
func (s *Service) RemoveOrganization(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
id := httprouter.GetParamFromContext(ctx, "id")
|
||||
id := httprouter.GetParamFromContext(ctx, "oid")
|
||||
|
||||
org, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &id})
|
||||
if err != nil {
|
||||
|
|
|
@ -82,7 +82,7 @@ func TestService_OrganizationID(t *testing.T) {
|
|||
context.Background(),
|
||||
httprouter.Params{
|
||||
{
|
||||
Key: "id",
|
||||
Key: "oid",
|
||||
Value: tt.id,
|
||||
},
|
||||
}))
|
||||
|
@ -411,7 +411,7 @@ func TestService_UpdateOrganization(t *testing.T) {
|
|||
tt.args.r = tt.args.r.WithContext(httprouter.WithParams(context.Background(),
|
||||
httprouter.Params{
|
||||
{
|
||||
Key: "id",
|
||||
Key: "oid",
|
||||
Value: tt.id,
|
||||
},
|
||||
}))
|
||||
|
@ -503,7 +503,7 @@ func TestService_RemoveOrganization(t *testing.T) {
|
|||
tt.args.r = tt.args.r.WithContext(httprouter.WithParams(context.Background(),
|
||||
httprouter.Params{
|
||||
{
|
||||
Key: "id",
|
||||
Key: "oid",
|
||||
Value: tt.id,
|
||||
},
|
||||
}))
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/oauth2"
|
||||
)
|
||||
|
||||
// AuthRoute are the routes for each type of OAuth2 provider
|
||||
|
@ -31,6 +33,7 @@ func (r *AuthRoutes) Lookup(provider string) (AuthRoute, bool) {
|
|||
type getRoutesResponse struct {
|
||||
Layouts string `json:"layouts"` // Location of the layouts endpoint
|
||||
Users string `json:"users"` // Location of the users endpoint
|
||||
AllUsers string `json:"allUsers"` // Location of the raw users endpoint
|
||||
Organizations string `json:"organizations"` // Location of the organizations endpoint
|
||||
Mappings string `json:"mappings"` // Location of the application mappings endpoint
|
||||
Sources string `json:"sources"` // Location of the sources endpoint
|
||||
|
@ -47,14 +50,15 @@ type getRoutesResponse struct {
|
|||
// external links for the client to know about, such as for JSON feeds or custom side nav buttons.
|
||||
// Optionally, routes for authentication can be returned.
|
||||
type AllRoutes struct {
|
||||
AuthRoutes []AuthRoute // Location of all auth routes. If no auth, this can be empty.
|
||||
LogoutLink string // Location of the logout route for all auth routes. If no auth, this can be empty.
|
||||
StatusFeed string // External link to the JSON Feed for the News Feed on the client's Status Page
|
||||
CustomLinks map[string]string // Custom external links for client's User menu, as passed in via CLI/ENV
|
||||
Logger chronograf.Logger
|
||||
GetPrincipal func(r *http.Request) oauth2.Principal // GetPrincipal is used to retrieve the principal on http request.
|
||||
AuthRoutes []AuthRoute // Location of all auth routes. If no auth, this can be empty.
|
||||
LogoutLink string // Location of the logout route for all auth routes. If no auth, this can be empty.
|
||||
StatusFeed string // External link to the JSON Feed for the News Feed on the client's Status Page
|
||||
CustomLinks map[string]string // Custom external links for client's User menu, as passed in via CLI/ENV
|
||||
Logger chronograf.Logger
|
||||
}
|
||||
|
||||
// ServeHTTP returns all top level routes and external links within chronograf
|
||||
// serveHTTP returns all top level routes and external links within chronograf
|
||||
func (a *AllRoutes) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
customLinks, err := NewCustomLinks(a.CustomLinks)
|
||||
if err != nil {
|
||||
|
@ -62,10 +66,20 @@ func (a *AllRoutes) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
org := "default"
|
||||
if a.GetPrincipal != nil {
|
||||
// If there is a principal, use the organization to populate the users routes
|
||||
// otherwise use the default organization
|
||||
if p := a.GetPrincipal(r); p.Organization != "" {
|
||||
org = p.Organization
|
||||
}
|
||||
}
|
||||
|
||||
routes := getRoutesResponse{
|
||||
Sources: "/chronograf/v1/sources",
|
||||
Layouts: "/chronograf/v1/layouts",
|
||||
Users: "/chronograf/v1/users",
|
||||
Users: fmt.Sprintf("/chronograf/v1/organizations/%s/users", org),
|
||||
AllUsers: "/chronograf/v1/users",
|
||||
Organizations: "/chronograf/v1/organizations",
|
||||
Me: "/chronograf/v1/me",
|
||||
Environment: "/chronograf/v1/env",
|
||||
|
|
|
@ -29,7 +29,7 @@ func TestAllRoutes(t *testing.T) {
|
|||
if err := json.Unmarshal(body, &routes); err != nil {
|
||||
t.Error("TestAllRoutes not able to unmarshal JSON response")
|
||||
}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":""}}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":""}}
|
||||
`
|
||||
if want != string(body) {
|
||||
t.Errorf("TestAllRoutes\nwanted\n*%s*\ngot\n*%s*", want, string(body))
|
||||
|
@ -67,7 +67,7 @@ func TestAllRoutesWithAuth(t *testing.T) {
|
|||
if err := json.Unmarshal(body, &routes); err != nil {
|
||||
t.Error("TestAllRoutesWithAuth not able to unmarshal JSON response")
|
||||
}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[{"name":"github","label":"GitHub","login":"/oauth/github/login","logout":"/oauth/github/logout","callback":"/oauth/github/callback"}],"logout":"/oauth/logout","external":{"statusFeed":""}}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[{"name":"github","label":"GitHub","login":"/oauth/github/login","logout":"/oauth/github/logout","callback":"/oauth/github/callback"}],"logout":"/oauth/logout","external":{"statusFeed":""}}
|
||||
`
|
||||
if want != string(body) {
|
||||
t.Errorf("TestAllRoutesWithAuth\nwanted\n*%s*\ngot\n*%s*", want, string(body))
|
||||
|
@ -100,7 +100,7 @@ func TestAllRoutesWithExternalLinks(t *testing.T) {
|
|||
if err := json.Unmarshal(body, &routes); err != nil {
|
||||
t.Error("TestAllRoutesWithExternalLinks not able to unmarshal JSON response")
|
||||
}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":"http://pineapple.life/feed.json","custom":[{"name":"cubeapple","url":"https://cube.apple"}]}}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":"http://pineapple.life/feed.json","custom":[{"name":"cubeapple","url":"https://cube.apple"}]}}
|
||||
`
|
||||
if want != string(body) {
|
||||
t.Errorf("TestAllRoutesWithExternalLinks\nwanted\n*%s*\ngot\n*%s*", want, string(body))
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"info": {
|
||||
"title": "Chronograf",
|
||||
"description": "API endpoints for Chronograf",
|
||||
"version": "1.4.0.0"
|
||||
"version": "1.4.0.1"
|
||||
},
|
||||
"schemes": ["http"],
|
||||
"basePath": "/chronograf/v1",
|
||||
|
@ -3970,6 +3970,24 @@
|
|||
"$ref": "#/definitions/DashboardColor"
|
||||
}
|
||||
},
|
||||
"legend": {
|
||||
"description":
|
||||
"Legend define encoding of the data into a cell's legend",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"description": "type is the style of the legend",
|
||||
"type": "string",
|
||||
"enum": ["static"]
|
||||
},
|
||||
"orientation": {
|
||||
"description":
|
||||
"orientation is the location of the legend with respect to the cell graph",
|
||||
"type": "string",
|
||||
"enum": ["top", "bottom", "left", "right"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -41,15 +41,15 @@ func (r *userRequest) ValidCreate() error {
|
|||
}
|
||||
|
||||
func (r *userRequest) ValidUpdate() error {
|
||||
if len(r.Roles) == 0 {
|
||||
if r.Roles == nil {
|
||||
return fmt.Errorf("No Roles to update")
|
||||
}
|
||||
return r.ValidRoles()
|
||||
}
|
||||
|
||||
func (r *userRequest) ValidRoles() error {
|
||||
orgs := map[string]bool{}
|
||||
if len(r.Roles) > 0 {
|
||||
orgs := map[string]bool{}
|
||||
for _, r := range r.Roles {
|
||||
if r.Organization == "" {
|
||||
return fmt.Errorf("no organization was provided")
|
||||
|
@ -59,10 +59,10 @@ func (r *userRequest) ValidRoles() error {
|
|||
}
|
||||
orgs[r.Organization] = true
|
||||
switch r.Name {
|
||||
case roles.MemberRoleName, roles.ViewerRoleName, roles.EditorRoleName, roles.AdminRoleName:
|
||||
case roles.MemberRoleName, roles.ViewerRoleName, roles.EditorRoleName, roles.AdminRoleName, roles.WildcardRoleName:
|
||||
continue
|
||||
default:
|
||||
return fmt.Errorf("Unknown role %s. Valid roles are 'member', 'viewer', 'editor', and 'admin'", r.Name)
|
||||
return fmt.Errorf("Unknown role %s. Valid roles are 'member', 'viewer', 'editor', 'admin', and '*'", r.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -79,13 +79,19 @@ type userResponse struct {
|
|||
Roles []chronograf.Role `json:"roles"`
|
||||
}
|
||||
|
||||
func newUserResponse(u *chronograf.User) *userResponse {
|
||||
func newUserResponse(u *chronograf.User, org string) *userResponse {
|
||||
// This ensures that any user response with no roles returns an empty array instead of
|
||||
// null when marshaled into JSON. That way, JavaScript doesn't need any guard on the
|
||||
// key existing and it can simply be iterated over.
|
||||
if u.Roles == nil {
|
||||
u.Roles = []chronograf.Role{}
|
||||
}
|
||||
var selfLink string
|
||||
if org != "" {
|
||||
selfLink = fmt.Sprintf("/chronograf/v1/organizations/%s/users/%d", org, u.ID)
|
||||
} else {
|
||||
selfLink = fmt.Sprintf("/chronograf/v1/users/%d", u.ID)
|
||||
}
|
||||
return &userResponse{
|
||||
ID: u.ID,
|
||||
Name: u.Name,
|
||||
|
@ -94,7 +100,7 @@ func newUserResponse(u *chronograf.User) *userResponse {
|
|||
Roles: u.Roles,
|
||||
SuperAdmin: u.SuperAdmin,
|
||||
Links: selfLinks{
|
||||
Self: fmt.Sprintf("/chronograf/v1/users/%d", u.ID),
|
||||
Self: selfLink,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -104,18 +110,25 @@ type usersResponse struct {
|
|||
Users []*userResponse `json:"users"`
|
||||
}
|
||||
|
||||
func newUsersResponse(users []chronograf.User) *usersResponse {
|
||||
func newUsersResponse(users []chronograf.User, org string) *usersResponse {
|
||||
usersResp := make([]*userResponse, len(users))
|
||||
for i, user := range users {
|
||||
usersResp[i] = newUserResponse(&user)
|
||||
usersResp[i] = newUserResponse(&user, org)
|
||||
}
|
||||
sort.Slice(usersResp, func(i, j int) bool {
|
||||
return usersResp[i].ID < usersResp[j].ID
|
||||
})
|
||||
|
||||
var selfLink string
|
||||
if org != "" {
|
||||
selfLink = fmt.Sprintf("/chronograf/v1/organizations/%s/users", org)
|
||||
} else {
|
||||
selfLink = "/chronograf/v1/users"
|
||||
}
|
||||
return &usersResponse{
|
||||
Users: usersResp,
|
||||
Links: selfLinks{
|
||||
Self: "/chronograf/v1/users",
|
||||
Self: selfLink,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -136,7 +149,9 @@ func (s *Service) UserID(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
res := newUserResponse(user)
|
||||
orgID := httprouter.GetParamFromContext(ctx, "oid")
|
||||
res := newUserResponse(user, orgID)
|
||||
location(w, res.Links.Self)
|
||||
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||
}
|
||||
|
||||
|
@ -162,6 +177,11 @@ func (s *Service) NewUser(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := s.validRoles(serverCtx, req.Roles); err != nil {
|
||||
invalidData(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user := &chronograf.User{
|
||||
Name: req.Name,
|
||||
Provider: req.Provider,
|
||||
|
@ -184,7 +204,8 @@ func (s *Service) NewUser(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
cu := newUserResponse(res)
|
||||
orgID := httprouter.GetParamFromContext(ctx, "oid")
|
||||
cu := newUserResponse(res, orgID)
|
||||
location(w, cu.Links.Self)
|
||||
encodeJSON(w, http.StatusCreated, cu, s.Logger)
|
||||
}
|
||||
|
@ -204,15 +225,6 @@ func (s *Service) RemoveUser(w http.ResponseWriter, r *http.Request) {
|
|||
Error(w, http.StatusNotFound, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
ctxUser, ok := hasUserContext(ctx)
|
||||
if !ok {
|
||||
Error(w, http.StatusBadRequest, "failed to retrieve user from context", s.Logger)
|
||||
return
|
||||
}
|
||||
if ctxUser.ID == u.ID {
|
||||
Error(w, http.StatusForbidden, "user cannot delete themselves", s.Logger)
|
||||
return
|
||||
}
|
||||
if err := s.Store.Users(ctx).Delete(ctx, u); err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
|
||||
return
|
||||
|
@ -248,6 +260,12 @@ func (s *Service) UpdateUser(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
serverCtx := serverContext(ctx)
|
||||
if err := s.validRoles(serverCtx, req.Roles); err != nil {
|
||||
invalidData(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
// ValidUpdate should ensure that req.Roles is not nil
|
||||
u.Roles = req.Roles
|
||||
|
||||
|
@ -299,7 +317,8 @@ func (s *Service) UpdateUser(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
cu := newUserResponse(u)
|
||||
orgID := httprouter.GetParamFromContext(ctx, "oid")
|
||||
cu := newUserResponse(u, orgID)
|
||||
location(w, cu.Links.Self)
|
||||
encodeJSON(w, http.StatusOK, cu, s.Logger)
|
||||
}
|
||||
|
@ -314,7 +333,8 @@ func (s *Service) Users(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
res := newUsersResponse(users)
|
||||
orgID := httprouter.GetParamFromContext(ctx, "oid")
|
||||
res := newUsersResponse(users, orgID)
|
||||
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||
}
|
||||
|
||||
|
@ -341,3 +361,19 @@ func setSuperAdmin(ctx context.Context, req userRequest, user *chronograf.User)
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) validRoles(ctx context.Context, rs []chronograf.Role) error {
|
||||
for i, role := range rs {
|
||||
// verify that the organization exists
|
||||
org, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &role.Organization})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if role.Name == roles.WildcardRoleName {
|
||||
role.Name = org.DefaultRole
|
||||
rs[i] = role
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -112,9 +112,10 @@ func TestService_UserID(t *testing.T) {
|
|||
|
||||
func TestService_NewUser(t *testing.T) {
|
||||
type fields struct {
|
||||
UsersStore chronograf.UsersStore
|
||||
ConfigStore chronograf.ConfigStore
|
||||
Logger chronograf.Logger
|
||||
UsersStore chronograf.UsersStore
|
||||
OrganizationsStore chronograf.OrganizationsStore
|
||||
ConfigStore chronograf.ConfigStore
|
||||
Logger chronograf.Logger
|
||||
}
|
||||
type args struct {
|
||||
w *httptest.ResponseRecorder
|
||||
|
@ -204,6 +205,25 @@ func TestService_NewUser(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
switch *q.ID {
|
||||
case "1":
|
||||
return &chronograf.Organization{
|
||||
ID: "1",
|
||||
Name: "org",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
}, nil
|
||||
case "2":
|
||||
return &chronograf.Organization{
|
||||
ID: "2",
|
||||
Name: "another",
|
||||
DefaultRole: roles.MemberRoleName,
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("org not found")
|
||||
},
|
||||
},
|
||||
UsersStore: &mocks.UsersStore{
|
||||
AddF: func(ctx context.Context, user *chronograf.User) (*chronograf.User, error) {
|
||||
return &chronograf.User{
|
||||
|
@ -427,14 +447,93 @@ func TestService_NewUser(t *testing.T) {
|
|||
wantContentType: "application/json",
|
||||
wantBody: `{"id":"1338","superAdmin":true,"name":"bob","provider":"github","scheme":"oauth2","roles":[],"links":{"self":"/chronograf/v1/users/1338"}}`,
|
||||
},
|
||||
{
|
||||
name: "Create a new Chronograf User with multiple roles with wildcard default role",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest(
|
||||
"POST",
|
||||
"http://any.url",
|
||||
nil,
|
||||
),
|
||||
user: &userRequest{
|
||||
Name: "bob",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: roles.AdminRoleName,
|
||||
Organization: "1",
|
||||
},
|
||||
{
|
||||
Name: roles.WildcardRoleName,
|
||||
Organization: "2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
switch *q.ID {
|
||||
case "1":
|
||||
return &chronograf.Organization{
|
||||
ID: "1",
|
||||
Name: "org",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
}, nil
|
||||
case "2":
|
||||
return &chronograf.Organization{
|
||||
ID: "2",
|
||||
Name: "another",
|
||||
DefaultRole: roles.MemberRoleName,
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("org not found")
|
||||
},
|
||||
},
|
||||
UsersStore: &mocks.UsersStore{
|
||||
AddF: func(ctx context.Context, user *chronograf.User) (*chronograf.User, error) {
|
||||
return &chronograf.User{
|
||||
ID: 1338,
|
||||
Name: "bob",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: roles.AdminRoleName,
|
||||
Organization: "1",
|
||||
},
|
||||
{
|
||||
Name: roles.MemberRoleName,
|
||||
Organization: "2",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wantStatus: http.StatusCreated,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"id":"1338","superAdmin":false,"name":"bob","provider":"github","scheme":"oauth2","roles":[{"name":"admin","organization":"1"},{"name":"member","organization":"2"}],"links":{"self":"/chronograf/v1/users/1338"}}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &Service{
|
||||
Store: &mocks.Store{
|
||||
UsersStore: tt.fields.UsersStore,
|
||||
ConfigStore: tt.fields.ConfigStore,
|
||||
UsersStore: tt.fields.UsersStore,
|
||||
ConfigStore: tt.fields.ConfigStore,
|
||||
OrganizationsStore: tt.fields.OrganizationsStore,
|
||||
},
|
||||
Logger: tt.fields.Logger,
|
||||
}
|
||||
|
@ -564,8 +663,8 @@ func TestService_RemoveUser(t *testing.T) {
|
|||
},
|
||||
id: "1339",
|
||||
},
|
||||
wantStatus: http.StatusForbidden,
|
||||
wantBody: `{"code":403,"message":"user cannot delete themselves"}`,
|
||||
wantStatus: http.StatusNoContent,
|
||||
wantBody: ``,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
@ -613,8 +712,9 @@ func TestService_RemoveUser(t *testing.T) {
|
|||
|
||||
func TestService_UpdateUser(t *testing.T) {
|
||||
type fields struct {
|
||||
UsersStore chronograf.UsersStore
|
||||
Logger chronograf.Logger
|
||||
UsersStore chronograf.UsersStore
|
||||
OrganizationsStore chronograf.OrganizationsStore
|
||||
Logger chronograf.Logger
|
||||
}
|
||||
type args struct {
|
||||
w *httptest.ResponseRecorder
|
||||
|
@ -631,10 +731,76 @@ func TestService_UpdateUser(t *testing.T) {
|
|||
wantContentType string
|
||||
wantBody string
|
||||
}{
|
||||
{
|
||||
name: "Update a Chronograf user - no roles",
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
UsersStore: &mocks.UsersStore{
|
||||
UpdateF: func(ctx context.Context, user *chronograf.User) error {
|
||||
return nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
|
||||
switch *q.ID {
|
||||
case 1336:
|
||||
return &chronograf.User{
|
||||
ID: 1336,
|
||||
Name: "bobbetta",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: roles.EditorRoleName,
|
||||
Organization: "1",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("User with ID %d not found", *q.ID)
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest(
|
||||
"PATCH",
|
||||
"http://any.url",
|
||||
nil,
|
||||
),
|
||||
userKeyUser: &chronograf.User{
|
||||
ID: 0,
|
||||
Name: "coolUser",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: false,
|
||||
},
|
||||
user: &userRequest{
|
||||
ID: 1336,
|
||||
Roles: []chronograf.Role{},
|
||||
},
|
||||
},
|
||||
id: "1336",
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"id":"1336","superAdmin":false,"name":"bobbetta","provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/users/1336"},"roles":[]}`,
|
||||
},
|
||||
{
|
||||
name: "Update a Chronograf user",
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
switch *q.ID {
|
||||
case "1":
|
||||
return &chronograf.Organization{
|
||||
ID: "1",
|
||||
Name: "org",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("org not found")
|
||||
},
|
||||
},
|
||||
UsersStore: &mocks.UsersStore{
|
||||
UpdateF: func(ctx context.Context, user *chronograf.User) error {
|
||||
return nil
|
||||
|
@ -693,6 +859,25 @@ func TestService_UpdateUser(t *testing.T) {
|
|||
name: "Update a Chronograf user roles different orgs",
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
switch *q.ID {
|
||||
case "1":
|
||||
return &chronograf.Organization{
|
||||
ID: "1",
|
||||
Name: "org",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
}, nil
|
||||
case "2":
|
||||
return &chronograf.Organization{
|
||||
ID: "2",
|
||||
Name: "another",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("org not found")
|
||||
},
|
||||
},
|
||||
UsersStore: &mocks.UsersStore{
|
||||
UpdateF: func(ctx context.Context, user *chronograf.User) error {
|
||||
return nil
|
||||
|
@ -804,6 +989,19 @@ func TestService_UpdateUser(t *testing.T) {
|
|||
name: "SuperAdmin modifying their own SuperAdmin Status - user missing from context",
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
switch *q.ID {
|
||||
case "1":
|
||||
return &chronograf.Organization{
|
||||
ID: "1",
|
||||
Name: "org",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("org not found")
|
||||
},
|
||||
},
|
||||
UsersStore: &mocks.UsersStore{
|
||||
UpdateF: func(ctx context.Context, user *chronograf.User) error {
|
||||
return nil
|
||||
|
@ -857,6 +1055,19 @@ func TestService_UpdateUser(t *testing.T) {
|
|||
name: "SuperAdmin modifying their own SuperAdmin Status",
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
switch *q.ID {
|
||||
case "1":
|
||||
return &chronograf.Organization{
|
||||
ID: "1",
|
||||
Name: "org",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("org not found")
|
||||
},
|
||||
},
|
||||
UsersStore: &mocks.UsersStore{
|
||||
UpdateF: func(ctx context.Context, user *chronograf.User) error {
|
||||
return nil
|
||||
|
@ -917,6 +1128,19 @@ func TestService_UpdateUser(t *testing.T) {
|
|||
name: "Update a SuperAdmin's Roles - without super admin context",
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
switch *q.ID {
|
||||
case "1":
|
||||
return &chronograf.Organization{
|
||||
ID: "1",
|
||||
Name: "org",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("org not found")
|
||||
},
|
||||
},
|
||||
UsersStore: &mocks.UsersStore{
|
||||
UpdateF: func(ctx context.Context, user *chronograf.User) error {
|
||||
return nil
|
||||
|
@ -977,6 +1201,19 @@ func TestService_UpdateUser(t *testing.T) {
|
|||
name: "Update a Chronograf user to super admin - without super admin context",
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
switch *q.ID {
|
||||
case "1":
|
||||
return &chronograf.Organization{
|
||||
ID: "1",
|
||||
Name: "org",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("org not found")
|
||||
},
|
||||
},
|
||||
UsersStore: &mocks.UsersStore{
|
||||
UpdateF: func(ctx context.Context, user *chronograf.User) error {
|
||||
return nil
|
||||
|
@ -1033,6 +1270,19 @@ func TestService_UpdateUser(t *testing.T) {
|
|||
name: "Update a Chronograf user to super admin - with super admin context",
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
switch *q.ID {
|
||||
case "1":
|
||||
return &chronograf.Organization{
|
||||
ID: "1",
|
||||
Name: "org",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("org not found")
|
||||
},
|
||||
},
|
||||
UsersStore: &mocks.UsersStore{
|
||||
UpdateF: func(ctx context.Context, user *chronograf.User) error {
|
||||
return nil
|
||||
|
@ -1090,7 +1340,8 @@ func TestService_UpdateUser(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &Service{
|
||||
Store: &mocks.Store{
|
||||
UsersStore: tt.fields.UsersStore,
|
||||
UsersStore: tt.fields.UsersStore,
|
||||
OrganizationsStore: tt.fields.OrganizationsStore,
|
||||
},
|
||||
Logger: tt.fields.Logger,
|
||||
}
|
||||
|
@ -1354,7 +1605,7 @@ func TestUserRequest_ValidCreate(t *testing.T) {
|
|||
},
|
||||
},
|
||||
wantErr: true,
|
||||
err: fmt.Errorf("Unknown role BilliettaSpecialRole. Valid roles are 'member', 'viewer', 'editor', and 'admin'"),
|
||||
err: fmt.Errorf("Unknown role BilliettaSpecialRole. Valid roles are 'member', 'viewer', 'editor', 'admin', and '*'"),
|
||||
},
|
||||
{
|
||||
name: "Invalid roles - missing organization",
|
||||
|
@ -1444,7 +1695,39 @@ func TestUserRequest_ValidUpdate(t *testing.T) {
|
|||
},
|
||||
},
|
||||
wantErr: true,
|
||||
err: fmt.Errorf("Unknown role BillietaSpecialOrg. Valid roles are 'member', 'viewer', 'editor', and 'admin'"),
|
||||
err: fmt.Errorf("Unknown role BillietaSpecialOrg. Valid roles are 'member', 'viewer', 'editor', 'admin', and '*'"),
|
||||
},
|
||||
{
|
||||
name: "Valid – roles empty",
|
||||
args: args{
|
||||
u: &userRequest{
|
||||
ID: 1337,
|
||||
Name: "billietta",
|
||||
Provider: "auth0",
|
||||
Scheme: "oauth2",
|
||||
Roles: []chronograf.Role{},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid - bad role name",
|
||||
args: args{
|
||||
u: &userRequest{
|
||||
ID: 1337,
|
||||
Name: "billietta",
|
||||
Provider: "auth0",
|
||||
Scheme: "oauth2",
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: "BillietaSpecialOrg",
|
||||
Organization: "0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
err: fmt.Errorf("Unknown role BillietaSpecialOrg. Valid roles are 'member', 'viewer', 'editor', 'admin', and '*'"),
|
||||
},
|
||||
{
|
||||
name: "Invalid - duplicate organization",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "chronograf-ui",
|
||||
"version": "1.4.0-0",
|
||||
"version": "1.4.0-1",
|
||||
"private": false,
|
||||
"license": "AGPL-3.0",
|
||||
"description": "",
|
||||
|
|
|
@ -2,7 +2,7 @@ import _ from 'lodash'
|
|||
|
||||
import linksReducer from 'shared/reducers/links'
|
||||
|
||||
import {linksReceived} from 'shared/actions/links'
|
||||
import {linksGetCompleted} from 'shared/actions/links'
|
||||
import {noop} from 'shared/actions/app'
|
||||
|
||||
const links = {
|
||||
|
@ -25,11 +25,10 @@ const links = {
|
|||
}
|
||||
|
||||
describe('Shared.Reducers.linksReducer', () => {
|
||||
it('can handle LINKS_RECEIVED', () => {
|
||||
it('can handle LINKS_GET_COMPLETED', () => {
|
||||
const initial = linksReducer(undefined, noop())
|
||||
const actual = linksReducer(initial, linksReceived(links))
|
||||
const actual = linksReducer(initial, linksGetCompleted(links))
|
||||
const expected = links
|
||||
|
||||
expect(_.isEqual(actual, expected)).to.equal(true)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -131,7 +131,11 @@ export const createUserAsync = (url, user) => async dispatch => {
|
|||
}
|
||||
}
|
||||
|
||||
export const updateUserAsync = (user, updatedUser) => async dispatch => {
|
||||
export const updateUserAsync = (
|
||||
user,
|
||||
updatedUser,
|
||||
successMessage
|
||||
) => async dispatch => {
|
||||
dispatch(updateUser(user, updatedUser))
|
||||
try {
|
||||
// currently the request will be rejected if name, provider, or scheme, or
|
||||
|
@ -145,12 +149,7 @@ export const updateUserAsync = (user, updatedUser) => async dispatch => {
|
|||
provider: null,
|
||||
scheme: null,
|
||||
})
|
||||
dispatch(
|
||||
publishAutoDismissingNotification(
|
||||
'success',
|
||||
`User updated: ${user.scheme}::${user.provider}::${user.name}`
|
||||
)
|
||||
)
|
||||
dispatch(publishAutoDismissingNotification('success', successMessage))
|
||||
// it's not necessary to syncUser again but it's useful for good
|
||||
// measure and for the clarity of insight in the redux story
|
||||
dispatch(syncUser(user, data))
|
||||
|
@ -160,14 +159,19 @@ export const updateUserAsync = (user, updatedUser) => async dispatch => {
|
|||
}
|
||||
}
|
||||
|
||||
export const deleteUserAsync = user => async dispatch => {
|
||||
export const deleteUserAsync = (
|
||||
user,
|
||||
{isAbsoluteDelete} = {}
|
||||
) => async dispatch => {
|
||||
dispatch(removeUser(user))
|
||||
try {
|
||||
await deleteUserAJAX(user)
|
||||
dispatch(
|
||||
publishAutoDismissingNotification(
|
||||
'success',
|
||||
`User removed from organization: ${user.scheme}::${user.provider}::${user.name}`
|
||||
`${user.name} has been removed from ${isAbsoluteDelete
|
||||
? 'all organizations and deleted'
|
||||
: 'the current organization'}`
|
||||
)
|
||||
)
|
||||
} catch (error) {
|
||||
|
|
|
@ -9,9 +9,11 @@ import {
|
|||
import {Tab, Tabs, TabPanel, TabPanels, TabList} from 'shared/components/Tabs'
|
||||
import OrganizationsPage from 'src/admin/containers/chronograf/OrganizationsPage'
|
||||
import UsersPage from 'src/admin/containers/chronograf/UsersPage'
|
||||
import AllUsersPage from 'src/admin/containers/chronograf/AllUsersPage'
|
||||
|
||||
const ORGANIZATIONS_TAB_NAME = 'Organizations'
|
||||
const USERS_TAB_NAME = 'Users'
|
||||
const CURRENT_ORG_USERS_TAB_NAME = 'Current Org Users'
|
||||
const ALL_USERS_TAB_NAME = 'All Users'
|
||||
|
||||
const AdminTabs = ({
|
||||
me: {currentOrganization: meCurrentOrganization, role: meRole, id: meID},
|
||||
|
@ -26,11 +28,16 @@ const AdminTabs = ({
|
|||
},
|
||||
{
|
||||
requiredRole: ADMIN_ROLE,
|
||||
type: USERS_TAB_NAME,
|
||||
type: CURRENT_ORG_USERS_TAB_NAME,
|
||||
component: (
|
||||
<UsersPage meID={meID} meCurrentOrganization={meCurrentOrganization} />
|
||||
),
|
||||
},
|
||||
{
|
||||
requiredRole: SUPERADMIN_ROLE,
|
||||
type: ALL_USERS_TAB_NAME,
|
||||
component: <AllUsersPage meID={meID} />,
|
||||
},
|
||||
].filter(t => isUserAuthorized(meRole, t.requiredRole))
|
||||
|
||||
return (
|
||||
|
|
|
@ -0,0 +1,215 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
|
||||
import uuid from 'node-uuid'
|
||||
|
||||
import AllUsersTableHeader from 'src/admin/components/chronograf/AllUsersTableHeader'
|
||||
import AllUsersTableRowNew from 'src/admin/components/chronograf/AllUsersTableRowNew'
|
||||
import AllUsersTableRow from 'src/admin/components/chronograf/AllUsersTableRow'
|
||||
|
||||
import {ALL_USERS_TABLE} from 'src/admin/constants/chronografTableSizing'
|
||||
const {
|
||||
colOrganizations,
|
||||
colProvider,
|
||||
colScheme,
|
||||
colSuperAdmin,
|
||||
colActions,
|
||||
} = ALL_USERS_TABLE
|
||||
|
||||
class AllUsersTable extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isCreatingUser: false,
|
||||
}
|
||||
}
|
||||
|
||||
handleUpdateAuthConfig = fieldName => updatedValue => {
|
||||
const {
|
||||
actionsConfig: {updateAuthConfigAsync},
|
||||
authConfig,
|
||||
links,
|
||||
} = this.props
|
||||
const updatedAuthConfig = {
|
||||
...authConfig,
|
||||
[fieldName]: updatedValue,
|
||||
}
|
||||
updateAuthConfigAsync(links.config.auth, authConfig, updatedAuthConfig)
|
||||
}
|
||||
|
||||
handleAddToOrganization = user => organization => {
|
||||
// '*' tells the server to fill in the current defaultRole of that org
|
||||
const newRoles = user.roles.concat({
|
||||
organization: organization.id,
|
||||
name: '*',
|
||||
})
|
||||
this.props.onUpdateUserRoles(
|
||||
user,
|
||||
newRoles,
|
||||
`${user.name} has been added to ${organization.name}`
|
||||
)
|
||||
}
|
||||
|
||||
handleRemoveFromOrganization = user => role => {
|
||||
const newRoles = user.roles.filter(
|
||||
r => r.organization !== role.organization
|
||||
)
|
||||
const {name} = this.props.organizations.find(
|
||||
o => o.id === role.organization
|
||||
)
|
||||
this.props.onUpdateUserRoles(
|
||||
user,
|
||||
newRoles,
|
||||
`${user.name} has been removed from ${name}`
|
||||
)
|
||||
}
|
||||
|
||||
handleChangeSuperAdmin = user => newStatus => {
|
||||
this.props.onUpdateUserSuperAdmin(user, newStatus)
|
||||
}
|
||||
|
||||
handleClickCreateUser = () => {
|
||||
this.setState({isCreatingUser: true})
|
||||
}
|
||||
|
||||
handleBlurCreateUserRow = () => {
|
||||
this.setState({isCreatingUser: false})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
users,
|
||||
organizations,
|
||||
onCreateUser,
|
||||
authConfig,
|
||||
meID,
|
||||
notify,
|
||||
onDeleteUser,
|
||||
isLoading,
|
||||
} = this.props
|
||||
|
||||
const {isCreatingUser} = this.state
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="panel panel-default">
|
||||
<div className="panel-body">
|
||||
<div className="page-spinner" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="panel panel-default">
|
||||
<AllUsersTableHeader
|
||||
numUsers={users.length}
|
||||
numOrganizations={organizations.length}
|
||||
onClickCreateUser={this.handleClickCreateUser}
|
||||
isCreatingUser={isCreatingUser}
|
||||
authConfig={authConfig}
|
||||
onChangeAuthConfig={this.handleUpdateAuthConfig}
|
||||
/>
|
||||
<div className="panel-body">
|
||||
<table className="table table-highlight v-center chronograf-admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th
|
||||
style={{width: colOrganizations}}
|
||||
className="align-with-col-text"
|
||||
>
|
||||
Organizations
|
||||
</th>
|
||||
<th style={{width: colProvider}}>Provider</th>
|
||||
<th style={{width: colScheme}}>Scheme</th>
|
||||
<th style={{width: colSuperAdmin}} className="text-center">
|
||||
SuperAdmin
|
||||
</th>
|
||||
<th className="text-right" style={{width: colActions}} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.length
|
||||
? users.map(user =>
|
||||
<AllUsersTableRow
|
||||
user={user}
|
||||
key={uuid.v4()}
|
||||
organizations={organizations}
|
||||
onAddToOrganization={this.handleAddToOrganization}
|
||||
onRemoveFromOrganization={
|
||||
this.handleRemoveFromOrganization
|
||||
}
|
||||
onChangeSuperAdmin={this.handleChangeSuperAdmin}
|
||||
onDelete={onDeleteUser}
|
||||
meID={meID}
|
||||
/>
|
||||
)
|
||||
: <tr className="table-empty-state">
|
||||
<th colSpan="6">
|
||||
<p>No Users to display</p>
|
||||
</th>
|
||||
</tr>}
|
||||
{isCreatingUser
|
||||
? <AllUsersTableRowNew
|
||||
organizations={organizations}
|
||||
onBlur={this.handleBlurCreateUserRow}
|
||||
onCreateUser={onCreateUser}
|
||||
notify={notify}
|
||||
/>
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||
|
||||
AllUsersTable.propTypes = {
|
||||
links: shape({
|
||||
config: shape({
|
||||
auth: string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
users: arrayOf(
|
||||
shape({
|
||||
id: string,
|
||||
links: shape({
|
||||
self: string.isRequired,
|
||||
}),
|
||||
name: string.isRequired,
|
||||
provider: string.isRequired,
|
||||
roles: arrayOf(
|
||||
shape({
|
||||
name: string.isRequired,
|
||||
organization: string.isRequired,
|
||||
})
|
||||
),
|
||||
scheme: string.isRequired,
|
||||
superAdmin: bool,
|
||||
})
|
||||
).isRequired,
|
||||
organizations: arrayOf(
|
||||
shape({
|
||||
name: string.isRequired,
|
||||
id: string.isRequired,
|
||||
})
|
||||
),
|
||||
onCreateUser: func.isRequired,
|
||||
onUpdateUserRoles: func.isRequired,
|
||||
onUpdateUserSuperAdmin: func.isRequired,
|
||||
onDeleteUser: func.isRequired,
|
||||
actionsConfig: shape({
|
||||
getAuthConfigAsync: func.isRequired,
|
||||
updateAuthConfigAsync: func.isRequired,
|
||||
}),
|
||||
authConfig: shape({
|
||||
superAdminNewUsers: bool,
|
||||
}),
|
||||
meID: string.isRequired,
|
||||
notify: func.isRequired,
|
||||
isLoading: bool.isRequired,
|
||||
}
|
||||
|
||||
export default AllUsersTable
|
|
@ -0,0 +1,65 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
import SlideToggle from 'shared/components/SlideToggle'
|
||||
|
||||
const AllUsersTableHeader = ({
|
||||
numUsers,
|
||||
numOrganizations,
|
||||
onClickCreateUser,
|
||||
isCreatingUser,
|
||||
authConfig: {superAdminNewUsers},
|
||||
onChangeAuthConfig,
|
||||
}) => {
|
||||
const numUsersString = `${numUsers} User${numUsers === 1 ? '' : 's'}`
|
||||
const numOrganizationsString = `${numOrganizations} Org${numOrganizations ===
|
||||
1
|
||||
? ''
|
||||
: 's'}`
|
||||
|
||||
return (
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<h2 className="panel-title">
|
||||
{numUsersString} in {numOrganizationsString}
|
||||
</h2>
|
||||
<div style={{display: 'flex', alignItems: 'center'}}>
|
||||
<div className="all-users-admin-toggle">
|
||||
<SlideToggle
|
||||
size="xs"
|
||||
active={superAdminNewUsers}
|
||||
onToggle={onChangeAuthConfig('superAdminNewUsers')}
|
||||
/>
|
||||
<span>All new users are SuperAdmins</span>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={onClickCreateUser}
|
||||
disabled={isCreatingUser || !onClickCreateUser}
|
||||
>
|
||||
<span className="icon plus" />
|
||||
Add User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const {bool, func, number, shape} = PropTypes
|
||||
|
||||
AllUsersTableHeader.defaultProps = {
|
||||
numUsers: 0,
|
||||
numOrganizations: 0,
|
||||
isCreatingUser: false,
|
||||
}
|
||||
|
||||
AllUsersTableHeader.propTypes = {
|
||||
numUsers: number.isRequired,
|
||||
numOrganizations: number.isRequired,
|
||||
onClickCreateUser: func,
|
||||
isCreatingUser: bool.isRequired,
|
||||
onChangeAuthConfig: func.isRequired,
|
||||
authConfig: shape({
|
||||
superAdminNewUsers: bool,
|
||||
}),
|
||||
}
|
||||
|
||||
export default AllUsersTableHeader
|
|
@ -0,0 +1,114 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
import Tags from 'shared/components/Tags'
|
||||
import SlideToggle from 'shared/components/SlideToggle'
|
||||
import ConfirmButton from 'shared/components/ConfirmButton'
|
||||
|
||||
import {ALL_USERS_TABLE} from 'src/admin/constants/chronografTableSizing'
|
||||
const {
|
||||
colOrganizations,
|
||||
colProvider,
|
||||
colScheme,
|
||||
colSuperAdmin,
|
||||
colActions,
|
||||
} = ALL_USERS_TABLE
|
||||
|
||||
const AllUsersTableRow = ({
|
||||
organizations,
|
||||
user,
|
||||
onAddToOrganization,
|
||||
onRemoveFromOrganization,
|
||||
onChangeSuperAdmin,
|
||||
onDelete,
|
||||
meID,
|
||||
}) => {
|
||||
const dropdownOrganizationsItems = organizations
|
||||
.filter(o => !user.roles.find(role => role.organization === o.id))
|
||||
.map(o => ({
|
||||
...o,
|
||||
text: o.name,
|
||||
}))
|
||||
|
||||
const userIsMe = user.id === meID
|
||||
|
||||
const userOrganizations = user.roles.map(r => ({
|
||||
...r,
|
||||
name: organizations.find(o => r.organization === o.id).name,
|
||||
}))
|
||||
|
||||
const wrappedDelete = () => onDelete(user)
|
||||
|
||||
const removeWarning = userIsMe
|
||||
? 'Delete your user record\nand log yourself out?'
|
||||
: 'Delete this user?'
|
||||
|
||||
return (
|
||||
<tr className={'chronograf-admin-table--user'}>
|
||||
<td>
|
||||
{userIsMe
|
||||
? <strong className="chronograf-user--me">
|
||||
<span className="icon user" />
|
||||
{user.name}
|
||||
</strong>
|
||||
: <strong>
|
||||
{user.name}
|
||||
</strong>}
|
||||
</td>
|
||||
<td style={{width: colOrganizations}}>
|
||||
<Tags
|
||||
tags={userOrganizations}
|
||||
onDeleteTag={onRemoveFromOrganization(user)}
|
||||
emptyStateText="None"
|
||||
addMenuItems={dropdownOrganizationsItems}
|
||||
addMenuChoose={onAddToOrganization(user)}
|
||||
/>
|
||||
</td>
|
||||
<td style={{width: colProvider}}>
|
||||
{user.provider}
|
||||
</td>
|
||||
<td style={{width: colScheme}}>
|
||||
{user.scheme}
|
||||
</td>
|
||||
<td style={{width: colSuperAdmin}} className="text-center">
|
||||
<SlideToggle
|
||||
active={user.superAdmin}
|
||||
onToggle={onChangeSuperAdmin(user)}
|
||||
size="xs"
|
||||
disabled={userIsMe}
|
||||
/>
|
||||
</td>
|
||||
<td style={{textAlign: 'right', width: colActions}}>
|
||||
<ConfirmButton
|
||||
confirmText={removeWarning}
|
||||
confirmAction={wrappedDelete}
|
||||
size="btn-xs"
|
||||
text="Remove"
|
||||
customClass="table--show-on-row-hover"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
|
||||
AllUsersTableRow.propTypes = {
|
||||
user: shape(),
|
||||
organization: shape({
|
||||
name: string.isRequired,
|
||||
id: string.isRequired,
|
||||
}),
|
||||
onAddToOrganization: func.isRequired,
|
||||
onRemoveFromOrganization: func.isRequired,
|
||||
onChangeSuperAdmin: func.isRequired,
|
||||
onDelete: func.isRequired,
|
||||
meID: string.isRequired,
|
||||
organizations: arrayOf(
|
||||
shape({
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
})
|
||||
),
|
||||
}
|
||||
|
||||
export default AllUsersTableRow
|
|
@ -0,0 +1,183 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
|
||||
import {ALL_USERS_TABLE} from 'src/admin/constants/chronografTableSizing'
|
||||
const {
|
||||
colOrganizations,
|
||||
colProvider,
|
||||
colScheme,
|
||||
colSuperAdmin,
|
||||
colActions,
|
||||
} = ALL_USERS_TABLE
|
||||
|
||||
const nullOrganization = {id: undefined, name: 'None'}
|
||||
const nullRole = {name: '*', organization: undefined}
|
||||
|
||||
class AllUsersTableRowNew extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
name: '',
|
||||
provider: '',
|
||||
scheme: 'oauth2',
|
||||
role: {
|
||||
...nullRole,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
handleInputChange = fieldName => e => {
|
||||
this.setState({[fieldName]: e.target.value.trim()})
|
||||
}
|
||||
|
||||
handleConfirmCreateUser = () => {
|
||||
const {onBlur, onCreateUser} = this.props
|
||||
const {name, provider, scheme, role, superAdmin} = this.state
|
||||
const newUser = {
|
||||
name,
|
||||
provider,
|
||||
scheme,
|
||||
superAdmin,
|
||||
// since you can only choose one organization, there is only one role in a new row
|
||||
// if no organization is selected ie the "None" organization,
|
||||
// then set roles to an empty array
|
||||
roles: role.organization === undefined ? [] : [role],
|
||||
}
|
||||
onCreateUser(newUser)
|
||||
onBlur()
|
||||
}
|
||||
|
||||
handleInputFocus = e => {
|
||||
e.target.select()
|
||||
}
|
||||
|
||||
handleSelectOrganization = newOrganization => {
|
||||
// if "None" was selected for organization, create a "null role" from the predefined null role
|
||||
// else create a new role with the organization as the newOrganization's id
|
||||
const newRole =
|
||||
newOrganization.id === undefined
|
||||
? {
|
||||
...nullRole,
|
||||
}
|
||||
: {
|
||||
organization: newOrganization.id,
|
||||
name: '*', // '*' causes the server to determine the current defaultRole of the selected organization
|
||||
}
|
||||
this.setState({role: newRole})
|
||||
}
|
||||
|
||||
handleKeyDown = e => {
|
||||
const {name, provider} = this.state
|
||||
const preventCreate = !name || !provider
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
this.props.onBlur()
|
||||
}
|
||||
|
||||
if (e.key === 'Enter') {
|
||||
if (preventCreate) {
|
||||
return this.props.notify(
|
||||
'warning',
|
||||
'User must have a name and provider'
|
||||
)
|
||||
}
|
||||
this.handleConfirmCreateUser()
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {organizations, onBlur} = this.props
|
||||
const {name, provider, scheme, role} = this.state
|
||||
|
||||
const dropdownOrganizationsItems = [
|
||||
{...nullOrganization},
|
||||
...organizations,
|
||||
].map(o => ({
|
||||
...o,
|
||||
text: o.name,
|
||||
}))
|
||||
const selectedRole = dropdownOrganizationsItems.find(
|
||||
o => role.organization === o.id
|
||||
)
|
||||
|
||||
const preventCreate = !name || !provider
|
||||
|
||||
return (
|
||||
<tr className="chronograf-admin-table--new-user">
|
||||
<td>
|
||||
<input
|
||||
className="form-control input-xs"
|
||||
type="text"
|
||||
placeholder="OAuth Username..."
|
||||
autoFocus={true}
|
||||
value={name}
|
||||
onChange={this.handleInputChange('name')}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
/>
|
||||
</td>
|
||||
<td style={{width: colOrganizations}}>
|
||||
<Dropdown
|
||||
items={dropdownOrganizationsItems}
|
||||
selected={selectedRole.text}
|
||||
onChoose={this.handleSelectOrganization}
|
||||
buttonColor="btn-primary"
|
||||
buttonSize="btn-xs"
|
||||
className="dropdown-stretch"
|
||||
/>
|
||||
</td>
|
||||
<td style={{width: colProvider}}>
|
||||
<input
|
||||
className="form-control input-xs"
|
||||
type="text"
|
||||
placeholder="OAuth Provider..."
|
||||
value={provider}
|
||||
onChange={this.handleInputChange('provider')}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
/>
|
||||
</td>
|
||||
<td style={{width: colScheme}}>
|
||||
<input
|
||||
className="form-control input-xs disabled"
|
||||
type="text"
|
||||
disabled={true}
|
||||
placeholder="OAuth Scheme..."
|
||||
value={scheme}
|
||||
/>
|
||||
</td>
|
||||
<td style={{width: colSuperAdmin}} className="text-center">
|
||||
—
|
||||
</td>
|
||||
<td className="text-right" style={{width: colActions}}>
|
||||
<button className="btn btn-xs btn-square btn-info" onClick={onBlur}>
|
||||
<span className="icon remove" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-xs btn-square btn-success"
|
||||
disabled={preventCreate}
|
||||
onClick={this.handleConfirmCreateUser}
|
||||
>
|
||||
<span className="icon checkmark" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
|
||||
AllUsersTableRowNew.propTypes = {
|
||||
organizations: arrayOf(
|
||||
shape({
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
})
|
||||
),
|
||||
onBlur: func.isRequired,
|
||||
onCreateUser: func.isRequired,
|
||||
notify: func.isRequired,
|
||||
}
|
||||
|
||||
export default AllUsersTableRowNew
|
|
@ -1,46 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import UsersTableHeader from 'src/admin/components/chronograf/UsersTableHeader'
|
||||
|
||||
import Authorized, {SUPERADMIN_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import {USERS_TABLE} from 'src/admin/constants/chronografTableSizing'
|
||||
|
||||
const EmptyUsersTable = () => {
|
||||
const {
|
||||
colRole,
|
||||
colSuperAdmin,
|
||||
colProvider,
|
||||
colScheme,
|
||||
colActions,
|
||||
} = USERS_TABLE
|
||||
|
||||
return (
|
||||
<div className="panel panel-default">
|
||||
<UsersTableHeader />
|
||||
<div className="panel-body">
|
||||
<table className="table table-highlight v-center chronograf-admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th style={{width: colRole}} className="align-with-col-text">
|
||||
Role
|
||||
</th>
|
||||
<Authorized requiredRole={SUPERADMIN_ROLE}>
|
||||
<th style={{width: colSuperAdmin}} className="text-center">
|
||||
SuperAdmin
|
||||
</th>
|
||||
</Authorized>
|
||||
<th style={{width: colProvider}}>Provider</th>
|
||||
<th style={{width: colScheme}}>Scheme</th>
|
||||
<th className="text-right" style={{width: colActions}} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody />
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EmptyUsersTable
|
|
@ -2,12 +2,9 @@ import React, {Component, PropTypes} from 'react'
|
|||
|
||||
import uuid from 'node-uuid'
|
||||
|
||||
import Authorized, {SUPERADMIN_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import OrganizationsTableRow from 'src/admin/components/chronograf/OrganizationsTableRow'
|
||||
import OrganizationsTableRowNew from 'src/admin/components/chronograf/OrganizationsTableRowNew'
|
||||
import QuestionMarkTooltip from 'shared/components/QuestionMarkTooltip'
|
||||
import SlideToggle from 'shared/components/SlideToggle'
|
||||
|
||||
import {PUBLIC_TOOLTIP} from 'src/admin/constants/index'
|
||||
|
||||
|
@ -42,8 +39,6 @@ class OrganizationsTable extends Component {
|
|||
onChooseDefaultRole,
|
||||
onTogglePublic,
|
||||
currentOrganization,
|
||||
authConfig: {superAdminNewUsers},
|
||||
onChangeAuthConfig,
|
||||
} = this.props
|
||||
const {isCreatingOrganization} = this.state
|
||||
|
||||
|
@ -52,6 +47,15 @@ class OrganizationsTable extends Component {
|
|||
? ''
|
||||
: 's'}`
|
||||
|
||||
if (!organizations.length) {
|
||||
return (
|
||||
<div className="panel panel-default">
|
||||
<div className="panel-body">
|
||||
<div className="page-spinner" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="panel panel-default">
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
|
@ -94,35 +98,13 @@ class OrganizationsTable extends Component {
|
|||
currentOrganization={currentOrganization}
|
||||
/>
|
||||
)}
|
||||
<Authorized requiredRole={SUPERADMIN_ROLE}>
|
||||
<table className="table v-center superadmin-config">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{width: 70}}>Config</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{width: 70}}>
|
||||
<SlideToggle
|
||||
size="xs"
|
||||
active={superAdminNewUsers}
|
||||
onToggle={onChangeAuthConfig('superAdminNewUsers')}
|
||||
/>
|
||||
</td>
|
||||
<td>All new users are SuperAdmins</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Authorized>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
|
||||
OrganizationsTable.propTypes = {
|
||||
organizations: arrayOf(
|
||||
|
@ -140,9 +122,5 @@ OrganizationsTable.propTypes = {
|
|||
onRenameOrg: func.isRequired,
|
||||
onTogglePublic: func.isRequired,
|
||||
onChooseDefaultRole: func.isRequired,
|
||||
onChangeAuthConfig: func.isRequired,
|
||||
authConfig: shape({
|
||||
superAdminNewUsers: bool,
|
||||
}),
|
||||
}
|
||||
export default OrganizationsTable
|
||||
|
|
|
@ -2,8 +2,6 @@ import React, {Component, PropTypes} from 'react'
|
|||
|
||||
import uuid from 'node-uuid'
|
||||
|
||||
import Authorized, {SUPERADMIN_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import UsersTableHeader from 'src/admin/components/chronograf/UsersTableHeader'
|
||||
import UsersTableRowNew from 'src/admin/components/chronograf/UsersTableRowNew'
|
||||
import UsersTableRow from 'src/admin/components/chronograf/UsersTableRow'
|
||||
|
@ -23,10 +21,6 @@ class UsersTable extends Component {
|
|||
this.props.onUpdateUserRole(user, currentRole, newRole)
|
||||
}
|
||||
|
||||
handleChangeSuperAdmin = user => newStatus => {
|
||||
this.props.onUpdateUserSuperAdmin(user, newStatus)
|
||||
}
|
||||
|
||||
handleDeleteUser = user => {
|
||||
this.props.onDeleteUser(user)
|
||||
}
|
||||
|
@ -40,17 +34,27 @@ class UsersTable extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {organization, users, onCreateUser, meID, notify} = this.props
|
||||
const {
|
||||
organization,
|
||||
users,
|
||||
onCreateUser,
|
||||
meID,
|
||||
notify,
|
||||
isLoading,
|
||||
} = this.props
|
||||
|
||||
const {isCreatingUser} = this.state
|
||||
const {
|
||||
colRole,
|
||||
colSuperAdmin,
|
||||
colProvider,
|
||||
colScheme,
|
||||
colActions,
|
||||
} = USERS_TABLE
|
||||
const {colRole, colProvider, colScheme, colActions} = USERS_TABLE
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="panel panel-default">
|
||||
<div className="panel-body">
|
||||
<div className="page-spinner" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="panel panel-default">
|
||||
<UsersTableHeader
|
||||
|
@ -67,11 +71,6 @@ class UsersTable extends Component {
|
|||
<th style={{width: colRole}} className="align-with-col-text">
|
||||
Role
|
||||
</th>
|
||||
<Authorized requiredRole={SUPERADMIN_ROLE}>
|
||||
<th style={{width: colSuperAdmin}} className="text-center">
|
||||
SuperAdmin
|
||||
</th>
|
||||
</Authorized>
|
||||
<th style={{width: colProvider}}>Provider</th>
|
||||
<th style={{width: colScheme}}>Scheme</th>
|
||||
<th className="text-right" style={{width: colActions}} />
|
||||
|
@ -86,31 +85,21 @@ class UsersTable extends Component {
|
|||
notify={notify}
|
||||
/>
|
||||
: null}
|
||||
{users.length || !isCreatingUser
|
||||
{users.length
|
||||
? users.map(user =>
|
||||
<UsersTableRow
|
||||
user={user}
|
||||
key={uuid.v4()}
|
||||
organization={organization}
|
||||
onChangeUserRole={this.handleChangeUserRole}
|
||||
onChangeSuperAdmin={this.handleChangeSuperAdmin}
|
||||
onDelete={this.handleDeleteUser}
|
||||
meID={meID}
|
||||
/>
|
||||
)
|
||||
: <tr className="table-empty-state">
|
||||
<Authorized
|
||||
requiredRole={SUPERADMIN_ROLE}
|
||||
replaceWithIfNotAuthorized={
|
||||
<th colSpan="5">
|
||||
<p>No Users to display</p>
|
||||
</th>
|
||||
}
|
||||
>
|
||||
<th colSpan="6">
|
||||
<p>No Users to display</p>
|
||||
</th>
|
||||
</Authorized>
|
||||
<th colSpan="5">
|
||||
<p>No Users to display</p>
|
||||
</th>
|
||||
</tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -138,7 +127,6 @@ UsersTable.propTypes = {
|
|||
})
|
||||
),
|
||||
scheme: string.isRequired,
|
||||
superAdmin: bool,
|
||||
})
|
||||
).isRequired,
|
||||
organization: shape({
|
||||
|
@ -147,10 +135,10 @@ UsersTable.propTypes = {
|
|||
}),
|
||||
onCreateUser: func.isRequired,
|
||||
onUpdateUserRole: func.isRequired,
|
||||
onUpdateUserSuperAdmin: func.isRequired,
|
||||
onDeleteUser: func.isRequired,
|
||||
meID: string.isRequired,
|
||||
notify: func.isRequired,
|
||||
isLoading: bool.isRequired,
|
||||
}
|
||||
|
||||
export default UsersTable
|
||||
|
|
|
@ -26,7 +26,7 @@ class UsersTableHeader extends Component {
|
|||
disabled={isCreatingUser || !onClickCreateUser}
|
||||
>
|
||||
<span className="icon plus" />
|
||||
Create User
|
||||
Add User
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
import Authorized, {SUPERADMIN_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
import SlideToggle from 'shared/components/SlideToggle'
|
||||
import DeleteConfirmTableCell from 'shared/components/DeleteConfirmTableCell'
|
||||
|
||||
import {USER_ROLES} from 'src/admin/constants/chronografAdmin'
|
||||
|
@ -13,11 +10,10 @@ const UsersTableRow = ({
|
|||
user,
|
||||
organization,
|
||||
onChangeUserRole,
|
||||
onChangeSuperAdmin,
|
||||
onDelete,
|
||||
meID,
|
||||
}) => {
|
||||
const {colRole, colSuperAdmin, colProvider, colScheme} = USERS_TABLE
|
||||
const {colRole, colProvider, colScheme} = USERS_TABLE
|
||||
|
||||
const dropdownRolesItems = USER_ROLES.map(r => ({
|
||||
...r,
|
||||
|
@ -53,16 +49,6 @@ const UsersTableRow = ({
|
|||
/>
|
||||
</span>
|
||||
</td>
|
||||
<Authorized requiredRole={SUPERADMIN_ROLE}>
|
||||
<td style={{width: colSuperAdmin}} className="text-center">
|
||||
<SlideToggle
|
||||
active={user.superAdmin}
|
||||
onToggle={onChangeSuperAdmin(user)}
|
||||
size="xs"
|
||||
disabled={userIsMe}
|
||||
/>
|
||||
</td>
|
||||
</Authorized>
|
||||
<td style={{width: colProvider}}>
|
||||
{user.provider}
|
||||
</td>
|
||||
|
@ -89,7 +75,6 @@ UsersTableRow.propTypes = {
|
|||
id: string.isRequired,
|
||||
}),
|
||||
onChangeUserRole: func.isRequired,
|
||||
onChangeSuperAdmin: func.isRequired,
|
||||
onDelete: func.isRequired,
|
||||
meID: string.isRequired,
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
|
||||
import Authorized, {SUPERADMIN_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
|
||||
import {USERS_TABLE} from 'src/admin/constants/chronografTableSizing'
|
||||
|
@ -25,13 +23,12 @@ class UsersTableRowNew extends Component {
|
|||
|
||||
handleConfirmCreateUser = () => {
|
||||
const {onBlur, onCreateUser, organization} = this.props
|
||||
const {name, provider, scheme, role, superAdmin} = this.state
|
||||
const {name, provider, scheme, role} = this.state
|
||||
|
||||
const newUser = {
|
||||
name,
|
||||
provider,
|
||||
scheme,
|
||||
superAdmin,
|
||||
roles: [
|
||||
{
|
||||
name: role,
|
||||
|
@ -72,13 +69,7 @@ class UsersTableRowNew extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
colRole,
|
||||
colProvider,
|
||||
colScheme,
|
||||
colSuperAdmin,
|
||||
colActions,
|
||||
} = USERS_TABLE
|
||||
const {colRole, colProvider, colScheme, colActions} = USERS_TABLE
|
||||
const {onBlur} = this.props
|
||||
const {name, provider, scheme, role} = this.state
|
||||
|
||||
|
@ -108,11 +99,6 @@ class UsersTableRowNew extends Component {
|
|||
className="dropdown-stretch"
|
||||
/>
|
||||
</td>
|
||||
<Authorized requiredRole={SUPERADMIN_ROLE}>
|
||||
<td style={{width: colSuperAdmin}} className="text-center">
|
||||
—
|
||||
</td>
|
||||
</Authorized>
|
||||
<td style={{width: colProvider}}>
|
||||
<input
|
||||
className="form-control input-xs"
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
export const USERS_TABLE = {
|
||||
colRole: 120,
|
||||
colOrganizations: 200,
|
||||
colSuperAdmin: 90,
|
||||
colProvider: 170,
|
||||
colScheme: 90,
|
||||
colActions: 80,
|
||||
}
|
||||
|
||||
export const ALL_USERS_TABLE = {
|
||||
colOrganizations: 320,
|
||||
colSuperAdmin: 90,
|
||||
colProvider: 100,
|
||||
colScheme: 90,
|
||||
colActions: 50,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import {bindActionCreators} from 'redux'
|
||||
|
||||
import * as adminChronografActionCreators from 'src/admin/actions/chronograf'
|
||||
import * as configActionCreators from 'shared/actions/config'
|
||||
import {publishAutoDismissingNotification} from 'shared/dispatchers'
|
||||
|
||||
import AllUsersTable from 'src/admin/components/chronograf/AllUsersTable'
|
||||
|
||||
class AllUsersPage extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isLoading: true,
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {links, actionsConfig: {getAuthConfigAsync}} = this.props
|
||||
getAuthConfigAsync(links.config.auth)
|
||||
}
|
||||
|
||||
handleCreateUser = user => {
|
||||
const {links, actionsAdmin: {createUserAsync}} = this.props
|
||||
createUserAsync(links.allUsers, user)
|
||||
}
|
||||
|
||||
handleUpdateUserRoles = (user, roles, successMessage) => {
|
||||
const {actionsAdmin: {updateUserAsync}} = this.props
|
||||
const updatedUser = {...user, roles}
|
||||
updateUserAsync(user, updatedUser, successMessage)
|
||||
}
|
||||
|
||||
handleUpdateUserSuperAdmin = (user, superAdmin) => {
|
||||
const {actionsAdmin: {updateUserAsync}} = this.props
|
||||
const updatedUser = {...user, superAdmin}
|
||||
updateUserAsync(
|
||||
user,
|
||||
updatedUser,
|
||||
`${user.name}'s SuperAdmin status has been updated`
|
||||
)
|
||||
}
|
||||
|
||||
handleDeleteUser = user => {
|
||||
const {actionsAdmin: {deleteUserAsync}} = this.props
|
||||
deleteUserAsync(user, {isAbsoluteDelete: true})
|
||||
}
|
||||
|
||||
async componentWillMount() {
|
||||
const {
|
||||
links,
|
||||
actionsAdmin: {loadOrganizationsAsync, loadUsersAsync},
|
||||
} = this.props
|
||||
|
||||
this.setState({isLoading: true})
|
||||
|
||||
await Promise.all([
|
||||
loadOrganizationsAsync(links.organizations),
|
||||
loadUsersAsync(links.allUsers),
|
||||
])
|
||||
|
||||
this.setState({isLoading: false})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
organizations,
|
||||
meID,
|
||||
users,
|
||||
authConfig,
|
||||
actionsConfig,
|
||||
links,
|
||||
notify,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<AllUsersTable
|
||||
meID={meID}
|
||||
users={users}
|
||||
organizations={organizations}
|
||||
onCreateUser={this.handleCreateUser}
|
||||
onUpdateUserRoles={this.handleUpdateUserRoles}
|
||||
onUpdateUserSuperAdmin={this.handleUpdateUserSuperAdmin}
|
||||
onDeleteUser={this.handleDeleteUser}
|
||||
links={links}
|
||||
authConfig={authConfig}
|
||||
actionsConfig={actionsConfig}
|
||||
notify={notify}
|
||||
isLoading={this.state.isLoading}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||
|
||||
AllUsersPage.propTypes = {
|
||||
links: shape({
|
||||
users: string.isRequired,
|
||||
config: shape({
|
||||
auth: string.isRequired,
|
||||
}).isRequired,
|
||||
}),
|
||||
meID: string.isRequired,
|
||||
users: arrayOf(shape),
|
||||
organizations: arrayOf(shape),
|
||||
actionsAdmin: shape({
|
||||
loadUsersAsync: func.isRequired,
|
||||
loadOrganizationsAsync: func.isRequired,
|
||||
createUserAsync: func.isRequired,
|
||||
updateUserAsync: func.isRequired,
|
||||
deleteUserAsync: func.isRequired,
|
||||
}),
|
||||
actionsConfig: shape({
|
||||
getAuthConfigAsync: func.isRequired,
|
||||
updateAuthConfigAsync: func.isRequired,
|
||||
}),
|
||||
authConfig: shape({
|
||||
superAdminNewUsers: bool,
|
||||
}),
|
||||
notify: func.isRequired,
|
||||
}
|
||||
|
||||
const mapStateToProps = ({
|
||||
links,
|
||||
adminChronograf: {organizations, users},
|
||||
config: {auth: authConfig},
|
||||
}) => ({
|
||||
links,
|
||||
organizations,
|
||||
users,
|
||||
authConfig,
|
||||
})
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
actionsAdmin: bindActionCreators(adminChronografActionCreators, dispatch),
|
||||
actionsConfig: bindActionCreators(configActionCreators, dispatch),
|
||||
notify: bindActionCreators(publishAutoDismissingNotification, dispatch),
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AllUsersPage)
|
|
@ -3,20 +3,14 @@ import {connect} from 'react-redux'
|
|||
import {bindActionCreators} from 'redux'
|
||||
|
||||
import * as adminChronografActionCreators from 'src/admin/actions/chronograf'
|
||||
import * as configActionCreators from 'shared/actions/config'
|
||||
import {getMeAsync} from 'shared/actions/auth'
|
||||
|
||||
import OrganizationsTable from 'src/admin/components/chronograf/OrganizationsTable'
|
||||
|
||||
class OrganizationsPage extends Component {
|
||||
componentDidMount() {
|
||||
const {
|
||||
links,
|
||||
actionsAdmin: {loadOrganizationsAsync},
|
||||
actionsConfig: {getAuthConfigAsync},
|
||||
} = this.props
|
||||
const {links, actionsAdmin: {loadOrganizationsAsync}} = this.props
|
||||
loadOrganizationsAsync(links.organizations)
|
||||
getAuthConfigAsync(links.config.auth)
|
||||
}
|
||||
|
||||
handleCreateOrganization = async organization => {
|
||||
|
@ -57,44 +51,29 @@ class OrganizationsPage extends Component {
|
|||
this.refreshMe()
|
||||
}
|
||||
|
||||
handleUpdateAuthConfig = fieldName => updatedValue => {
|
||||
const {
|
||||
actionsConfig: {updateAuthConfigAsync},
|
||||
authConfig,
|
||||
links,
|
||||
} = this.props
|
||||
const updatedAuthConfig = {
|
||||
...authConfig,
|
||||
[fieldName]: updatedValue,
|
||||
}
|
||||
updateAuthConfigAsync(links.config.auth, authConfig, updatedAuthConfig)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {meCurrentOrganization, organizations, authConfig, me} = this.props
|
||||
const {meCurrentOrganization, organizations, me} = this.props
|
||||
|
||||
const organization = organizations.find(
|
||||
o => o.id === meCurrentOrganization.id
|
||||
)
|
||||
|
||||
return organizations.length
|
||||
? <OrganizationsTable
|
||||
organizations={organizations}
|
||||
currentOrganization={organization}
|
||||
onCreateOrg={this.handleCreateOrganization}
|
||||
onDeleteOrg={this.handleDeleteOrganization}
|
||||
onRenameOrg={this.handleRenameOrganization}
|
||||
onTogglePublic={this.handleTogglePublic}
|
||||
onChooseDefaultRole={this.handleChooseDefaultRole}
|
||||
authConfig={authConfig}
|
||||
onChangeAuthConfig={this.handleUpdateAuthConfig}
|
||||
me={me}
|
||||
/>
|
||||
: <div className="page-spinner" />
|
||||
return (
|
||||
<OrganizationsTable
|
||||
organizations={organizations}
|
||||
currentOrganization={organization}
|
||||
onCreateOrg={this.handleCreateOrganization}
|
||||
onDeleteOrg={this.handleDeleteOrganization}
|
||||
onRenameOrg={this.handleRenameOrganization}
|
||||
onTogglePublic={this.handleTogglePublic}
|
||||
onChooseDefaultRole={this.handleChooseDefaultRole}
|
||||
me={me}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
|
||||
OrganizationsPage.propTypes = {
|
||||
links: shape({
|
||||
|
@ -116,18 +95,11 @@ OrganizationsPage.propTypes = {
|
|||
updateOrganizationAsync: func.isRequired,
|
||||
deleteOrganizationAsync: func.isRequired,
|
||||
}),
|
||||
actionsConfig: shape({
|
||||
getAuthConfigAsync: func.isRequired,
|
||||
updateAuthConfigAsync: func.isRequired,
|
||||
}),
|
||||
getMe: func.isRequired,
|
||||
meCurrentOrganization: shape({
|
||||
name: string.isRequired,
|
||||
id: string.isRequired,
|
||||
}),
|
||||
authConfig: shape({
|
||||
superAdminNewUsers: bool,
|
||||
}),
|
||||
me: shape({
|
||||
organizations: arrayOf(
|
||||
shape({
|
||||
|
@ -142,18 +114,15 @@ OrganizationsPage.propTypes = {
|
|||
const mapStateToProps = ({
|
||||
links,
|
||||
adminChronograf: {organizations},
|
||||
config: {auth: authConfig},
|
||||
auth: {me},
|
||||
}) => ({
|
||||
links,
|
||||
organizations,
|
||||
authConfig,
|
||||
me,
|
||||
})
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
actionsAdmin: bindActionCreators(adminChronografActionCreators, dispatch),
|
||||
actionsConfig: bindActionCreators(configActionCreators, dispatch),
|
||||
getMe: bindActionCreators(getMeAsync, dispatch),
|
||||
})
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import {bindActionCreators} from 'redux'
|
|||
import * as adminChronografActionCreators from 'src/admin/actions/chronograf'
|
||||
import {publishAutoDismissingNotification} from 'shared/dispatchers'
|
||||
|
||||
import EmptyUsersTable from 'src/admin/components/chronograf/EmptyUsersTable'
|
||||
import UsersTable from 'src/admin/components/chronograf/UsersTable'
|
||||
|
||||
class UsersPage extends Component {
|
||||
|
@ -28,18 +27,16 @@ class UsersPage extends Component {
|
|||
const newRoles = user.roles.map(
|
||||
r => (r.organization === currentRole.organization ? updatedRole : r)
|
||||
)
|
||||
updateUserAsync(user, {...user, roles: newRoles})
|
||||
}
|
||||
|
||||
handleUpdateUserSuperAdmin = (user, superAdmin) => {
|
||||
const {actions: {updateUserAsync}} = this.props
|
||||
const updatedUser = {...user, superAdmin}
|
||||
updateUserAsync(user, updatedUser)
|
||||
updateUserAsync(
|
||||
user,
|
||||
{...user, roles: newRoles},
|
||||
`${user.name} is now a ${name}`
|
||||
)
|
||||
}
|
||||
|
||||
handleDeleteUser = user => {
|
||||
const {actions: {deleteUserAsync}} = this.props
|
||||
deleteUserAsync(user)
|
||||
deleteUserAsync(user, {isAbsoluteDelete: false})
|
||||
}
|
||||
|
||||
async componentWillMount() {
|
||||
|
@ -68,10 +65,6 @@ class UsersPage extends Component {
|
|||
} = this.props
|
||||
const {isLoading} = this.state
|
||||
|
||||
if (isLoading) {
|
||||
return <EmptyUsersTable />
|
||||
}
|
||||
|
||||
const organization = organizations.find(
|
||||
o => o.id === meCurrentOrganization.id
|
||||
)
|
||||
|
@ -83,9 +76,9 @@ class UsersPage extends Component {
|
|||
organization={organization}
|
||||
onCreateUser={this.handleCreateUser}
|
||||
onUpdateUserRole={this.handleUpdateUserRole}
|
||||
onUpdateUserSuperAdmin={this.handleUpdateUserSuperAdmin}
|
||||
onDeleteUser={this.handleDeleteUser}
|
||||
notify={notify}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ const initialState = {
|
|||
users: [],
|
||||
organizations: [],
|
||||
authConfig: {
|
||||
superAdminNewUsers: true,
|
||||
superAdminNewUsers: false,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ const adminChronograf = (state = initialState, action) => {
|
|||
|
||||
case 'CHRONOGRAF_ADD_USER': {
|
||||
const {user} = action.payload
|
||||
return {...state, users: [user, ...state.users]}
|
||||
return {...state, users: [...state.users, user]}
|
||||
}
|
||||
|
||||
case 'CHRONOGRAF_UPDATE_USER': {
|
||||
|
|
|
@ -76,6 +76,10 @@ class CellEditorOverlay extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
this.overlayRef.focus()
|
||||
}
|
||||
|
||||
handleAddThreshold = () => {
|
||||
const {colors, cellWorkingType} = this.state
|
||||
const sortedColors = _.sortBy(colors, color => Number(color.value))
|
||||
|
@ -442,6 +446,23 @@ class CellEditorOverlay extends Component {
|
|||
return prevQuery.source
|
||||
}
|
||||
|
||||
handleKeyDown = e => {
|
||||
if (e.key === 'Enter' && e.metaKey && e.target === this.overlayRef) {
|
||||
this.handleSaveCell()
|
||||
}
|
||||
if (e.key === 'Enter' && e.metaKey && e.target !== this.overlayRef) {
|
||||
e.target.blur()
|
||||
setTimeout(this.handleSaveCell, 50)
|
||||
}
|
||||
if (e.key === 'Escape' && e.target === this.overlayRef) {
|
||||
this.props.onCancel()
|
||||
}
|
||||
if (e.key === 'Escape' && e.target !== this.overlayRef) {
|
||||
e.target.blur()
|
||||
this.overlayRef.focus()
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
onCancel,
|
||||
|
@ -472,7 +493,12 @@ class CellEditorOverlay extends Component {
|
|||
!!query.rawText
|
||||
|
||||
return (
|
||||
<div className={OVERLAY_TECHNOLOGY}>
|
||||
<div
|
||||
className={OVERLAY_TECHNOLOGY}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
tabIndex="0"
|
||||
ref={r => (this.overlayRef = r)}
|
||||
>
|
||||
<ResizeContainer
|
||||
containerClass="resizer--full-size"
|
||||
minTopHeight={MINIMUM_HEIGHTS.visualization}
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import React, {PropTypes, Component} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import {bindActionCreators} from 'redux'
|
||||
import _ from 'lodash'
|
||||
|
||||
import HostsTable from 'src/hosts/components/HostsTable'
|
||||
import SourceIndicator from 'shared/components/SourceIndicator'
|
||||
import AutoRefreshDropdown from 'shared/components/AutoRefreshDropdown'
|
||||
import ManualRefresh from 'src/shared/components/ManualRefresh'
|
||||
|
||||
import {getCpuAndLoadForHosts, getLayouts, getAppsForHosts} from '../apis'
|
||||
import {getEnv} from 'src/shared/apis/env'
|
||||
import {setAutoRefresh} from 'shared/actions/app'
|
||||
|
||||
class HostsPage extends Component {
|
||||
constructor(props) {
|
||||
|
@ -19,59 +23,26 @@ class HostsPage extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
async fetchHostsData() {
|
||||
const {source, links, addFlashMessage} = this.props
|
||||
|
||||
const {telegrafSystemInterval} = await getEnv(links.environment)
|
||||
|
||||
const hostsError = 'Unable to get apps for hosts'
|
||||
let hosts, layouts
|
||||
|
||||
try {
|
||||
const [h, {data}] = await Promise.all([
|
||||
getCpuAndLoadForHosts(
|
||||
source.links.proxy,
|
||||
source.telegraf,
|
||||
telegrafSystemInterval
|
||||
),
|
||||
getLayouts(),
|
||||
new Promise(resolve => {
|
||||
this.setState({hostsLoading: true})
|
||||
resolve()
|
||||
}),
|
||||
])
|
||||
|
||||
hosts = h
|
||||
layouts = data.layouts
|
||||
|
||||
this.setState({
|
||||
hosts,
|
||||
hostsLoading: false,
|
||||
})
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
hostsError: error.toString(),
|
||||
hostsLoading: false,
|
||||
})
|
||||
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
if (!hosts || !layouts) {
|
||||
addFlashMessage({type: 'error', text: hostsError})
|
||||
return this.setState({
|
||||
hostsError,
|
||||
hostsLoading: false,
|
||||
})
|
||||
}
|
||||
|
||||
const hostsError = 'Unable to get hosts'
|
||||
try {
|
||||
const hosts = await getCpuAndLoadForHosts(
|
||||
source.links.proxy,
|
||||
source.telegraf,
|
||||
telegrafSystemInterval
|
||||
)
|
||||
if (!hosts) {
|
||||
throw new Error(hostsError)
|
||||
}
|
||||
const newHosts = await getAppsForHosts(
|
||||
source.links.proxy,
|
||||
hosts,
|
||||
layouts,
|
||||
this.layouts,
|
||||
source.telegraf
|
||||
)
|
||||
|
||||
this.setState({
|
||||
hosts: newHosts,
|
||||
hostsError: '',
|
||||
|
@ -87,8 +58,50 @@ class HostsPage extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const {addFlashMessage, autoRefresh} = this.props
|
||||
|
||||
this.setState({hostsLoading: true}) // Only print this once
|
||||
const {data} = await getLayouts()
|
||||
this.layouts = data.layouts
|
||||
if (!this.layouts) {
|
||||
const layoutError = 'Unable to get apps for hosts'
|
||||
addFlashMessage({type: 'error', text: layoutError})
|
||||
this.setState({
|
||||
hostsError: layoutError,
|
||||
hostsLoading: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
await this.fetchHostsData()
|
||||
if (autoRefresh) {
|
||||
this.intervalID = setInterval(() => this.fetchHostsData(), autoRefresh)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.manualRefresh !== nextProps.manualRefresh) {
|
||||
this.fetchHostsData()
|
||||
}
|
||||
if (this.props.autoRefresh !== nextProps.autoRefresh) {
|
||||
clearInterval(this.intervalID)
|
||||
|
||||
if (nextProps.autoRefresh) {
|
||||
this.intervalID = setInterval(
|
||||
() => this.fetchHostsData(),
|
||||
nextProps.autoRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {source} = this.props
|
||||
const {
|
||||
source,
|
||||
autoRefresh,
|
||||
onChooseAutoRefresh,
|
||||
onManualRefresh,
|
||||
} = this.props
|
||||
const {hosts, hostsLoading, hostsError} = this.state
|
||||
return (
|
||||
<div className="page hosts-list-page">
|
||||
|
@ -99,6 +112,12 @@ class HostsPage extends Component {
|
|||
</div>
|
||||
<div className="page-header__right">
|
||||
<SourceIndicator />
|
||||
<AutoRefreshDropdown
|
||||
iconName="refresh"
|
||||
selected={autoRefresh}
|
||||
onChoose={onChooseAutoRefresh}
|
||||
onManualRefresh={onManualRefresh}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -119,13 +138,20 @@ class HostsPage extends Component {
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.intervalID)
|
||||
this.intervalID = false
|
||||
}
|
||||
}
|
||||
|
||||
const {func, shape, string} = PropTypes
|
||||
const {func, shape, string, number} = PropTypes
|
||||
|
||||
const mapStateToProps = ({links}) => {
|
||||
const mapStateToProps = state => {
|
||||
const {app: {persisted: {autoRefresh}}, links} = state
|
||||
return {
|
||||
links,
|
||||
autoRefresh,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -143,6 +169,20 @@ HostsPage.propTypes = {
|
|||
environment: string.isRequired,
|
||||
}),
|
||||
addFlashMessage: func,
|
||||
autoRefresh: number.isRequired,
|
||||
manualRefresh: number,
|
||||
onChooseAutoRefresh: func.isRequired,
|
||||
onManualRefresh: func.isRequired,
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(HostsPage)
|
||||
HostsPage.defaultProps = {
|
||||
manualRefresh: 0,
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onChooseAutoRefresh: bindActionCreators(setAutoRefresh, dispatch),
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(
|
||||
ManualRefresh(HostsPage)
|
||||
)
|
||||
|
|
|
@ -35,6 +35,7 @@ import {AdminChronografPage, AdminInfluxDBPage} from 'src/admin'
|
|||
import {SourcePage, ManageSources} from 'src/sources'
|
||||
import NotFound from 'shared/components/NotFound'
|
||||
|
||||
import {getLinksAsync} from 'shared/actions/links'
|
||||
import {getMeAsync} from 'shared/actions/auth'
|
||||
|
||||
import {disablePresentationMode} from 'shared/actions/app'
|
||||
|
@ -73,11 +74,20 @@ window.addEventListener('keyup', event => {
|
|||
const history = syncHistoryWithStore(browserHistory, store)
|
||||
|
||||
const Root = React.createClass({
|
||||
componentWillMount() {
|
||||
async componentWillMount() {
|
||||
this.flushErrorsQueue()
|
||||
this.checkAuth()
|
||||
|
||||
try {
|
||||
await this.getLinks()
|
||||
this.checkAuth()
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
}
|
||||
},
|
||||
|
||||
getLinks: bindActionCreators(getLinksAsync, dispatch),
|
||||
getMe: bindActionCreators(getMeAsync, dispatch),
|
||||
|
||||
async checkAuth() {
|
||||
try {
|
||||
await this.performHeartbeat({shouldResetMe: true})
|
||||
|
@ -86,8 +96,6 @@ const Root = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
getMe: bindActionCreators(getMeAsync, dispatch),
|
||||
|
||||
async performHeartbeat({shouldResetMe = false} = {}) {
|
||||
await this.getMe({shouldResetMe})
|
||||
|
||||
|
@ -133,6 +141,10 @@ const Root = React.createClass({
|
|||
<Route path="tickscript/:ruleID" component={TickscriptPage} />
|
||||
<Route path="kapacitors/new" component={KapacitorPage} />
|
||||
<Route path="kapacitors/:id/edit" component={KapacitorPage} />
|
||||
<Route
|
||||
path="kapacitors/:id/edit:hash"
|
||||
component={KapacitorPage}
|
||||
/>
|
||||
<Route path="kapacitor-tasks" component={KapacitorTasksPage} />
|
||||
<Route path="admin-chronograf" component={AdminChronografPage} />
|
||||
<Route path="admin-influxdb" component={AdminInfluxDBPage} />
|
||||
|
|
|
@ -195,16 +195,10 @@ export const updateRuleStatus = (rule, status) => dispatch => {
|
|||
})
|
||||
}
|
||||
|
||||
export const createTask = (
|
||||
kapacitor,
|
||||
task,
|
||||
router,
|
||||
sourceID
|
||||
) => async dispatch => {
|
||||
export const createTask = (kapacitor, task) => async dispatch => {
|
||||
try {
|
||||
const {data} = await createTaskAJAX(kapacitor, task)
|
||||
router.push(`/sources/${sourceID}/alert-rules`)
|
||||
dispatch(publishNotification('success', 'You made a TICKscript!'))
|
||||
dispatch(publishNotification('success', 'TICKscript successfully created'))
|
||||
return data
|
||||
} catch (error) {
|
||||
if (!error) {
|
||||
|
@ -220,20 +214,17 @@ export const updateTask = (
|
|||
kapacitor,
|
||||
task,
|
||||
ruleID,
|
||||
router,
|
||||
sourceID
|
||||
) => async dispatch => {
|
||||
try {
|
||||
const {data} = await updateTaskAJAX(kapacitor, task, ruleID, sourceID)
|
||||
router.push(`/sources/${sourceID}/alert-rules`)
|
||||
dispatch(publishNotification('success', 'TICKscript updated successully'))
|
||||
dispatch(publishNotification('success', 'TICKscript saved'))
|
||||
return data
|
||||
} catch (error) {
|
||||
if (!error) {
|
||||
dispatch(errorThrown('Could not communicate with server'))
|
||||
return
|
||||
}
|
||||
|
||||
return error.data
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,11 @@ import React, {Component, PropTypes} from 'react'
|
|||
import _ from 'lodash'
|
||||
|
||||
import {Tab, Tabs, TabPanel, TabPanels, TabList} from 'shared/components/Tabs'
|
||||
import {getKapacitorConfig, updateKapacitorConfigSection} from 'shared/apis'
|
||||
import {
|
||||
getKapacitorConfig,
|
||||
updateKapacitorConfigSection,
|
||||
testAlertOutput,
|
||||
} from 'shared/apis'
|
||||
|
||||
import {
|
||||
AlertaConfig,
|
||||
|
@ -23,7 +27,6 @@ class AlertTabs extends Component {
|
|||
super(props)
|
||||
|
||||
this.state = {
|
||||
selectedHandler: 'smtp',
|
||||
configSections: null,
|
||||
}
|
||||
}
|
||||
|
@ -38,18 +41,17 @@ class AlertTabs extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
refreshKapacitorConfig = kapacitor => {
|
||||
getKapacitorConfig(kapacitor)
|
||||
.then(({data: {sections}}) => {
|
||||
this.setState({configSections: sections})
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({configSections: null})
|
||||
this.props.addFlashMessage({
|
||||
type: 'error',
|
||||
text: 'There was an error getting the Kapacitor config',
|
||||
})
|
||||
refreshKapacitorConfig = async kapacitor => {
|
||||
try {
|
||||
const {data: {sections}} = await getKapacitorConfig(kapacitor)
|
||||
this.setState({configSections: sections})
|
||||
} catch (error) {
|
||||
this.setState({configSections: null})
|
||||
this.props.addFlashMessage({
|
||||
type: 'error',
|
||||
text: 'There was an error getting the Kapacitor config',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
getSection = (sections, section) => {
|
||||
|
@ -68,23 +70,53 @@ class AlertTabs extends Component {
|
|||
return this.getSection(sections, section)
|
||||
}
|
||||
|
||||
handleSaveConfig = section => properties => {
|
||||
handleSaveConfig = section => async properties => {
|
||||
if (section !== '') {
|
||||
const propsToSend = this.sanitizeProperties(section, properties)
|
||||
updateKapacitorConfigSection(this.props.kapacitor, section, propsToSend)
|
||||
.then(() => {
|
||||
this.refreshKapacitorConfig(this.props.kapacitor)
|
||||
this.props.addFlashMessage({
|
||||
type: 'success',
|
||||
text: `Alert for ${section} successfully saved`,
|
||||
})
|
||||
try {
|
||||
await updateKapacitorConfigSection(
|
||||
this.props.kapacitor,
|
||||
section,
|
||||
propsToSend
|
||||
)
|
||||
this.refreshKapacitorConfig(this.props.kapacitor)
|
||||
this.props.addFlashMessage({
|
||||
type: 'success',
|
||||
text: `Alert configuration for ${section} successfully saved.`,
|
||||
})
|
||||
.catch(() => {
|
||||
this.props.addFlashMessage({
|
||||
type: 'error',
|
||||
text: 'There was an error saving the kapacitor config',
|
||||
})
|
||||
return true
|
||||
} catch ({data: {error}}) {
|
||||
const errorMsg = _.join(_.drop(_.split(error, ': '), 2), ': ')
|
||||
this.props.addFlashMessage({
|
||||
type: 'error',
|
||||
text: `There was an error saving the alert configuration for ${section}: ${errorMsg}`,
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleTestConfig = section => async e => {
|
||||
e.preventDefault()
|
||||
|
||||
try {
|
||||
const {data} = await testAlertOutput(this.props.kapacitor, section)
|
||||
if (data.success) {
|
||||
this.props.addFlashMessage({
|
||||
type: 'success',
|
||||
text: `Successfully triggered an alert to ${section}. If the alert does not reach its destination, please check your configuration settings.`,
|
||||
})
|
||||
} else {
|
||||
this.props.addFlashMessage({
|
||||
type: 'error',
|
||||
text: `There was an error sending an alert to ${section}: ${data.message}`,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
this.props.addFlashMessage({
|
||||
type: 'error',
|
||||
text: `There was an error sending an alert to ${section}.`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -102,8 +134,14 @@ class AlertTabs extends Component {
|
|||
return cleanProps
|
||||
}
|
||||
|
||||
getInitialIndex = (supportedConfigs, hash) => {
|
||||
const index = _.indexOf(_.keys(supportedConfigs), _.replace(hash, '#', ''))
|
||||
return index >= 0 ? index : 0
|
||||
}
|
||||
|
||||
render() {
|
||||
const {configSections} = this.state
|
||||
const {hash} = this.props
|
||||
|
||||
if (!configSections) {
|
||||
return null
|
||||
|
@ -116,6 +154,8 @@ class AlertTabs extends Component {
|
|||
<AlertaConfig
|
||||
onSave={this.handleSaveConfig('alerta')}
|
||||
config={this.getSection(configSections, 'alerta')}
|
||||
onTest={this.handleTestConfig('alerta')}
|
||||
enabled={this.getEnabled(configSections, 'alerta')}
|
||||
/>,
|
||||
},
|
||||
hipchat: {
|
||||
|
@ -125,6 +165,8 @@ class AlertTabs extends Component {
|
|||
<HipChatConfig
|
||||
onSave={this.handleSaveConfig('hipchat')}
|
||||
config={this.getSection(configSections, 'hipchat')}
|
||||
onTest={this.handleTestConfig('hipchat')}
|
||||
enabled={this.getEnabled(configSections, 'hipchat')}
|
||||
/>,
|
||||
},
|
||||
opsgenie: {
|
||||
|
@ -134,6 +176,8 @@ class AlertTabs extends Component {
|
|||
<OpsGenieConfig
|
||||
onSave={this.handleSaveConfig('opsgenie')}
|
||||
config={this.getSection(configSections, 'opsgenie')}
|
||||
onTest={this.handleTestConfig('opsgenie')}
|
||||
enabled={this.getEnabled(configSections, 'opsgenie')}
|
||||
/>,
|
||||
},
|
||||
pagerduty: {
|
||||
|
@ -143,6 +187,8 @@ class AlertTabs extends Component {
|
|||
<PagerDutyConfig
|
||||
onSave={this.handleSaveConfig('pagerduty')}
|
||||
config={this.getSection(configSections, 'pagerduty')}
|
||||
onTest={this.handleTestConfig('pagerduty')}
|
||||
enabled={this.getEnabled(configSections, 'pagerduty')}
|
||||
/>,
|
||||
},
|
||||
pushover: {
|
||||
|
@ -152,6 +198,8 @@ class AlertTabs extends Component {
|
|||
<PushoverConfig
|
||||
onSave={this.handleSaveConfig('pushover')}
|
||||
config={this.getSection(configSections, 'pushover')}
|
||||
onTest={this.handleTestConfig('pushover')}
|
||||
enabled={this.getEnabled(configSections, 'pushover')}
|
||||
/>,
|
||||
},
|
||||
sensu: {
|
||||
|
@ -161,6 +209,8 @@ class AlertTabs extends Component {
|
|||
<SensuConfig
|
||||
onSave={this.handleSaveConfig('sensu')}
|
||||
config={this.getSection(configSections, 'sensu')}
|
||||
onTest={this.handleTestConfig('sensu')}
|
||||
enabled={this.getEnabled(configSections, 'sensu')}
|
||||
/>,
|
||||
},
|
||||
slack: {
|
||||
|
@ -170,6 +220,8 @@ class AlertTabs extends Component {
|
|||
<SlackConfig
|
||||
onSave={this.handleSaveConfig('slack')}
|
||||
config={this.getSection(configSections, 'slack')}
|
||||
onTest={this.handleTestConfig('slack')}
|
||||
enabled={this.getEnabled(configSections, 'slack')}
|
||||
/>,
|
||||
},
|
||||
smtp: {
|
||||
|
@ -179,6 +231,8 @@ class AlertTabs extends Component {
|
|||
<SMTPConfig
|
||||
onSave={this.handleSaveConfig('smtp')}
|
||||
config={this.getSection(configSections, 'smtp')}
|
||||
onTest={this.handleTestConfig('smtp')}
|
||||
enabled={this.getEnabled(configSections, 'smtp')}
|
||||
/>,
|
||||
},
|
||||
talk: {
|
||||
|
@ -188,6 +242,8 @@ class AlertTabs extends Component {
|
|||
<TalkConfig
|
||||
onSave={this.handleSaveConfig('talk')}
|
||||
config={this.getSection(configSections, 'talk')}
|
||||
onTest={this.handleTestConfig('talk')}
|
||||
enabled={this.getEnabled(configSections, 'talk')}
|
||||
/>,
|
||||
},
|
||||
telegram: {
|
||||
|
@ -197,6 +253,8 @@ class AlertTabs extends Component {
|
|||
<TelegramConfig
|
||||
onSave={this.handleSaveConfig('telegram')}
|
||||
config={this.getSection(configSections, 'telegram')}
|
||||
onTest={this.handleTestConfig('telegram')}
|
||||
enabled={this.getEnabled(configSections, 'telegram')}
|
||||
/>,
|
||||
},
|
||||
victorops: {
|
||||
|
@ -206,10 +264,11 @@ class AlertTabs extends Component {
|
|||
<VictorOpsConfig
|
||||
onSave={this.handleSaveConfig('victorops')}
|
||||
config={this.getSection(configSections, 'victorops')}
|
||||
onTest={this.handleTestConfig('victorops')}
|
||||
enabled={this.getEnabled(configSections, 'victorops')}
|
||||
/>,
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="panel panel-minimal">
|
||||
|
@ -218,7 +277,10 @@ class AlertTabs extends Component {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs tabContentsClass="config-endpoint">
|
||||
<Tabs
|
||||
tabContentsClass="config-endpoint"
|
||||
initialIndex={this.getInitialIndex(supportedConfigs, hash)}
|
||||
>
|
||||
<TabList customClass="config-endpoint--tabs">
|
||||
{_.reduce(
|
||||
configSections,
|
||||
|
@ -269,6 +331,7 @@ AlertTabs.propTypes = {
|
|||
}).isRequired,
|
||||
}),
|
||||
addFlashMessage: func.isRequired,
|
||||
hash: string.isRequired,
|
||||
}
|
||||
|
||||
export default AlertTabs
|
||||
|
|
|
@ -65,7 +65,7 @@ class HandlerOptions extends Component {
|
|||
<EmailHandler
|
||||
selectedHandler={selectedHandler}
|
||||
handleModifyHandler={handleModifyHandler}
|
||||
onGoToConfig={onGoToConfig}
|
||||
onGoToConfig={onGoToConfig('smtp')}
|
||||
validationError={validationError}
|
||||
updateDetails={updateDetails}
|
||||
rule={rule}
|
||||
|
@ -76,7 +76,7 @@ class HandlerOptions extends Component {
|
|||
<AlertaHandler
|
||||
selectedHandler={selectedHandler}
|
||||
handleModifyHandler={handleModifyHandler}
|
||||
onGoToConfig={onGoToConfig}
|
||||
onGoToConfig={onGoToConfig('alerta')}
|
||||
validationError={validationError}
|
||||
/>
|
||||
)
|
||||
|
@ -85,7 +85,7 @@ class HandlerOptions extends Component {
|
|||
<HipchatHandler
|
||||
selectedHandler={selectedHandler}
|
||||
handleModifyHandler={handleModifyHandler}
|
||||
onGoToConfig={onGoToConfig}
|
||||
onGoToConfig={onGoToConfig('hipchat')}
|
||||
validationError={validationError}
|
||||
/>
|
||||
)
|
||||
|
@ -94,7 +94,7 @@ class HandlerOptions extends Component {
|
|||
<OpsgenieHandler
|
||||
selectedHandler={selectedHandler}
|
||||
handleModifyHandler={handleModifyHandler}
|
||||
onGoToConfig={onGoToConfig}
|
||||
onGoToConfig={onGoToConfig('opsgenie')}
|
||||
validationError={validationError}
|
||||
/>
|
||||
)
|
||||
|
@ -103,7 +103,7 @@ class HandlerOptions extends Component {
|
|||
<PagerdutyHandler
|
||||
selectedHandler={selectedHandler}
|
||||
handleModifyHandler={handleModifyHandler}
|
||||
onGoToConfig={onGoToConfig}
|
||||
onGoToConfig={onGoToConfig('pagerduty')}
|
||||
validationError={validationError}
|
||||
/>
|
||||
)
|
||||
|
@ -112,7 +112,7 @@ class HandlerOptions extends Component {
|
|||
<PushoverHandler
|
||||
selectedHandler={selectedHandler}
|
||||
handleModifyHandler={handleModifyHandler}
|
||||
onGoToConfig={onGoToConfig}
|
||||
onGoToConfig={onGoToConfig('pushover')}
|
||||
validationError={validationError}
|
||||
/>
|
||||
)
|
||||
|
@ -121,7 +121,7 @@ class HandlerOptions extends Component {
|
|||
<SensuHandler
|
||||
selectedHandler={selectedHandler}
|
||||
handleModifyHandler={handleModifyHandler}
|
||||
onGoToConfig={onGoToConfig}
|
||||
onGoToConfig={onGoToConfig('sensu')}
|
||||
validationError={validationError}
|
||||
/>
|
||||
)
|
||||
|
@ -130,7 +130,7 @@ class HandlerOptions extends Component {
|
|||
<SlackHandler
|
||||
selectedHandler={selectedHandler}
|
||||
handleModifyHandler={handleModifyHandler}
|
||||
onGoToConfig={onGoToConfig}
|
||||
onGoToConfig={onGoToConfig('slack')}
|
||||
validationError={validationError}
|
||||
/>
|
||||
)
|
||||
|
@ -139,7 +139,7 @@ class HandlerOptions extends Component {
|
|||
<TalkHandler
|
||||
selectedHandler={selectedHandler}
|
||||
handleModifyHandler={handleModifyHandler}
|
||||
onGoToConfig={onGoToConfig}
|
||||
onGoToConfig={onGoToConfig('talk')}
|
||||
validationError={validationError}
|
||||
/>
|
||||
)
|
||||
|
@ -148,7 +148,7 @@ class HandlerOptions extends Component {
|
|||
<TelegramHandler
|
||||
selectedHandler={selectedHandler}
|
||||
handleModifyHandler={handleModifyHandler}
|
||||
onGoToConfig={onGoToConfig}
|
||||
onGoToConfig={onGoToConfig('telegram')}
|
||||
validationError={validationError}
|
||||
/>
|
||||
)
|
||||
|
@ -157,7 +157,7 @@ class HandlerOptions extends Component {
|
|||
<VictoropsHandler
|
||||
selectedHandler={selectedHandler}
|
||||
handleModifyHandler={handleModifyHandler}
|
||||
onGoToConfig={onGoToConfig}
|
||||
onGoToConfig={onGoToConfig('victorops')}
|
||||
validationError={validationError}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -6,14 +6,15 @@ import FancyScrollbar from 'shared/components/FancyScrollbar'
|
|||
class KapacitorForm extends Component {
|
||||
render() {
|
||||
const {onInputChange, onReset, kapacitor, onSubmit, exists} = this.props
|
||||
const {url, name, username, password} = kapacitor
|
||||
|
||||
const {url: kapaUrl, name, username, password} = kapacitor
|
||||
return (
|
||||
<div className="page">
|
||||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">Configure Kapacitor</h1>
|
||||
<h1 className="page-header__title">{`${exists
|
||||
? 'Configure'
|
||||
: 'Add a New'} Kapacitor Connection`}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -29,13 +30,13 @@ class KapacitorForm extends Component {
|
|||
<form onSubmit={onSubmit}>
|
||||
<div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="url">Kapacitor URL</label>
|
||||
<label htmlFor="kapaUrl">Kapacitor URL</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="url"
|
||||
name="url"
|
||||
placeholder={url}
|
||||
value={url}
|
||||
id="kapaUrl"
|
||||
name="kapaUrl"
|
||||
placeholder={kapaUrl}
|
||||
value={kapaUrl}
|
||||
onChange={onInputChange}
|
||||
spellCheck="false"
|
||||
/>
|
||||
|
@ -60,7 +61,7 @@ class KapacitorForm extends Component {
|
|||
id="username"
|
||||
name="username"
|
||||
placeholder="username"
|
||||
value={username}
|
||||
value={username || ''}
|
||||
onChange={onInputChange}
|
||||
spellCheck="false"
|
||||
/>
|
||||
|
@ -73,7 +74,7 @@ class KapacitorForm extends Component {
|
|||
type="password"
|
||||
name="password"
|
||||
placeholder="password"
|
||||
value={password}
|
||||
value={password || ''}
|
||||
onChange={onInputChange}
|
||||
spellCheck="false"
|
||||
/>
|
||||
|
@ -108,7 +109,7 @@ class KapacitorForm extends Component {
|
|||
|
||||
// TODO: move these to another page. they dont belong on this page
|
||||
renderAlertOutputs() {
|
||||
const {exists, kapacitor, addFlashMessage, source} = this.props
|
||||
const {exists, kapacitor, addFlashMessage, source, hash} = this.props
|
||||
|
||||
if (exists) {
|
||||
return (
|
||||
|
@ -116,6 +117,7 @@ class KapacitorForm extends Component {
|
|||
source={source}
|
||||
kapacitor={kapacitor}
|
||||
addFlashMessage={addFlashMessage}
|
||||
hash={hash}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -153,6 +155,7 @@ KapacitorForm.propTypes = {
|
|||
source: shape({}).isRequired,
|
||||
addFlashMessage: func.isRequired,
|
||||
exists: bool.isRequired,
|
||||
hash: string.isRequired,
|
||||
}
|
||||
|
||||
export default KapacitorForm
|
||||
|
|
|
@ -25,7 +25,7 @@ class KapacitorRule extends Component {
|
|||
this.setState({timeRange})
|
||||
}
|
||||
|
||||
handleCreate = link => {
|
||||
handleCreate = pathname => {
|
||||
const {
|
||||
addFlashMessage,
|
||||
queryConfigs,
|
||||
|
@ -42,7 +42,7 @@ class KapacitorRule extends Component {
|
|||
|
||||
createRule(kapacitor, newRule)
|
||||
.then(() => {
|
||||
router.push(link || `/sources/${source.id}/alert-rules`)
|
||||
router.push(pathname || `/sources/${source.id}/alert-rules`)
|
||||
addFlashMessage({type: 'success', text: 'Rule successfully created'})
|
||||
})
|
||||
.catch(() => {
|
||||
|
@ -53,7 +53,7 @@ class KapacitorRule extends Component {
|
|||
})
|
||||
}
|
||||
|
||||
handleEdit = link => {
|
||||
handleEdit = pathname => {
|
||||
const {addFlashMessage, queryConfigs, rule, router, source} = this.props
|
||||
const updatedRule = Object.assign({}, rule, {
|
||||
query: queryConfigs[rule.queryID],
|
||||
|
@ -61,7 +61,7 @@ class KapacitorRule extends Component {
|
|||
|
||||
editRule(updatedRule)
|
||||
.then(() => {
|
||||
router.push(link || `/sources/${source.id}/alert-rules`)
|
||||
router.push(pathname || `/sources/${source.id}/alert-rules`)
|
||||
addFlashMessage({
|
||||
type: 'success',
|
||||
text: `${rule.name} successfully saved!`,
|
||||
|
@ -75,14 +75,28 @@ class KapacitorRule extends Component {
|
|||
})
|
||||
}
|
||||
|
||||
handleSaveToConfig = () => {
|
||||
const {rule, configLink, router} = this.props
|
||||
if (this.validationError()) {
|
||||
router.push(configLink)
|
||||
} else if (rule.id === DEFAULT_RULE_ID) {
|
||||
this.handleCreate(configLink)
|
||||
handleSave = () => {
|
||||
const {rule} = this.props
|
||||
if (rule.id === DEFAULT_RULE_ID) {
|
||||
this.handleCreate()
|
||||
} else {
|
||||
this.handleEdit(configLink)
|
||||
this.handleEdit()
|
||||
}
|
||||
}
|
||||
|
||||
handleSaveToConfig = configName => () => {
|
||||
const {rule, configLink, router} = this.props
|
||||
const pathname = `${configLink}#${configName}`
|
||||
if (this.validationError()) {
|
||||
router.push({
|
||||
pathname,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (rule.id === DEFAULT_RULE_ID) {
|
||||
this.handleCreate(pathname)
|
||||
} else {
|
||||
this.handleEdit(pathname)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -157,13 +171,12 @@ class KapacitorRule extends Component {
|
|||
} = this.props
|
||||
const {chooseTrigger, updateRuleValues} = ruleActions
|
||||
const {timeRange} = this.state
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<RuleHeader
|
||||
source={source}
|
||||
onSave={
|
||||
rule.id === DEFAULT_RULE_ID ? this.handleCreate : this.handleEdit
|
||||
}
|
||||
onSave={this.handleSave}
|
||||
validationError={this.validationError()}
|
||||
/>
|
||||
<FancyScrollbar className="page-contents fancy-scroll--kapacitor">
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const LogsToggle = ({areLogsVisible, onToggleLogsVisbility}) =>
|
||||
const LogsToggle = ({areLogsVisible, onToggleLogsVisibility}) =>
|
||||
<ul className="nav nav-tablist nav-tablist-sm nav-tablist-malachite logs-toggle">
|
||||
<li
|
||||
className={areLogsVisible ? null : 'active'}
|
||||
onClick={onToggleLogsVisbility}
|
||||
onClick={onToggleLogsVisibility}
|
||||
>
|
||||
Editor
|
||||
</li>
|
||||
<li
|
||||
className={areLogsVisible ? 'active' : null}
|
||||
onClick={onToggleLogsVisbility}
|
||||
onClick={onToggleLogsVisibility}
|
||||
>
|
||||
Editor + Logs
|
||||
</li>
|
||||
|
@ -20,7 +20,7 @@ const {bool, func} = PropTypes
|
|||
|
||||
LogsToggle.propTypes = {
|
||||
areLogsVisible: bool,
|
||||
onToggleLogsVisbility: func.isRequired,
|
||||
onToggleLogsVisibility: func.isRequired,
|
||||
}
|
||||
|
||||
export default LogsToggle
|
||||
|
|
|
@ -8,25 +8,29 @@ import LogsTable from 'src/kapacitor/components/LogsTable'
|
|||
|
||||
const Tickscript = ({
|
||||
onSave,
|
||||
onExit,
|
||||
task,
|
||||
logs,
|
||||
validation,
|
||||
consoleMessage,
|
||||
onSelectDbrps,
|
||||
onChangeScript,
|
||||
onChangeType,
|
||||
onChangeID,
|
||||
unsavedChanges,
|
||||
isNewTickscript,
|
||||
areLogsVisible,
|
||||
areLogsEnabled,
|
||||
onToggleLogsVisbility,
|
||||
onToggleLogsVisibility,
|
||||
}) =>
|
||||
<div className="page">
|
||||
<TickscriptHeader
|
||||
task={task}
|
||||
onSave={onSave}
|
||||
onExit={onExit}
|
||||
unsavedChanges={unsavedChanges}
|
||||
areLogsVisible={areLogsVisible}
|
||||
areLogsEnabled={areLogsEnabled}
|
||||
onToggleLogsVisbility={onToggleLogsVisbility}
|
||||
onToggleLogsVisibility={onToggleLogsVisibility}
|
||||
isNewTickscript={isNewTickscript}
|
||||
/>
|
||||
<div className="page-contents--split">
|
||||
|
@ -38,11 +42,14 @@ const Tickscript = ({
|
|||
onChangeID={onChangeID}
|
||||
task={task}
|
||||
/>
|
||||
<TickscriptEditorConsole validation={validation} />
|
||||
<TickscriptEditor
|
||||
script={task.tickscript}
|
||||
onChangeScript={onChangeScript}
|
||||
/>
|
||||
<TickscriptEditorConsole
|
||||
consoleMessage={consoleMessage}
|
||||
unsavedChanges={unsavedChanges}
|
||||
/>
|
||||
</div>
|
||||
{areLogsVisible ? <LogsTable logs={logs} /> : null}
|
||||
</div>
|
||||
|
@ -53,12 +60,13 @@ const {arrayOf, bool, func, shape, string} = PropTypes
|
|||
Tickscript.propTypes = {
|
||||
logs: arrayOf(shape()).isRequired,
|
||||
onSave: func.isRequired,
|
||||
onExit: func.isRequired,
|
||||
source: shape({
|
||||
id: string,
|
||||
}),
|
||||
areLogsVisible: bool,
|
||||
areLogsEnabled: bool,
|
||||
onToggleLogsVisbility: func.isRequired,
|
||||
onToggleLogsVisibility: func.isRequired,
|
||||
task: shape({
|
||||
id: string,
|
||||
script: string,
|
||||
|
@ -66,10 +74,11 @@ Tickscript.propTypes = {
|
|||
}).isRequired,
|
||||
onChangeScript: func.isRequired,
|
||||
onSelectDbrps: func.isRequired,
|
||||
validation: string,
|
||||
consoleMessage: string,
|
||||
onChangeType: func.isRequired,
|
||||
onChangeID: func.isRequired,
|
||||
isNewTickscript: bool.isRequired,
|
||||
unsavedChanges: bool,
|
||||
}
|
||||
|
||||
export default Tickscript
|
||||
|
|
|
@ -1,22 +1,31 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const TickscriptEditorConsole = ({validation}) =>
|
||||
<div className="tickscript-console">
|
||||
<div className="tickscript-console--output">
|
||||
{validation
|
||||
? <p>
|
||||
{validation}
|
||||
</p>
|
||||
: <p className="tickscript-console--default">
|
||||
Save your TICKscript to validate it
|
||||
</p>}
|
||||
</div>
|
||||
</div>
|
||||
const TickscriptEditorConsole = ({consoleMessage, unsavedChanges}) => {
|
||||
let consoleOutput = 'TICKscript is valid'
|
||||
let consoleClass = 'tickscript-console--valid'
|
||||
|
||||
const {string} = PropTypes
|
||||
if (consoleMessage) {
|
||||
consoleOutput = consoleMessage
|
||||
consoleClass = 'tickscript-console--error'
|
||||
} else if (unsavedChanges) {
|
||||
consoleOutput = 'You have unsaved changes, save to validate TICKscript'
|
||||
consoleClass = 'tickscript-console--default'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tickscript-console">
|
||||
<p className={consoleClass}>
|
||||
{consoleOutput}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const {bool, string} = PropTypes
|
||||
|
||||
TickscriptEditorConsole.propTypes = {
|
||||
validation: string,
|
||||
consoleMessage: string,
|
||||
unsavedChanges: bool,
|
||||
}
|
||||
|
||||
export default TickscriptEditorConsole
|
||||
|
|
|
@ -2,14 +2,17 @@ import React, {PropTypes} from 'react'
|
|||
|
||||
import SourceIndicator from 'shared/components/SourceIndicator'
|
||||
import LogsToggle from 'src/kapacitor/components/LogsToggle'
|
||||
import ConfirmButton from 'src/shared/components/ConfirmButton'
|
||||
|
||||
const TickscriptHeader = ({
|
||||
task: {id},
|
||||
onSave,
|
||||
onExit,
|
||||
unsavedChanges,
|
||||
areLogsVisible,
|
||||
areLogsEnabled,
|
||||
isNewTickscript,
|
||||
onToggleLogsVisbility,
|
||||
onToggleLogsVisibility,
|
||||
}) =>
|
||||
<div className="page-header full-width">
|
||||
<div className="page-header__container">
|
||||
|
@ -20,18 +23,40 @@ const TickscriptHeader = ({
|
|||
<LogsToggle
|
||||
areLogsVisible={areLogsVisible}
|
||||
areLogsEnabled={areLogsEnabled}
|
||||
onToggleLogsVisbility={onToggleLogsVisbility}
|
||||
onToggleLogsVisibility={onToggleLogsVisibility}
|
||||
/>}
|
||||
<div className="page-header__right">
|
||||
<SourceIndicator />
|
||||
<button
|
||||
className="btn btn-success btn-sm"
|
||||
title={id ? '' : 'ID your TICKscript to save'}
|
||||
onClick={onSave}
|
||||
disabled={!id}
|
||||
>
|
||||
{isNewTickscript ? 'Save New TICKscript' : 'Save TICKscript'}
|
||||
</button>
|
||||
{isNewTickscript
|
||||
? <button
|
||||
className="btn btn-success btn-sm"
|
||||
title="Name your TICKscript to save"
|
||||
onClick={onSave}
|
||||
disabled={!id}
|
||||
>
|
||||
Save New TICKscript
|
||||
</button>
|
||||
: <button
|
||||
className="btn btn-success btn-sm"
|
||||
title="You have unsaved changes"
|
||||
onClick={onSave}
|
||||
disabled={!unsavedChanges}
|
||||
>
|
||||
Save Changes
|
||||
</button>}
|
||||
{unsavedChanges
|
||||
? <ConfirmButton
|
||||
text="Exit"
|
||||
confirmText="Discard unsaved changes?"
|
||||
confirmAction={onExit}
|
||||
/>
|
||||
: <button
|
||||
className="btn btn-default btn-sm"
|
||||
title="Return to Alert Rules"
|
||||
onClick={onExit}
|
||||
>
|
||||
Exit
|
||||
</button>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -41,9 +66,10 @@ const {arrayOf, bool, func, shape, string} = PropTypes
|
|||
TickscriptHeader.propTypes = {
|
||||
isNewTickscript: bool,
|
||||
onSave: func,
|
||||
onExit: func.isRequired,
|
||||
areLogsVisible: bool,
|
||||
areLogsEnabled: bool,
|
||||
onToggleLogsVisbility: func.isRequired,
|
||||
onToggleLogsVisibility: func.isRequired,
|
||||
task: shape({
|
||||
dbrps: arrayOf(
|
||||
shape({
|
||||
|
@ -52,6 +78,7 @@ TickscriptHeader.propTypes = {
|
|||
})
|
||||
),
|
||||
}),
|
||||
unsavedChanges: bool,
|
||||
}
|
||||
|
||||
export default TickscriptHeader
|
||||
|
|
|
@ -5,9 +5,12 @@ import RedactedInput from './RedactedInput'
|
|||
class AlertaConfig extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
testEnabled: this.props.enabled,
|
||||
}
|
||||
}
|
||||
|
||||
handleSaveAlert = e => {
|
||||
handleSubmit = async e => {
|
||||
e.preventDefault()
|
||||
|
||||
const properties = {
|
||||
|
@ -17,7 +20,14 @@ class AlertaConfig extends Component {
|
|||
url: this.url.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
const success = await this.props.onSave(properties)
|
||||
if (success) {
|
||||
this.setState({testEnabled: true})
|
||||
}
|
||||
}
|
||||
|
||||
disableTest = () => {
|
||||
this.setState({testEnabled: false})
|
||||
}
|
||||
|
||||
handleTokenRef = r => (this.token = r)
|
||||
|
@ -26,7 +36,7 @@ class AlertaConfig extends Component {
|
|||
const {environment, origin, token, url} = this.props.config.options
|
||||
|
||||
return (
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="environment">Environment</label>
|
||||
<input
|
||||
|
@ -35,6 +45,7 @@ class AlertaConfig extends Component {
|
|||
type="text"
|
||||
ref={r => (this.environment = r)}
|
||||
defaultValue={environment || ''}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -46,6 +57,7 @@ class AlertaConfig extends Component {
|
|||
type="text"
|
||||
ref={r => (this.origin = r)}
|
||||
defaultValue={origin || ''}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -55,6 +67,7 @@ class AlertaConfig extends Component {
|
|||
defaultValue={token}
|
||||
id="token"
|
||||
refFunc={this.handleTokenRef}
|
||||
disableTest={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -66,12 +79,26 @@ class AlertaConfig extends Component {
|
|||
type="text"
|
||||
ref={r => (this.url = r)}
|
||||
defaultValue={url || ''}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group-submit col-xs-12 text-center">
|
||||
<button className="btn btn-primary" type="submit">
|
||||
Update Alerta Config
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
type="submit"
|
||||
disabled={this.state.testEnabled}
|
||||
>
|
||||
<span className="icon checkmark" />
|
||||
Save Changes
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={!this.state.testEnabled}
|
||||
onClick={this.props.onTest}
|
||||
>
|
||||
<span className="icon pulse-c" />
|
||||
Send Test Alert
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -91,6 +118,8 @@ AlertaConfig.propTypes = {
|
|||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: func.isRequired,
|
||||
onTest: func.isRequired,
|
||||
enabled: bool.isRequired,
|
||||
}
|
||||
|
||||
export default AlertaConfig
|
||||
|
|
|
@ -7,9 +7,12 @@ import RedactedInput from './RedactedInput'
|
|||
class HipchatConfig extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
testEnabled: this.props.enabled,
|
||||
}
|
||||
}
|
||||
|
||||
handleSaveAlert = e => {
|
||||
handleSubmit = async e => {
|
||||
e.preventDefault()
|
||||
|
||||
const properties = {
|
||||
|
@ -18,7 +21,14 @@ class HipchatConfig extends Component {
|
|||
token: this.token.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
const success = await this.props.onSave(properties)
|
||||
if (success) {
|
||||
this.setState({testEnabled: true})
|
||||
}
|
||||
}
|
||||
|
||||
disableTest = () => {
|
||||
this.setState({testEnabled: false})
|
||||
}
|
||||
|
||||
handleTokenRef = r => (this.token = r)
|
||||
|
@ -32,7 +42,7 @@ class HipchatConfig extends Component {
|
|||
.replace('.hipchat.com/v2/room', '')
|
||||
|
||||
return (
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="url">Subdomain</label>
|
||||
<input
|
||||
|
@ -42,6 +52,7 @@ class HipchatConfig extends Component {
|
|||
placeholder="your-subdomain"
|
||||
ref={r => (this.url = r)}
|
||||
defaultValue={subdomain && subdomain.length ? subdomain : ''}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -54,6 +65,7 @@ class HipchatConfig extends Component {
|
|||
placeholder="your-hipchat-room"
|
||||
ref={r => (this.room = r)}
|
||||
defaultValue={room || ''}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -66,12 +78,26 @@ class HipchatConfig extends Component {
|
|||
defaultValue={token}
|
||||
id="token"
|
||||
refFunc={this.handleTokenRef}
|
||||
disableTest={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group-submit col-xs-12 text-center">
|
||||
<button className="btn btn-primary" type="submit">
|
||||
Update HipChat Config
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
type="submit"
|
||||
disabled={this.state.testEnabled}
|
||||
>
|
||||
<span className="icon checkmark" />
|
||||
Save Changes
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={!this.state.testEnabled}
|
||||
onClick={this.props.onTest}
|
||||
>
|
||||
<span className="icon pulse-c" />
|
||||
Send Test Alert
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -90,6 +116,8 @@ HipchatConfig.propTypes = {
|
|||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: func.isRequired,
|
||||
onTest: func.isRequired,
|
||||
enabled: bool.isRequired,
|
||||
}
|
||||
|
||||
export default HipchatConfig
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, {PropTypes, Component} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import RedactedInput from './RedactedInput'
|
||||
import TagInput from 'shared/components/TagInput'
|
||||
|
||||
class OpsGenieConfig extends Component {
|
||||
constructor(props) {
|
||||
|
@ -12,10 +12,11 @@ class OpsGenieConfig extends Component {
|
|||
this.state = {
|
||||
currentTeams: teams || [],
|
||||
currentRecipients: recipients || [],
|
||||
testEnabled: this.props.enabled,
|
||||
}
|
||||
}
|
||||
|
||||
handleSaveAlert = e => {
|
||||
handleSubmit = async e => {
|
||||
e.preventDefault()
|
||||
|
||||
const properties = {
|
||||
|
@ -24,7 +25,14 @@ class OpsGenieConfig extends Component {
|
|||
recipients: this.state.currentRecipients,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
const success = await this.props.onSave(properties)
|
||||
if (success) {
|
||||
this.setState({testEnabled: true})
|
||||
}
|
||||
}
|
||||
|
||||
disableTest = () => {
|
||||
this.setState({testEnabled: false})
|
||||
}
|
||||
|
||||
handleAddTeam = team => {
|
||||
|
@ -37,13 +45,13 @@ class OpsGenieConfig extends Component {
|
|||
})
|
||||
}
|
||||
|
||||
handleDeleteTeam = team => () => {
|
||||
handleDeleteTeam = team => {
|
||||
this.setState({
|
||||
currentTeams: this.state.currentTeams.filter(t => t !== team),
|
||||
})
|
||||
}
|
||||
|
||||
handleDeleteRecipient = recipient => () => {
|
||||
handleDeleteRecipient = recipient => {
|
||||
this.setState({
|
||||
currentRecipients: this.state.currentRecipients.filter(
|
||||
r => r !== recipient
|
||||
|
@ -59,13 +67,14 @@ class OpsGenieConfig extends Component {
|
|||
const {currentTeams, currentRecipients} = this.state
|
||||
|
||||
return (
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="api-key">API Key</label>
|
||||
<RedactedInput
|
||||
defaultValue={apiKey}
|
||||
id="api-key"
|
||||
refFunc={this.handleApiKeyRef}
|
||||
disableTest={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -74,17 +83,32 @@ class OpsGenieConfig extends Component {
|
|||
onAddTag={this.handleAddTeam}
|
||||
onDeleteTag={this.handleDeleteTeam}
|
||||
tags={currentTeams}
|
||||
disableTest={this.disableTest}
|
||||
/>
|
||||
<TagInput
|
||||
title="Recipients"
|
||||
onAddTag={this.handleAddRecipient}
|
||||
onDeleteTag={this.handleDeleteRecipient}
|
||||
tags={currentRecipients}
|
||||
disableTest={this.disableTest}
|
||||
/>
|
||||
|
||||
<div className="form-group-submit col-xs-12 text-center">
|
||||
<button className="btn btn-primary" type="submit">
|
||||
Update OpsGenie Config
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
type="submit"
|
||||
disabled={this.state.testEnabled}
|
||||
>
|
||||
<span className="icon checkmark" />
|
||||
Save Changes
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={!this.state.testEnabled}
|
||||
onClick={this.props.onTest}
|
||||
>
|
||||
<span className="icon pulse-c" />
|
||||
Send Test Alert
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -92,7 +116,7 @@ class OpsGenieConfig extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
const {array, arrayOf, bool, func, shape, string} = PropTypes
|
||||
const {array, bool, func, shape} = PropTypes
|
||||
|
||||
OpsGenieConfig.propTypes = {
|
||||
config: shape({
|
||||
|
@ -103,84 +127,8 @@ OpsGenieConfig.propTypes = {
|
|||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: func.isRequired,
|
||||
}
|
||||
|
||||
class TagInput extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
handleAddTag = e => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
const newItem = e.target.value.trim()
|
||||
const {tags, onAddTag} = this.props
|
||||
if (!this.shouldAddToList(newItem, tags)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.input.value = ''
|
||||
onAddTag(newItem)
|
||||
}
|
||||
}
|
||||
|
||||
shouldAddToList(item, tags) {
|
||||
return !_.isEmpty(item) && !tags.find(l => l === item)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {title, tags, onDeleteTag} = this.props
|
||||
|
||||
return (
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor={title}>
|
||||
{title}
|
||||
</label>
|
||||
<input
|
||||
placeholder={`Type and hit 'Enter' to add to list of ${title}`}
|
||||
autoComplete="off"
|
||||
className="form-control"
|
||||
id={title}
|
||||
type="text"
|
||||
ref={r => (this.input = r)}
|
||||
onKeyDown={this.handleAddTag}
|
||||
/>
|
||||
<Tags tags={tags} onDeleteTag={onDeleteTag} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TagInput.propTypes = {
|
||||
onAddTag: func.isRequired,
|
||||
onDeleteTag: func.isRequired,
|
||||
tags: arrayOf(string).isRequired,
|
||||
title: string.isRequired,
|
||||
}
|
||||
|
||||
const Tags = ({tags, onDeleteTag}) =>
|
||||
<div className="input-tag-list">
|
||||
{tags.map(item => {
|
||||
return <Tag key={item} item={item} onDelete={onDeleteTag} />
|
||||
})}
|
||||
</div>
|
||||
|
||||
Tags.propTypes = {
|
||||
tags: arrayOf(string),
|
||||
onDeleteTag: func,
|
||||
}
|
||||
|
||||
const Tag = ({item, onDelete}) =>
|
||||
<span key={item} className="input-tag-item">
|
||||
<span>
|
||||
{item}
|
||||
</span>
|
||||
<span className="icon remove" onClick={onDelete(item)} />
|
||||
</span>
|
||||
|
||||
Tag.propTypes = {
|
||||
item: string,
|
||||
onDelete: func,
|
||||
onTest: func.isRequired,
|
||||
enabled: bool.isRequired,
|
||||
}
|
||||
|
||||
export default OpsGenieConfig
|
||||
|
|
|
@ -4,9 +4,12 @@ import RedactedInput from './RedactedInput'
|
|||
class PagerDutyConfig extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
testEnabled: this.props.enabled,
|
||||
}
|
||||
}
|
||||
|
||||
handleSaveAlert = e => {
|
||||
handleSubmit = async e => {
|
||||
e.preventDefault()
|
||||
|
||||
const properties = {
|
||||
|
@ -14,7 +17,14 @@ class PagerDutyConfig extends Component {
|
|||
url: this.url.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
const success = await this.props.onSave(properties)
|
||||
if (success) {
|
||||
this.setState({testEnabled: true})
|
||||
}
|
||||
}
|
||||
|
||||
disableTest = () => {
|
||||
this.setState({testEnabled: false})
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -23,13 +33,14 @@ class PagerDutyConfig extends Component {
|
|||
const serviceKey = options['service-key']
|
||||
const refFunc = r => (this.serviceKey = r)
|
||||
return (
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="service-key">Service Key</label>
|
||||
<RedactedInput
|
||||
defaultValue={serviceKey || ''}
|
||||
id="service-key"
|
||||
refFunc={refFunc}
|
||||
disableTest={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -41,12 +52,26 @@ class PagerDutyConfig extends Component {
|
|||
type="text"
|
||||
ref={r => (this.url = r)}
|
||||
defaultValue={url || ''}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group-submit col-xs-12 text-center">
|
||||
<button className="btn btn-primary" type="submit">
|
||||
Update PagerDuty Config
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
type="submit"
|
||||
disabled={this.state.testEnabled}
|
||||
>
|
||||
<span className="icon checkmark" />
|
||||
Save Changes
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={!this.state.testEnabled}
|
||||
onClick={this.props.onTest}
|
||||
>
|
||||
<span className="icon pulse-c" />
|
||||
Send Test Alert
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -64,6 +89,8 @@ PagerDutyConfig.propTypes = {
|
|||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: func.isRequired,
|
||||
onTest: func.isRequired,
|
||||
enabled: bool.isRequired,
|
||||
}
|
||||
|
||||
export default PagerDutyConfig
|
||||
|
|
|
@ -8,9 +8,12 @@ import {PUSHOVER_DOCS_LINK} from 'src/kapacitor/copy'
|
|||
class PushoverConfig extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
testEnabled: this.props.enabled,
|
||||
}
|
||||
}
|
||||
|
||||
handleSaveAlert = e => {
|
||||
handleSubmit = async e => {
|
||||
e.preventDefault()
|
||||
|
||||
const properties = {
|
||||
|
@ -19,7 +22,14 @@ class PushoverConfig extends Component {
|
|||
'user-key': this.userKey.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
const success = await this.props.onSave(properties)
|
||||
if (success) {
|
||||
this.setState({testEnabled: true})
|
||||
}
|
||||
}
|
||||
|
||||
disableTest = () => {
|
||||
this.setState({testEnabled: false})
|
||||
}
|
||||
|
||||
handleUserKeyRef = r => (this.userKey = r)
|
||||
|
@ -32,7 +42,7 @@ class PushoverConfig extends Component {
|
|||
const userKey = options['user-key']
|
||||
|
||||
return (
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="user-key">
|
||||
User Key
|
||||
|
@ -45,6 +55,7 @@ class PushoverConfig extends Component {
|
|||
defaultValue={userKey}
|
||||
id="user-key"
|
||||
refFunc={this.handleUserKeyRef}
|
||||
disableTest={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -60,6 +71,7 @@ class PushoverConfig extends Component {
|
|||
defaultValue={token}
|
||||
id="token"
|
||||
refFunc={this.handleTokenRef}
|
||||
disableTest={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -71,12 +83,26 @@ class PushoverConfig extends Component {
|
|||
type="text"
|
||||
ref={r => (this.url = r)}
|
||||
defaultValue={url || ''}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group-submit col-xs-12 text-center">
|
||||
<button className="btn btn-primary" type="submit">
|
||||
Update Pushover Config
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
type="submit"
|
||||
disabled={this.state.testEnabled}
|
||||
>
|
||||
<span className="icon checkmark" />
|
||||
Save Changes
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={!this.state.testEnabled}
|
||||
onClick={this.props.onTest}
|
||||
>
|
||||
<span className="icon pulse-c" />
|
||||
Send Test Alert
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -95,6 +121,8 @@ PushoverConfig.propTypes = {
|
|||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: func.isRequired,
|
||||
onTest: func.isRequired,
|
||||
enabled: bool.isRequired,
|
||||
}
|
||||
|
||||
export default PushoverConfig
|
||||
|
|
|
@ -13,7 +13,7 @@ class RedactedInput extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {defaultValue, id, refFunc} = this.props
|
||||
const {defaultValue, id, refFunc, disableTest} = this.props
|
||||
const {editing} = this.state
|
||||
|
||||
if (defaultValue === true && !editing) {
|
||||
|
@ -43,6 +43,7 @@ class RedactedInput extends Component {
|
|||
type="text"
|
||||
ref={refFunc}
|
||||
defaultValue={''}
|
||||
onChange={disableTest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -54,6 +55,7 @@ RedactedInput.propTypes = {
|
|||
id: string.isRequired,
|
||||
defaultValue: bool,
|
||||
refFunc: func.isRequired,
|
||||
disableTest: func,
|
||||
}
|
||||
|
||||
export default RedactedInput
|
||||
|
|
|
@ -3,27 +3,37 @@ import React, {PropTypes, Component} from 'react'
|
|||
class SMTPConfig extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
testEnabled: this.props.enabled,
|
||||
}
|
||||
}
|
||||
|
||||
handleSaveAlert = e => {
|
||||
handleSubmit = async e => {
|
||||
e.preventDefault()
|
||||
|
||||
const properties = {
|
||||
host: this.host.value,
|
||||
port: this.port.value,
|
||||
from: this.from.value,
|
||||
to: this.to.value ? [this.to.value] : [],
|
||||
username: this.username.value,
|
||||
password: this.password.value,
|
||||
}
|
||||
const success = await this.props.onSave(properties)
|
||||
if (success) {
|
||||
this.setState({testEnabled: true})
|
||||
}
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
disableTest = () => {
|
||||
this.setState({testEnabled: false})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {host, port, from, username, password} = this.props.config.options
|
||||
const {host, port, from, username, password, to} = this.props.config.options
|
||||
|
||||
return (
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className="form-group col-xs-12 col-md-6">
|
||||
<label htmlFor="smtp-host">SMTP Host</label>
|
||||
<input
|
||||
|
@ -32,6 +42,7 @@ class SMTPConfig extends Component {
|
|||
type="text"
|
||||
ref={r => (this.host = r)}
|
||||
defaultValue={host || ''}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -43,10 +54,11 @@ class SMTPConfig extends Component {
|
|||
type="text"
|
||||
ref={r => (this.port = r)}
|
||||
defaultValue={port || ''}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12">
|
||||
<div className="form-group col-xs-6">
|
||||
<label htmlFor="smtp-from">From Email</label>
|
||||
<input
|
||||
className="form-control"
|
||||
|
@ -55,6 +67,20 @@ class SMTPConfig extends Component {
|
|||
type="text"
|
||||
ref={r => (this.from = r)}
|
||||
defaultValue={from || ''}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group col-xs-12 col-md-6">
|
||||
<label htmlFor="smtp-to">To Email</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="smtp-to"
|
||||
placeholder="email@domain.com"
|
||||
type="text"
|
||||
ref={r => (this.to = r)}
|
||||
defaultValue={to || ''}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -66,6 +92,7 @@ class SMTPConfig extends Component {
|
|||
type="text"
|
||||
ref={r => (this.username = r)}
|
||||
defaultValue={username || ''}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -77,12 +104,26 @@ class SMTPConfig extends Component {
|
|||
type="password"
|
||||
ref={r => (this.password = r)}
|
||||
defaultValue={`${password}`}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group-submit col-xs-12 text-center">
|
||||
<button className="btn btn-primary" type="submit">
|
||||
Update SMTP Config
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
type="submit"
|
||||
disabled={this.state.testEnabled}
|
||||
>
|
||||
<span className="icon checkmark" />
|
||||
Save Changes
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={!this.state.testEnabled}
|
||||
onClick={this.props.onTest}
|
||||
>
|
||||
<span className="icon pulse-c" />
|
||||
Send Test Alert
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -103,6 +144,8 @@ SMTPConfig.propTypes = {
|
|||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: func.isRequired,
|
||||
onTest: func.isRequired,
|
||||
enabled: bool.isRequired,
|
||||
}
|
||||
|
||||
export default SMTPConfig
|
||||
|
|
|
@ -3,9 +3,12 @@ import React, {PropTypes, Component} from 'react'
|
|||
class SensuConfig extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
testEnabled: this.props.enabled,
|
||||
}
|
||||
}
|
||||
|
||||
handleSaveAlert = e => {
|
||||
handleSubmit = async e => {
|
||||
e.preventDefault()
|
||||
|
||||
const properties = {
|
||||
|
@ -13,14 +16,21 @@ class SensuConfig extends Component {
|
|||
addr: this.addr.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
const success = await this.props.onSave(properties)
|
||||
if (success) {
|
||||
this.setState({testEnabled: true})
|
||||
}
|
||||
}
|
||||
|
||||
disableTest = () => {
|
||||
this.setState({testEnabled: false})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {source, addr} = this.props.config.options
|
||||
|
||||
return (
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className="form-group col-xs-12 col-md-6">
|
||||
<label htmlFor="source">Source</label>
|
||||
<input
|
||||
|
@ -29,6 +39,7 @@ class SensuConfig extends Component {
|
|||
type="text"
|
||||
ref={r => (this.source = r)}
|
||||
defaultValue={source || ''}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -40,12 +51,26 @@ class SensuConfig extends Component {
|
|||
type="text"
|
||||
ref={r => (this.addr = r)}
|
||||
defaultValue={addr || ''}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group-submit col-xs-12 text-center">
|
||||
<button className="btn btn-primary" type="submit">
|
||||
Update Sensu Config
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
type="submit"
|
||||
disabled={this.state.testEnabled}
|
||||
>
|
||||
<span className="icon checkmark" />
|
||||
Save Changes
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={!this.state.testEnabled}
|
||||
onClick={this.props.onTest}
|
||||
>
|
||||
<span className="icon pulse-c" />
|
||||
Send Test Alert
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -53,7 +78,7 @@ class SensuConfig extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
const {func, shape, string} = PropTypes
|
||||
const {bool, func, shape, string} = PropTypes
|
||||
|
||||
SensuConfig.propTypes = {
|
||||
config: shape({
|
||||
|
@ -63,6 +88,8 @@ SensuConfig.propTypes = {
|
|||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: func.isRequired,
|
||||
onTest: func.isRequired,
|
||||
enabled: bool.isRequired,
|
||||
}
|
||||
|
||||
export default SensuConfig
|
||||
|
|
|
@ -6,25 +6,23 @@ class SlackConfig extends Component {
|
|||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
testEnabled: !!this.props.config.options.url,
|
||||
testEnabled: this.props.enabled,
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.setState({
|
||||
testEnabled: !!nextProps.config.options.url,
|
||||
})
|
||||
}
|
||||
|
||||
handleSaveAlert = e => {
|
||||
handleSubmit = async e => {
|
||||
e.preventDefault()
|
||||
|
||||
const properties = {
|
||||
url: this.url.value,
|
||||
channel: this.channel.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
const success = await this.props.onSave(properties)
|
||||
if (success) {
|
||||
this.setState({testEnabled: true})
|
||||
}
|
||||
}
|
||||
disableTest = () => {
|
||||
this.setState({testEnabled: false})
|
||||
}
|
||||
|
||||
handleUrlRef = r => (this.url = r)
|
||||
|
@ -33,7 +31,7 @@ class SlackConfig extends Component {
|
|||
const {url, channel} = this.props.config.options
|
||||
|
||||
return (
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="slack-url">
|
||||
Slack Webhook URL (
|
||||
|
@ -46,6 +44,7 @@ class SlackConfig extends Component {
|
|||
defaultValue={url}
|
||||
id="url"
|
||||
refFunc={this.handleUrlRef}
|
||||
disableTest={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -58,14 +57,30 @@ class SlackConfig extends Component {
|
|||
placeholder="#alerts"
|
||||
ref={r => (this.channel = r)}
|
||||
defaultValue={channel || ''}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group-submit col-xs-12 text-center">
|
||||
<button className="btn btn-primary" type="submit">
|
||||
Update Slack Config
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
type="submit"
|
||||
disabled={this.state.testEnabled}
|
||||
>
|
||||
<span className="icon checkmark" />
|
||||
Save Changes
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={!this.state.testEnabled}
|
||||
onClick={this.props.onTest}
|
||||
>
|
||||
<span className="icon pulse-c" />
|
||||
Send Test Alert
|
||||
</button>
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
@ -81,6 +96,8 @@ SlackConfig.propTypes = {
|
|||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: func.isRequired,
|
||||
onTest: func.isRequired,
|
||||
enabled: bool.isRequired,
|
||||
}
|
||||
|
||||
export default SlackConfig
|
||||
|
|
|
@ -5,9 +5,12 @@ import RedactedInput from './RedactedInput'
|
|||
class TalkConfig extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
testEnabled: this.props.enabled,
|
||||
}
|
||||
}
|
||||
|
||||
handleSaveAlert = e => {
|
||||
handleSubmit = async e => {
|
||||
e.preventDefault()
|
||||
|
||||
const properties = {
|
||||
|
@ -15,7 +18,14 @@ class TalkConfig extends Component {
|
|||
author_name: this.author.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
const success = await this.props.onSave(properties)
|
||||
if (success) {
|
||||
this.setState({testEnabled: true})
|
||||
}
|
||||
}
|
||||
|
||||
disableTest = () => {
|
||||
this.setState({testEnabled: false})
|
||||
}
|
||||
|
||||
handleUrlRef = r => (this.url = r)
|
||||
|
@ -24,13 +34,14 @@ class TalkConfig extends Component {
|
|||
const {url, author_name: author} = this.props.config.options
|
||||
|
||||
return (
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="url">URL</label>
|
||||
<RedactedInput
|
||||
defaultValue={url}
|
||||
id="url"
|
||||
refFunc={this.handleUrlRef}
|
||||
disableTest={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -42,12 +53,26 @@ class TalkConfig extends Component {
|
|||
type="text"
|
||||
ref={r => (this.author = r)}
|
||||
defaultValue={author || ''}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group-submit col-xs-12 text-center">
|
||||
<button className="btn btn-primary" type="submit">
|
||||
Update Talk Config
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
type="submit"
|
||||
disabled={this.state.testEnabled}
|
||||
>
|
||||
<span className="icon checkmark" />
|
||||
Save Changes
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={!this.state.testEnabled}
|
||||
onClick={this.props.onTest}
|
||||
>
|
||||
<span className="icon pulse-c" />
|
||||
Send Test Alert
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -65,6 +90,8 @@ TalkConfig.propTypes = {
|
|||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: func.isRequired,
|
||||
onTest: func.isRequired,
|
||||
enabled: bool.isRequired,
|
||||
}
|
||||
|
||||
export default TalkConfig
|
||||
|
|
|
@ -7,9 +7,12 @@ import RedactedInput from './RedactedInput'
|
|||
class TelegramConfig extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
testEnabled: this.props.enabled,
|
||||
}
|
||||
}
|
||||
|
||||
handleSaveAlert = e => {
|
||||
handleSubmit = async e => {
|
||||
e.preventDefault()
|
||||
|
||||
let parseMode
|
||||
|
@ -28,7 +31,14 @@ class TelegramConfig extends Component {
|
|||
token: this.token.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
const success = await this.props.onSave(properties)
|
||||
if (success) {
|
||||
this.setState({testEnabled: true})
|
||||
}
|
||||
}
|
||||
|
||||
disableTest = () => {
|
||||
this.setState({testEnabled: false})
|
||||
}
|
||||
|
||||
handleTokenRef = r => (this.token = r)
|
||||
|
@ -42,7 +52,7 @@ class TelegramConfig extends Component {
|
|||
const parseMode = options['parse-mode']
|
||||
|
||||
return (
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className="form-group col-xs-12">
|
||||
<div className="alert alert-warning alert-icon no-user-select">
|
||||
<span className="icon triangle" />
|
||||
|
@ -68,6 +78,7 @@ class TelegramConfig extends Component {
|
|||
defaultValue={token}
|
||||
id="token"
|
||||
refFunc={this.handleTokenRef}
|
||||
disableTest={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -86,6 +97,7 @@ class TelegramConfig extends Component {
|
|||
placeholder="your-telegram-chat-id"
|
||||
ref={r => (this.chatID = r)}
|
||||
defaultValue={chatID || ''}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -100,6 +112,7 @@ class TelegramConfig extends Component {
|
|||
value="markdown"
|
||||
defaultChecked={parseMode !== 'HTML'}
|
||||
ref={r => (this.parseModeMarkdown = r)}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
<label htmlFor="parseModeMarkdown">Markdown</label>
|
||||
</div>
|
||||
|
@ -111,6 +124,7 @@ class TelegramConfig extends Component {
|
|||
value="html"
|
||||
defaultChecked={parseMode === 'HTML'}
|
||||
ref={r => (this.parseModeHTML = r)}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
<label htmlFor="parseModeHTML">HTML</label>
|
||||
</div>
|
||||
|
@ -124,6 +138,7 @@ class TelegramConfig extends Component {
|
|||
type="checkbox"
|
||||
defaultChecked={disableWebPagePreview}
|
||||
ref={r => (this.disableWebPagePreview = r)}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
<label htmlFor="disableWebPagePreview">
|
||||
Disable{' '}
|
||||
|
@ -142,6 +157,7 @@ class TelegramConfig extends Component {
|
|||
type="checkbox"
|
||||
defaultChecked={disableNotification}
|
||||
ref={r => (this.disableNotification = r)}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
<label htmlFor="disableNotification">
|
||||
Disable notifications on iOS devices and disable sounds on Android
|
||||
|
@ -151,8 +167,21 @@ class TelegramConfig extends Component {
|
|||
</div>
|
||||
|
||||
<div className="form-group-submit col-xs-12 text-center">
|
||||
<button className="btn btn-primary" type="submit">
|
||||
Update Telegram Config
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
type="submit"
|
||||
disabled={this.state.testEnabled}
|
||||
>
|
||||
<span className="icon checkmark" />
|
||||
Save Changes
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={!this.state.testEnabled}
|
||||
onClick={this.props.onTest}
|
||||
>
|
||||
<span className="icon pulse-c" />
|
||||
Send Test Alert
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -173,6 +202,8 @@ TelegramConfig.propTypes = {
|
|||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: func.isRequired,
|
||||
onTest: func.isRequired,
|
||||
enabled: bool.isRequired,
|
||||
}
|
||||
|
||||
export default TelegramConfig
|
||||
|
|
|
@ -5,9 +5,12 @@ import RedactedInput from './RedactedInput'
|
|||
class VictorOpsConfig extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
testEnabled: this.props.enabled,
|
||||
}
|
||||
}
|
||||
|
||||
handleSaveAlert = e => {
|
||||
handleSubmit = async e => {
|
||||
e.preventDefault()
|
||||
|
||||
const properties = {
|
||||
|
@ -16,7 +19,14 @@ class VictorOpsConfig extends Component {
|
|||
url: this.url.value,
|
||||
}
|
||||
|
||||
this.props.onSave(properties)
|
||||
const success = await this.props.onSave(properties)
|
||||
if (success) {
|
||||
this.setState({testEnabled: true})
|
||||
}
|
||||
}
|
||||
|
||||
disableTest = () => {
|
||||
this.setState({testEnabled: false})
|
||||
}
|
||||
|
||||
handleApiRef = r => (this.apiKey = r)
|
||||
|
@ -28,13 +38,14 @@ class VictorOpsConfig extends Component {
|
|||
const {url} = options
|
||||
|
||||
return (
|
||||
<form onSubmit={this.handleSaveAlert}>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor="api-key">API Key</label>
|
||||
<RedactedInput
|
||||
defaultValue={apiKey}
|
||||
id="api-key"
|
||||
refFunc={this.handleApiRef}
|
||||
disableTest={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -46,6 +57,7 @@ class VictorOpsConfig extends Component {
|
|||
type="text"
|
||||
ref={r => (this.routingKey = r)}
|
||||
defaultValue={routingKey || ''}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -57,12 +69,26 @@ class VictorOpsConfig extends Component {
|
|||
type="text"
|
||||
ref={r => (this.url = r)}
|
||||
defaultValue={url || ''}
|
||||
onChange={this.disableTest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group-submit col-xs-12 text-center">
|
||||
<button className="btn btn-primary" type="submit">
|
||||
Update VictorOps Config
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
type="submit"
|
||||
disabled={this.state.testEnabled}
|
||||
>
|
||||
<span className="icon checkmark" />
|
||||
Save Changes
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={!this.state.testEnabled}
|
||||
onClick={this.props.onTest}
|
||||
>
|
||||
<span className="icon pulse-c" />
|
||||
Send Test Alert
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -81,6 +107,8 @@ VictorOpsConfig.propTypes = {
|
|||
}).isRequired,
|
||||
}).isRequired,
|
||||
onSave: func.isRequired,
|
||||
onTest: func.isRequired,
|
||||
enabled: bool.isRequired,
|
||||
}
|
||||
|
||||
export default VictorOpsConfig
|
||||
|
|
|
@ -55,7 +55,7 @@ class KapacitorPage extends Component {
|
|||
const {value, name} = e.target
|
||||
|
||||
this.setState(prevState => {
|
||||
const update = {[name]: value.trim()}
|
||||
const update = {[name]: value}
|
||||
return {kapacitor: {...prevState.kapacitor, ...update}}
|
||||
})
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ class KapacitorPage extends Component {
|
|||
router,
|
||||
} = this.props
|
||||
const {kapacitor} = this.state
|
||||
|
||||
kapacitor.name = kapacitor.name.trim()
|
||||
const isNameTaken = kapacitors.some(k => k.name === kapacitor.name)
|
||||
const isNew = !params.id
|
||||
|
||||
|
@ -136,9 +136,9 @@ class KapacitorPage extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {source, addFlashMessage} = this.props
|
||||
const {source, addFlashMessage, location, params} = this.props
|
||||
const hash = (location && location.hash) || (params && params.hash) || ''
|
||||
const {kapacitor, exists} = this.state
|
||||
|
||||
return (
|
||||
<KapacitorForm
|
||||
onSubmit={this.handleSubmit}
|
||||
|
@ -148,6 +148,7 @@ class KapacitorPage extends Component {
|
|||
source={source}
|
||||
addFlashMessage={addFlashMessage}
|
||||
exists={exists}
|
||||
hash={hash}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -168,6 +169,7 @@ KapacitorPage.propTypes = {
|
|||
url: string.isRequired,
|
||||
kapacitors: array,
|
||||
}),
|
||||
location: shape({pathname: string, hash: string}).isRequired,
|
||||
}
|
||||
|
||||
export default withRouter(KapacitorPage)
|
||||
|
|
|
@ -24,11 +24,12 @@ class TickscriptPage extends Component {
|
|||
dbrps: [],
|
||||
type: 'stream',
|
||||
},
|
||||
validation: '',
|
||||
consoleMessage: '',
|
||||
isEditingID: true,
|
||||
logs: [],
|
||||
areLogsEnabled: false,
|
||||
failStr: '',
|
||||
unsavedChanges: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -172,9 +173,10 @@ class TickscriptPage extends Component {
|
|||
} else {
|
||||
response = await createTask(kapacitor, task, router, sourceID)
|
||||
}
|
||||
|
||||
if (response && response.code === 500) {
|
||||
return this.setState({validation: response.message})
|
||||
if (response.code) {
|
||||
this.setState({unsavedChanges: true, consoleMessage: response.message})
|
||||
} else {
|
||||
this.setState({unsavedChanges: false, consoleMessage: ''})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
@ -182,37 +184,57 @@ class TickscriptPage extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleExit = () => {
|
||||
const {source: {id: sourceID}, router} = this.props
|
||||
|
||||
return router.push(`/sources/${sourceID}/alert-rules`)
|
||||
}
|
||||
|
||||
handleChangeScript = tickscript => {
|
||||
this.setState({task: {...this.state.task, tickscript}})
|
||||
this.setState({
|
||||
task: {...this.state.task, tickscript},
|
||||
unsavedChanges: true,
|
||||
})
|
||||
}
|
||||
|
||||
handleSelectDbrps = dbrps => {
|
||||
this.setState({task: {...this.state.task, dbrps}})
|
||||
this.setState({task: {...this.state.task, dbrps}, unsavedChanges: true})
|
||||
}
|
||||
|
||||
handleChangeType = type => () => {
|
||||
this.setState({task: {...this.state.task, type}})
|
||||
this.setState({task: {...this.state.task, type}, unsavedChanges: true})
|
||||
}
|
||||
|
||||
handleChangeID = e => {
|
||||
this.setState({task: {...this.state.task, id: e.target.value}})
|
||||
this.setState({
|
||||
task: {...this.state.task, id: e.target.value},
|
||||
unsavedChanges: true,
|
||||
})
|
||||
}
|
||||
|
||||
handleToggleLogsVisbility = () => {
|
||||
handleToggleLogsVisibility = () => {
|
||||
this.setState({areLogsVisible: !this.state.areLogsVisible})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {source} = this.props
|
||||
const {task, validation, logs, areLogsVisible, areLogsEnabled} = this.state
|
||||
|
||||
const {
|
||||
task,
|
||||
logs,
|
||||
areLogsVisible,
|
||||
areLogsEnabled,
|
||||
unsavedChanges,
|
||||
consoleMessage,
|
||||
} = this.state
|
||||
return (
|
||||
<Tickscript
|
||||
task={task}
|
||||
logs={logs}
|
||||
source={source}
|
||||
validation={validation}
|
||||
consoleMessage={consoleMessage}
|
||||
onSave={this.handleSave}
|
||||
unsavedChanges={unsavedChanges}
|
||||
onExit={this.handleExit}
|
||||
isNewTickscript={!this._isEditing()}
|
||||
onSelectDbrps={this.handleSelectDbrps}
|
||||
onChangeScript={this.handleChangeScript}
|
||||
|
@ -220,7 +242,7 @@ class TickscriptPage extends Component {
|
|||
onChangeID={this.handleChangeID}
|
||||
areLogsVisible={areLogsVisible}
|
||||
areLogsEnabled={areLogsEnabled}
|
||||
onToggleLogsVisbility={this.handleToggleLogsVisbility}
|
||||
onToggleLogsVisibility={this.handleToggleLogsVisibility}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import {getMe as getMeAJAX, updateMe as updateMeAJAX} from 'shared/apis/auth'
|
||||
|
||||
import {linksReceived} from 'shared/actions/links'
|
||||
import {getLinksAsync} from 'shared/actions/links'
|
||||
|
||||
import {publishAutoDismissingNotification} from 'shared/dispatchers'
|
||||
import {errorThrown} from 'shared/actions/errors'
|
||||
|
||||
import {LONG_NOTIFICATION_DISMISS_DELAY} from 'shared/constants'
|
||||
import {NOTIFICATION_DISMISS_DELAY} from 'shared/constants'
|
||||
|
||||
export const authExpired = auth => ({
|
||||
type: 'AUTH_EXPIRED',
|
||||
|
@ -58,18 +58,8 @@ export const getMeAsync = ({shouldResetMe = false} = {}) => async dispatch => {
|
|||
}
|
||||
try {
|
||||
// These non-me objects are added to every response by some AJAX trickery
|
||||
const {
|
||||
data: me,
|
||||
auth,
|
||||
users,
|
||||
meLink,
|
||||
config,
|
||||
external,
|
||||
logoutLink,
|
||||
organizations,
|
||||
environment,
|
||||
} = await getMeAJAX()
|
||||
|
||||
const {data: me, auth, logoutLink} = await getMeAJAX()
|
||||
// TODO: eventually, get the links for auth and logout out of here and into linksGetCompleted
|
||||
dispatch(
|
||||
meGetCompleted({
|
||||
me,
|
||||
|
@ -77,23 +67,20 @@ export const getMeAsync = ({shouldResetMe = false} = {}) => async dispatch => {
|
|||
logoutLink,
|
||||
})
|
||||
)
|
||||
|
||||
dispatch(
|
||||
linksReceived({
|
||||
external,
|
||||
users,
|
||||
organizations,
|
||||
me: meLink,
|
||||
config,
|
||||
environment,
|
||||
})
|
||||
) // TODO: put this before meGetCompleted... though for some reason it doesn't fire the first time then
|
||||
} catch (error) {
|
||||
dispatch(meGetFailed())
|
||||
dispatch(errorThrown(error))
|
||||
}
|
||||
}
|
||||
|
||||
// meChangeOrganizationAsync is for switching the user's current organization.
|
||||
//
|
||||
// Global links state also needs to be refreshed upon organization change so
|
||||
// that Admin Chronograf / Current Org User tab's link is valid, but this is
|
||||
// happening automatically because we are using a browser redirect to reload
|
||||
// the application. If at some point we stop using a redirect and instead
|
||||
// make it a seamless SPA experience, a la issue #2463, we'll need to make sure
|
||||
// links are still refreshed.
|
||||
export const meChangeOrganizationAsync = (
|
||||
url,
|
||||
organization
|
||||
|
@ -109,11 +96,15 @@ export const meChangeOrganizationAsync = (
|
|||
'success',
|
||||
`Now logged in to '${me.currentOrganization
|
||||
.name}' as '${currentRole.name}'`,
|
||||
LONG_NOTIFICATION_DISMISS_DELAY
|
||||
NOTIFICATION_DISMISS_DELAY
|
||||
)
|
||||
)
|
||||
dispatch(meChangeOrganizationCompleted())
|
||||
dispatch(meGetCompleted({me, auth, logoutLink}))
|
||||
|
||||
// refresh links after every successful meChangeOrganization to refresh
|
||||
// /organizations/:id/users link for Admin / Current Org Users page to load
|
||||
dispatch(getLinksAsync())
|
||||
// TODO: reload sources upon me change org if non-refresh behavior preferred
|
||||
// instead of current behavior on both invocations of meChangeOrganization,
|
||||
// which is to refresh index via router.push('')
|
||||
|
|
|
@ -1,6 +1,30 @@
|
|||
import * as actionTypes from 'shared/constants/actionTypes'
|
||||
import {getLinks as getLinksAJAX} from 'shared/apis/links'
|
||||
|
||||
export const linksReceived = links => ({
|
||||
type: actionTypes.LINKS_RECEIVED,
|
||||
import {errorThrown} from 'shared/actions/errors'
|
||||
|
||||
import {linksLink} from 'shared/constants'
|
||||
|
||||
const linksGetRequested = () => ({
|
||||
type: 'LINKS_GET_REQUESTED',
|
||||
})
|
||||
|
||||
export const linksGetCompleted = links => ({
|
||||
type: 'LINKS_GET_COMPLETED',
|
||||
payload: {links},
|
||||
})
|
||||
|
||||
const linksGetFailed = () => ({
|
||||
type: 'LINKS_GET_FAILED',
|
||||
})
|
||||
|
||||
export const getLinksAsync = () => async dispatch => {
|
||||
dispatch(linksGetRequested())
|
||||
try {
|
||||
const {data} = await getLinksAJAX()
|
||||
dispatch(linksGetCompleted(data))
|
||||
} catch (error) {
|
||||
const message = `Failed to retrieve api links from ${linksLink}`
|
||||
dispatch(errorThrown(error, message))
|
||||
dispatch(linksGetFailed())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ export const getEnv = async url => {
|
|||
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('Error retreieving envs: ', error)
|
||||
console.error('Error retrieving envs: ', error)
|
||||
return DEFAULT_ENVS
|
||||
}
|
||||
}
|
||||
|
|
|
@ -152,20 +152,18 @@ export function updateKapacitorConfigSection(kapacitor, section, properties) {
|
|||
})
|
||||
}
|
||||
|
||||
export function testAlertOutput(kapacitor, outputName, properties) {
|
||||
return kapacitorProxy(
|
||||
kapacitor,
|
||||
'GET',
|
||||
'/kapacitor/v1/service-tests'
|
||||
).then(({data: {services}}) => {
|
||||
const service = services.find(s => s.name === outputName)
|
||||
return kapacitorProxy(
|
||||
export const testAlertOutput = async (kapacitor, outputName) => {
|
||||
try {
|
||||
const {data: {services}} = await kapacitorProxy(
|
||||
kapacitor,
|
||||
'POST',
|
||||
service.link.href,
|
||||
Object.assign({}, service.options, properties)
|
||||
'GET',
|
||||
'/kapacitor/v1/service-tests'
|
||||
)
|
||||
})
|
||||
const service = services.find(s => s.name === outputName)
|
||||
return kapacitorProxy(kapacitor, 'POST', service.link.href, {})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
export function createKapacitorTask(kapacitor, id, type, dbrps, script) {
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import {getAJAX, setAJAXLinks} from 'utils/ajax'
|
||||
|
||||
import {linksLink} from 'shared/constants'
|
||||
|
||||
export const getLinks = async () => {
|
||||
try {
|
||||
const response = await getAJAX(linksLink)
|
||||
// TODO: Remove use of links entirely from within AJAX function so that
|
||||
// call to setAJAXLinks is not necessary. See issue #1486.
|
||||
setAJAXLinks({updatedLinks: response.data})
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
|
||||
import OnClickOutside from 'shared/components/OnClickOutside'
|
||||
|
||||
class ConfirmButton extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
expanded: false,
|
||||
}
|
||||
}
|
||||
|
||||
handleButtonClick = () => {
|
||||
if (this.props.disabled) {
|
||||
return
|
||||
}
|
||||
this.setState({expanded: !this.state.expanded})
|
||||
}
|
||||
|
||||
handleConfirmClick = () => {
|
||||
this.setState({expanded: false})
|
||||
this.props.confirmAction()
|
||||
}
|
||||
|
||||
handleClickOutside = () => {
|
||||
this.setState({expanded: false})
|
||||
}
|
||||
|
||||
calculatePosition = () => {
|
||||
if (!this.buttonDiv || !this.tooltipDiv) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const windowWidth = window.innerWidth
|
||||
const buttonRect = this.buttonDiv.getBoundingClientRect()
|
||||
const tooltipRect = this.tooltipDiv.getBoundingClientRect()
|
||||
|
||||
const rightGap = windowWidth - buttonRect.right
|
||||
|
||||
if (tooltipRect.width / 2 > rightGap) {
|
||||
return 'left'
|
||||
}
|
||||
|
||||
return 'bottom'
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
text,
|
||||
confirmText,
|
||||
type,
|
||||
size,
|
||||
square,
|
||||
icon,
|
||||
disabled,
|
||||
customClass,
|
||||
} = this.props
|
||||
const {expanded} = this.state
|
||||
|
||||
const customClassString = customClass ? ` ${customClass}` : ''
|
||||
const squareString = square ? ' btn-square' : ''
|
||||
const expandedString = expanded ? ' active' : ''
|
||||
const disabledString = disabled ? ' disabled' : ''
|
||||
|
||||
const classname = `confirm-button btn ${type} ${size}${customClassString}${squareString}${expandedString}${disabledString}`
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classname}
|
||||
onClick={this.handleButtonClick}
|
||||
ref={r => (this.buttonDiv = r)}
|
||||
>
|
||||
{icon && <span className={`icon ${icon}`} />}
|
||||
{text && text}
|
||||
<div className={`confirm-button--tooltip ${this.calculatePosition()}`}>
|
||||
<div
|
||||
className="confirm-button--confirmation"
|
||||
onClick={this.handleConfirmClick}
|
||||
ref={r => (this.tooltipDiv = r)}
|
||||
>
|
||||
{confirmText}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {bool, func, string} = PropTypes
|
||||
|
||||
ConfirmButton.defaultProps = {
|
||||
confirmText: 'Confirm',
|
||||
type: 'btn-default',
|
||||
size: 'btn-sm',
|
||||
square: false,
|
||||
}
|
||||
ConfirmButton.propTypes = {
|
||||
text: string,
|
||||
confirmText: string,
|
||||
confirmAction: func.isRequired,
|
||||
type: string,
|
||||
size: string,
|
||||
square: bool,
|
||||
icon: string,
|
||||
disabled: bool,
|
||||
customClass: string,
|
||||
}
|
||||
|
||||
export default OnClickOutside(ConfirmButton)
|
|
@ -31,7 +31,7 @@ class SlideToggle extends Component {
|
|||
|
||||
const className = `slide-toggle slide-toggle__${size} ${active
|
||||
? 'active'
|
||||
: null} ${disabled ? 'disabled' : null}`
|
||||
: ''} ${disabled ? 'disabled' : ''}`
|
||||
|
||||
return (
|
||||
<div className={className} onClick={this.handleClick}>
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import Tags from 'shared/components/Tags'
|
||||
|
||||
class TagInput extends Component {
|
||||
handleAddTag = e => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
const newItem = e.target.value.trim()
|
||||
const {tags, onAddTag} = this.props
|
||||
if (!this.shouldAddToList(newItem, tags)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.input.value = ''
|
||||
onAddTag(newItem)
|
||||
this.props.disableTest()
|
||||
}
|
||||
}
|
||||
|
||||
handleDeleteTag = item => {
|
||||
this.props.onDeleteTag(item)
|
||||
}
|
||||
|
||||
shouldAddToList(item, tags) {
|
||||
return !_.isEmpty(item) && !tags.find(l => l === item)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {title, tags} = this.props
|
||||
|
||||
return (
|
||||
<div className="form-group col-xs-12">
|
||||
<label htmlFor={title}>
|
||||
{title}
|
||||
</label>
|
||||
<input
|
||||
placeholder={`Type and hit 'Enter' to add to list of ${title}`}
|
||||
autoComplete="off"
|
||||
className="form-control tag-input"
|
||||
id={title}
|
||||
type="text"
|
||||
ref={r => (this.input = r)}
|
||||
onKeyDown={this.handleAddTag}
|
||||
/>
|
||||
<Tags tags={tags} onDeleteTag={this.handleDeleteTag} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, func, string} = PropTypes
|
||||
|
||||
TagInput.propTypes = {
|
||||
onAddTag: func.isRequired,
|
||||
onDeleteTag: func.isRequired,
|
||||
tags: arrayOf(string).isRequired,
|
||||
title: string.isRequired,
|
||||
disableTest: func,
|
||||
}
|
||||
|
||||
export default TagInput
|
|
@ -0,0 +1,61 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
import TagsAddButton from 'src/shared/components/TagsAddButton'
|
||||
import ConfirmButton from 'src/shared/components/ConfirmButton'
|
||||
|
||||
const Tags = ({tags, onDeleteTag, addMenuItems, addMenuChoose}) =>
|
||||
<div className="input-tag-list">
|
||||
{tags.map(item => {
|
||||
return (
|
||||
<Tag
|
||||
key={item.text || item.name || item}
|
||||
item={item}
|
||||
onDelete={onDeleteTag}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{addMenuItems.length && addMenuChoose
|
||||
? <TagsAddButton items={addMenuItems} onChoose={addMenuChoose} />
|
||||
: null}
|
||||
</div>
|
||||
|
||||
class Tag extends Component {
|
||||
handleClickDelete = item => () => {
|
||||
this.props.onDelete(item)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {item} = this.props
|
||||
return (
|
||||
<span key={item} className="input-tag-item">
|
||||
<span>
|
||||
{item.text || item.name || item}
|
||||
</span>
|
||||
{
|
||||
<ConfirmButton
|
||||
icon="remove"
|
||||
size="btn-xs"
|
||||
customClass="btn-xxs"
|
||||
confirmText="Remove user from organization?"
|
||||
confirmAction={this.handleClickDelete(item)}
|
||||
/>
|
||||
}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, func, oneOfType, shape, string} = PropTypes
|
||||
|
||||
Tags.propTypes = {
|
||||
tags: arrayOf(oneOfType([shape(), string])),
|
||||
onDeleteTag: func,
|
||||
addMenuItems: arrayOf(shape({})),
|
||||
addMenuChoose: func,
|
||||
}
|
||||
|
||||
Tag.propTypes = {
|
||||
item: oneOfType([shape(), string]),
|
||||
onDelete: func,
|
||||
}
|
||||
|
||||
export default Tags
|
|
@ -0,0 +1,57 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
|
||||
import OnClickOutside from 'shared/components/OnClickOutside'
|
||||
import uuid from 'node-uuid'
|
||||
|
||||
class TagsAddButton extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {open: false}
|
||||
}
|
||||
|
||||
handleButtonClick = () => {
|
||||
this.setState({open: !this.state.open})
|
||||
}
|
||||
|
||||
handleMenuClick = item => () => {
|
||||
this.setState({open: false})
|
||||
this.props.onChoose(item)
|
||||
}
|
||||
|
||||
handleClickOutside = () => {
|
||||
this.setState({open: false})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {open} = this.state
|
||||
const {items} = this.props
|
||||
|
||||
const classname = `tags-add${open ? ' open' : ''}`
|
||||
return (
|
||||
<div className={classname} onClick={this.handleButtonClick}>
|
||||
<span className="icon plus" />
|
||||
<div className="tags-add--menu">
|
||||
{items.map(item =>
|
||||
<div
|
||||
key={uuid.v4()}
|
||||
className="tags-add--menu-item"
|
||||
onClick={this.handleMenuClick(item)}
|
||||
>
|
||||
{item.text}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {array, func} = PropTypes
|
||||
|
||||
TagsAddButton.propTypes = {
|
||||
items: array.isRequired,
|
||||
onChoose: func.isRequired,
|
||||
}
|
||||
|
||||
export default OnClickOutside(TagsAddButton)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue