pull/2526/head
Benjamin Schweizer 2018-03-28 13:30:05 +02:00
commit 0a98d1c7b7
593 changed files with 30806 additions and 28569 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 1.4.1.3
current_version = 1.4.2.3
files = README.md server/swagger.json
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\.(?P<release>\d+)
serialize = {major}.{minor}.{patch}.{release}

1
.eslintrc Symbolic link
View File

@ -0,0 +1 @@
ui/.eslintrc

1
.gitignore vendored
View File

@ -22,3 +22,4 @@ chronograf*.db
*_gen.go
canned/apps_gen.go
npm-debug.log
yarn-error.log

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,7 @@ We really like to receive feature requests, as it helps us prioritize our work.
Contributing to the source code
-------------------------------
Chronograf is built using Go for its API backend and serving the front-end assets. The front-end visualization is built with React and uses Yarn for package management. The assumption is that all your Go development are done in `$GOPATH/src`. `GOPATH` can be any directory under which Chronograf and all its dependencies will be cloned. For full details on the project structure, follow along below.
Chronograf is built using Go for its API backend and serving the front-end assets, and uses Dep for dependency management. The front-end visualization is built with React (JavaScript) and uses Yarn for dependency management. The assumption is that all your Go development are done in `$GOPATH/src`. `GOPATH` can be any directory under which Chronograf and all its dependencies will be cloned. For full details on the project structure, follow along below.
Submitting a pull request
-------------------------
@ -43,9 +43,13 @@ Signing the CLA
If you are going to be contributing back to Chronograf please take a second to sign our CLA, which can be found
[on our website](https://influxdata.com/community/cla/).
Installing Yarn
Installing & Using Yarn
--------------
You'll need to install Yarn to manage the JavaScript modules that the front-end uses. This varies depending on what platform you're developing on, but you should be able to find an installer on [the Yarn installation page](https://yarnpkg.com/en/docs/install).
You'll need to install Yarn to manage the frontend (JavaScript) dependencies.
* [Install Yarn](https://yarnpkg.com/en/docs/install)
To add a dependency via Yarn, for example, run `yarn add <dependency>` from within the `/chronograf/ui` directory.
Installing Go
-------------
@ -62,13 +66,13 @@ running the following:
gvm use go1.7.5 --default
```
Installing GDM
Installing & Using Dep
--------------
Chronograf uses [gdm](https://github.com/sparrc/gdm) to manage dependencies. Install it by running the following:
You'll need to install Dep to manage the backend (Go) dependencies.
```bash
go get github.com/sparrc/gdm
```
* [Install Dep](https://github.com/golang/dep)
To add a dependency via Dep, for example, run `dep ensure -add <dependency>` from within the `/chronograf` directory. _Note that as of this writing, `dep ensure` will modify many extraneous vendor files, so you'll need to run `dep prune` to clean this up before committing your changes. Apparently, the next version of `dep` will take care of this step for you._
Revision Control Systems
------------------------

89
Gopkg.lock generated
View File

@ -39,34 +39,7 @@
[[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]]
@ -77,13 +50,7 @@
[[projects]]
name = "github.com/google/go-cmp"
packages = [
"cmp",
"cmp/cmpopts",
"cmp/internal/diff",
"cmp/internal/function",
"cmp/internal/value"
]
packages = ["cmp","cmp/cmpopts","cmp/internal/diff","cmp/internal/function","cmp/internal/value"]
revision = "8099a9787ce5dc5984ed879a3bda47dc730a8e97"
version = "v0.1.0"
@ -100,28 +67,13 @@
[[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]]
@ -163,21 +115,13 @@
[[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"
]
revision = "1e695b1c8febf17aad3bfa7bf0a819ef94b98ad5"
packages = [".","github","heroku","internal"]
revision = "2f32c3ac0fa4fb807a0fcefb0b6f2468a0d99bd0"
[[projects]]
branch = "master"
@ -187,31 +131,18 @@
[[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 = "11df631364d11bc05c8f71af1aa735360b5a40a793d32d47d1f1d8c694a55f6f"
inputs-digest = "a4df1b0953349e64a89581f4b83ac3a2f40e17681e19f8de3cbf828b6375a3ba"
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -62,7 +62,7 @@ required = ["github.com/kevinburke/go-bindata","github.com/gogo/protobuf/proto",
[[constraint]]
name = "golang.org/x/oauth2"
revision = "1e695b1c8febf17aad3bfa7bf0a819ef94b98ad5"
revision = "2f32c3ac0fa4fb807a0fcefb0b6f2468a0d99bd0"
[[constraint]]
name = "google.golang.org/api"

View File

@ -101,7 +101,10 @@ gotestrace:
go test -race ./...
jstest:
cd ui && yarn test
cd ui && yarn test --runInBand
jslint:
cd ui && yarn run lint:fix
run: ${BINARY}
./chronograf

View File

@ -136,7 +136,7 @@ option.
## Versions
The most recent version of Chronograf is
[v1.4.1.3](https://www.influxdata.com/downloads/).
[v1.4.2.3](https://www.influxdata.com/downloads/).
Spotted a bug or have a feature request? Please open
[an issue](https://github.com/influxdata/chronograf/issues/new)!
@ -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.1.3
docker pull chronograf:1.4.2.3
```
### From Source

View File

@ -75,14 +75,15 @@ func UnmarshalSource(data []byte, s *chronograf.Source) error {
// MarshalServer encodes a server to binary protobuf format.
func MarshalServer(s chronograf.Server) ([]byte, error) {
return proto.Marshal(&Server{
ID: int64(s.ID),
SrcID: int64(s.SrcID),
Name: s.Name,
Username: s.Username,
Password: s.Password,
URL: s.URL,
Active: s.Active,
Organization: s.Organization,
ID: int64(s.ID),
SrcID: int64(s.SrcID),
Name: s.Name,
Username: s.Username,
Password: s.Password,
URL: s.URL,
Active: s.Active,
Organization: s.Organization,
InsecureSkipVerify: s.InsecureSkipVerify,
})
}
@ -101,6 +102,7 @@ func UnmarshalServer(data []byte, s *chronograf.Server) error {
s.URL = pb.URL
s.Active = pb.Active
s.Organization = pb.Organization
s.InsecureSkipVerify = pb.InsecureSkipVerify
return nil
}
@ -263,6 +265,30 @@ func MarshalDashboard(d chronograf.Dashboard) ([]byte, error) {
}
}
sortBy := &RenamableField{
InternalName: c.TableOptions.SortBy.InternalName,
DisplayName: c.TableOptions.SortBy.DisplayName,
Visible: c.TableOptions.SortBy.Visible,
}
fieldNames := make([]*RenamableField, len(c.TableOptions.FieldNames))
for i, field := range c.TableOptions.FieldNames {
fieldNames[i] = &RenamableField{
InternalName: field.InternalName,
DisplayName: field.DisplayName,
Visible: field.Visible,
}
}
tableOptions := &TableOptions{
TimeFormat: c.TableOptions.TimeFormat,
VerticalTimeAxis: c.TableOptions.VerticalTimeAxis,
SortBy: sortBy,
Wrapping: c.TableOptions.Wrapping,
FieldNames: fieldNames,
FixFirstColumn: c.TableOptions.FixFirstColumn,
}
cells[i] = &DashboardCell{
ID: c.ID,
X: c.X,
@ -278,6 +304,7 @@ func MarshalDashboard(d chronograf.Dashboard) ([]byte, error) {
Type: c.Legend.Type,
Orientation: c.Legend.Orientation,
},
TableOptions: tableOptions,
}
}
templates := make([]*Template, len(d.Templates))
@ -404,18 +431,51 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error {
legend.Orientation = c.Legend.Orientation
}
tableOptions := chronograf.TableOptions{}
if c.TableOptions != nil {
sortBy := chronograf.RenamableField{}
if c.TableOptions.SortBy != nil {
sortBy.InternalName = c.TableOptions.SortBy.InternalName
sortBy.DisplayName = c.TableOptions.SortBy.DisplayName
sortBy.Visible = c.TableOptions.SortBy.Visible
}
tableOptions.SortBy = sortBy
fieldNames := make([]chronograf.RenamableField, len(c.TableOptions.FieldNames))
for i, field := range c.TableOptions.FieldNames {
fieldNames[i] = chronograf.RenamableField{}
fieldNames[i].InternalName = field.InternalName
fieldNames[i].DisplayName = field.DisplayName
fieldNames[i].Visible = field.Visible
}
tableOptions.FieldNames = fieldNames
tableOptions.TimeFormat = c.TableOptions.TimeFormat
tableOptions.VerticalTimeAxis = c.TableOptions.VerticalTimeAxis
tableOptions.Wrapping = c.TableOptions.Wrapping
tableOptions.FixFirstColumn = c.TableOptions.FixFirstColumn
}
// FIXME: this is merely for legacy cells and
// should be removed as soon as possible
cellType := c.Type
if cellType == "" {
cellType = "line"
}
cells[i] = chronograf.DashboardCell{
ID: c.ID,
X: c.X,
Y: c.Y,
W: c.W,
H: c.H,
Name: c.Name,
Queries: queries,
Type: c.Type,
Axes: axes,
CellColors: colors,
Legend: legend,
ID: c.ID,
X: c.X,
Y: c.Y,
W: c.W,
H: c.H,
Name: c.Name,
Queries: queries,
Type: cellType,
Axes: axes,
CellColors: colors,
Legend: legend,
TableOptions: tableOptions,
}
}

View File

@ -11,6 +11,8 @@ It has these top-level messages:
Source
Dashboard
DashboardCell
TableOptions
RenamableField
Color
Legend
Axis
@ -210,17 +212,18 @@ func (m *Dashboard) GetOrganization() string {
}
type DashboardCell struct {
X int32 `protobuf:"varint,1,opt,name=x,proto3" json:"x,omitempty"`
Y int32 `protobuf:"varint,2,opt,name=y,proto3" json:"y,omitempty"`
W int32 `protobuf:"varint,3,opt,name=w,proto3" json:"w,omitempty"`
H int32 `protobuf:"varint,4,opt,name=h,proto3" json:"h,omitempty"`
Queries []*Query `protobuf:"bytes,5,rep,name=queries" json:"queries,omitempty"`
Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"`
Type string `protobuf:"bytes,7,opt,name=type,proto3" json:"type,omitempty"`
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"`
X int32 `protobuf:"varint,1,opt,name=x,proto3" json:"x,omitempty"`
Y int32 `protobuf:"varint,2,opt,name=y,proto3" json:"y,omitempty"`
W int32 `protobuf:"varint,3,opt,name=w,proto3" json:"w,omitempty"`
H int32 `protobuf:"varint,4,opt,name=h,proto3" json:"h,omitempty"`
Queries []*Query `protobuf:"bytes,5,rep,name=queries" json:"queries,omitempty"`
Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"`
Type string `protobuf:"bytes,7,opt,name=type,proto3" json:"type,omitempty"`
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"`
TableOptions *TableOptions `protobuf:"bytes,12,opt,name=tableOptions" json:"tableOptions,omitempty"`
}
func (m *DashboardCell) Reset() { *m = DashboardCell{} }
@ -305,6 +308,101 @@ func (m *DashboardCell) GetLegend() *Legend {
return nil
}
func (m *DashboardCell) GetTableOptions() *TableOptions {
if m != nil {
return m.TableOptions
}
return nil
}
type TableOptions struct {
TimeFormat string `protobuf:"bytes,1,opt,name=timeFormat,proto3" json:"timeFormat,omitempty"`
VerticalTimeAxis bool `protobuf:"varint,2,opt,name=verticalTimeAxis,proto3" json:"verticalTimeAxis,omitempty"`
SortBy *RenamableField `protobuf:"bytes,3,opt,name=sortBy" json:"sortBy,omitempty"`
Wrapping string `protobuf:"bytes,4,opt,name=wrapping,proto3" json:"wrapping,omitempty"`
FieldNames []*RenamableField `protobuf:"bytes,5,rep,name=fieldNames" json:"fieldNames,omitempty"`
FixFirstColumn bool `protobuf:"varint,6,opt,name=fixFirstColumn,proto3" json:"fixFirstColumn,omitempty"`
}
func (m *TableOptions) Reset() { *m = TableOptions{} }
func (m *TableOptions) String() string { return proto.CompactTextString(m) }
func (*TableOptions) ProtoMessage() {}
func (*TableOptions) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{3} }
func (m *TableOptions) GetTimeFormat() string {
if m != nil {
return m.TimeFormat
}
return ""
}
func (m *TableOptions) GetVerticalTimeAxis() bool {
if m != nil {
return m.VerticalTimeAxis
}
return false
}
func (m *TableOptions) GetSortBy() *RenamableField {
if m != nil {
return m.SortBy
}
return nil
}
func (m *TableOptions) GetWrapping() string {
if m != nil {
return m.Wrapping
}
return ""
}
func (m *TableOptions) GetFieldNames() []*RenamableField {
if m != nil {
return m.FieldNames
}
return nil
}
func (m *TableOptions) GetFixFirstColumn() bool {
if m != nil {
return m.FixFirstColumn
}
return false
}
type RenamableField struct {
InternalName string `protobuf:"bytes,1,opt,name=internalName,proto3" json:"internalName,omitempty"`
DisplayName string `protobuf:"bytes,2,opt,name=displayName,proto3" json:"displayName,omitempty"`
Visible bool `protobuf:"varint,3,opt,name=visible,proto3" json:"visible,omitempty"`
}
func (m *RenamableField) Reset() { *m = RenamableField{} }
func (m *RenamableField) String() string { return proto.CompactTextString(m) }
func (*RenamableField) ProtoMessage() {}
func (*RenamableField) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{4} }
func (m *RenamableField) GetInternalName() string {
if m != nil {
return m.InternalName
}
return ""
}
func (m *RenamableField) GetDisplayName() string {
if m != nil {
return m.DisplayName
}
return ""
}
func (m *RenamableField) GetVisible() bool {
if m != nil {
return m.Visible
}
return false
}
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"`
@ -316,7 +414,7 @@ type Color struct {
func (m *Color) Reset() { *m = Color{} }
func (m *Color) String() string { return proto.CompactTextString(m) }
func (*Color) ProtoMessage() {}
func (*Color) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{3} }
func (*Color) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{5} }
func (m *Color) GetID() string {
if m != nil {
@ -361,7 +459,7 @@ type Legend struct {
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 (*Legend) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{6} }
func (m *Legend) GetType() string {
if m != nil {
@ -390,7 +488,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{5} }
func (*Axis) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{7} }
func (m *Axis) GetLegacyBounds() []int64 {
if m != nil {
@ -453,7 +551,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{6} }
func (*Template) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{8} }
func (m *Template) GetID() string {
if m != nil {
@ -506,7 +604,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{7} }
func (*TemplateValue) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{9} }
func (m *TemplateValue) GetType() string {
if m != nil {
@ -541,7 +639,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{8} }
func (*TemplateQuery) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{10} }
func (m *TemplateQuery) GetCommand() string {
if m != nil {
@ -586,20 +684,21 @@ func (m *TemplateQuery) GetFieldKey() string {
}
type Server struct {
ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
Username string `protobuf:"bytes,3,opt,name=Username,proto3" json:"Username,omitempty"`
Password string `protobuf:"bytes,4,opt,name=Password,proto3" json:"Password,omitempty"`
URL string `protobuf:"bytes,5,opt,name=URL,proto3" json:"URL,omitempty"`
SrcID int64 `protobuf:"varint,6,opt,name=SrcID,proto3" json:"SrcID,omitempty"`
Active bool `protobuf:"varint,7,opt,name=Active,proto3" json:"Active,omitempty"`
Organization string `protobuf:"bytes,8,opt,name=Organization,proto3" json:"Organization,omitempty"`
ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
Username string `protobuf:"bytes,3,opt,name=Username,proto3" json:"Username,omitempty"`
Password string `protobuf:"bytes,4,opt,name=Password,proto3" json:"Password,omitempty"`
URL string `protobuf:"bytes,5,opt,name=URL,proto3" json:"URL,omitempty"`
SrcID int64 `protobuf:"varint,6,opt,name=SrcID,proto3" json:"SrcID,omitempty"`
Active bool `protobuf:"varint,7,opt,name=Active,proto3" json:"Active,omitempty"`
Organization string `protobuf:"bytes,8,opt,name=Organization,proto3" json:"Organization,omitempty"`
InsecureSkipVerify bool `protobuf:"varint,9,opt,name=InsecureSkipVerify,proto3" json:"InsecureSkipVerify,omitempty"`
}
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{9} }
func (*Server) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{11} }
func (m *Server) GetID() int64 {
if m != nil {
@ -657,6 +756,13 @@ func (m *Server) GetOrganization() string {
return ""
}
func (m *Server) GetInsecureSkipVerify() bool {
if m != nil {
return m.InsecureSkipVerify
}
return false
}
type Layout struct {
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
Application string `protobuf:"bytes,2,opt,name=Application,proto3" json:"Application,omitempty"`
@ -668,7 +774,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{10} }
func (*Layout) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{12} }
func (m *Layout) GetID() string {
if m != nil {
@ -722,7 +828,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{11} }
func (*Cell) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{13} }
func (m *Cell) GetX() int32 {
if m != nil {
@ -816,7 +922,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{12} }
func (*Query) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{14} }
func (m *Query) GetCommand() string {
if m != nil {
@ -890,7 +996,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{13} }
func (*TimeShift) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{15} }
func (m *TimeShift) GetLabel() string {
if m != nil {
@ -921,7 +1027,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{14} }
func (*Range) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{16} }
func (m *Range) GetUpper() int64 {
if m != nil {
@ -947,7 +1053,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{15} }
func (*AlertRule) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{17} }
func (m *AlertRule) GetID() string {
if m != nil {
@ -989,7 +1095,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{16} }
func (*User) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{18} }
func (m *User) GetID() uint64 {
if m != nil {
@ -1041,7 +1147,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{17} }
func (*Role) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{19} }
func (m *Role) GetOrganization() string {
if m != nil {
@ -1068,7 +1174,7 @@ type Mapping struct {
func (m *Mapping) Reset() { *m = Mapping{} }
func (m *Mapping) String() string { return proto.CompactTextString(m) }
func (*Mapping) ProtoMessage() {}
func (*Mapping) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{18} }
func (*Mapping) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{20} }
func (m *Mapping) GetProvider() string {
if m != nil {
@ -1114,7 +1220,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{19} }
func (*Organization) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{21} }
func (m *Organization) GetID() string {
if m != nil {
@ -1144,7 +1250,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{20} }
func (*Config) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{22} }
func (m *Config) GetAuth() *AuthConfig {
if m != nil {
@ -1160,7 +1266,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{21} }
func (*AuthConfig) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{23} }
func (m *AuthConfig) GetSuperAdminNewUsers() bool {
if m != nil {
@ -1177,7 +1283,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{22} }
func (*BuildInfo) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{24} }
func (m *BuildInfo) GetVersion() string {
if m != nil {
@ -1197,6 +1303,8 @@ func init() {
proto.RegisterType((*Source)(nil), "internal.Source")
proto.RegisterType((*Dashboard)(nil), "internal.Dashboard")
proto.RegisterType((*DashboardCell)(nil), "internal.DashboardCell")
proto.RegisterType((*TableOptions)(nil), "internal.TableOptions")
proto.RegisterType((*RenamableField)(nil), "internal.RenamableField")
proto.RegisterType((*Color)(nil), "internal.Color")
proto.RegisterType((*Legend)(nil), "internal.Legend")
proto.RegisterType((*Axis)(nil), "internal.Axis")
@ -1222,93 +1330,105 @@ func init() {
func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) }
var fileDescriptorInternal = []byte{
// 1406 bytes of a gzipped FileDescriptorProto
// 1586 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x57, 0x5f, 0x8f, 0xdb, 0x44,
0x10, 0x97, 0x63, 0x3b, 0x89, 0x27, 0xd7, 0x52, 0x99, 0x13, 0x35, 0x45, 0x42, 0xc1, 0x02, 0x11,
0x04, 0x3d, 0xd0, 0x55, 0x48, 0x08, 0x41, 0xa5, 0xdc, 0x05, 0x95, 0xa3, 0xd7, 0xf6, 0xba, 0xb9,
0x3b, 0x9e, 0x50, 0xb5, 0x97, 0x4c, 0x12, 0xab, 0x8e, 0x6d, 0xd6, 0xf6, 0x5d, 0xcc, 0x87, 0x41,
0x42, 0x82, 0x2f, 0x80, 0x78, 0xe7, 0x15, 0xf1, 0x41, 0xf8, 0x0a, 0x3c, 0x21, 0xa1, 0xd9, 0x5d,
0xff, 0xc9, 0x25, 0xad, 0xfa, 0x80, 0x78, 0xdb, 0xdf, 0xcc, 0x66, 0x76, 0xfe, 0xfc, 0x66, 0xc6,
0x81, 0x9b, 0x41, 0x94, 0xa1, 0x88, 0x78, 0xb8, 0x97, 0x88, 0x38, 0x8b, 0xdd, 0x6e, 0x89, 0xfd,
0xbf, 0x5a, 0xd0, 0x1e, 0xc7, 0xb9, 0x98, 0xa0, 0x7b, 0x13, 0x5a, 0x47, 0x23, 0xcf, 0xe8, 0x1b,
0x03, 0x93, 0xb5, 0x8e, 0x46, 0xae, 0x0b, 0xd6, 0x63, 0xbe, 0x44, 0xaf, 0xd5, 0x37, 0x06, 0x0e,
0x93, 0x67, 0x92, 0x9d, 0x16, 0x09, 0x7a, 0xa6, 0x92, 0xd1, 0xd9, 0xbd, 0x03, 0xdd, 0xb3, 0x94,
0xac, 0x2d, 0xd1, 0xb3, 0xa4, 0xbc, 0xc2, 0xa4, 0x3b, 0xe1, 0x69, 0x7a, 0x15, 0x8b, 0xa9, 0x67,
0x2b, 0x5d, 0x89, 0xdd, 0x5b, 0x60, 0x9e, 0xb1, 0x63, 0xaf, 0x2d, 0xc5, 0x74, 0x74, 0x3d, 0xe8,
0x8c, 0x70, 0xc6, 0xf3, 0x30, 0xf3, 0x3a, 0x7d, 0x63, 0xd0, 0x65, 0x25, 0x24, 0x3b, 0xa7, 0x18,
0xe2, 0x5c, 0xf0, 0x99, 0xd7, 0x55, 0x76, 0x4a, 0xec, 0xee, 0x81, 0x7b, 0x14, 0xa5, 0x38, 0xc9,
0x05, 0x8e, 0x9f, 0x07, 0xc9, 0x39, 0x8a, 0x60, 0x56, 0x78, 0x8e, 0x34, 0xb0, 0x45, 0x43, 0xaf,
0x3c, 0xc2, 0x8c, 0xd3, 0xdb, 0x20, 0x4d, 0x95, 0xd0, 0xf5, 0x61, 0x67, 0xbc, 0xe0, 0x02, 0xa7,
0x63, 0x9c, 0x08, 0xcc, 0xbc, 0x9e, 0x54, 0xaf, 0xc9, 0xe8, 0xce, 0x13, 0x31, 0xe7, 0x51, 0xf0,
0x03, 0xcf, 0x82, 0x38, 0xf2, 0x76, 0xd4, 0x9d, 0xa6, 0x8c, 0xb2, 0xc4, 0xe2, 0x10, 0xbd, 0x1b,
0x2a, 0x4b, 0x74, 0xf6, 0x7f, 0x33, 0xc0, 0x19, 0xf1, 0x74, 0x71, 0x11, 0x73, 0x31, 0x7d, 0xa5,
0x5c, 0xdf, 0x05, 0x7b, 0x82, 0x61, 0x98, 0x7a, 0x66, 0xdf, 0x1c, 0xf4, 0xf6, 0x6f, 0xef, 0x55,
0x45, 0xac, 0xec, 0x1c, 0x62, 0x18, 0x32, 0x75, 0xcb, 0xfd, 0x04, 0x9c, 0x0c, 0x97, 0x49, 0xc8,
0x33, 0x4c, 0x3d, 0x4b, 0xfe, 0xc4, 0xad, 0x7f, 0x72, 0xaa, 0x55, 0xac, 0xbe, 0xb4, 0x11, 0x8a,
0xbd, 0x19, 0x8a, 0xff, 0x4f, 0x0b, 0x6e, 0xac, 0x3d, 0xe7, 0xee, 0x80, 0xb1, 0x92, 0x9e, 0xdb,
0xcc, 0x58, 0x11, 0x2a, 0xa4, 0xd7, 0x36, 0x33, 0x0a, 0x42, 0x57, 0x92, 0x1b, 0x36, 0x33, 0xae,
0x08, 0x2d, 0x24, 0x23, 0x6c, 0x66, 0x2c, 0xdc, 0x0f, 0xa0, 0xf3, 0x7d, 0x8e, 0x22, 0xc0, 0xd4,
0xb3, 0xa5, 0x77, 0xaf, 0xd5, 0xde, 0x3d, 0xcd, 0x51, 0x14, 0xac, 0xd4, 0x53, 0x36, 0x24, 0x9b,
0x14, 0x35, 0xe4, 0x99, 0x64, 0x19, 0x31, 0xaf, 0xa3, 0x64, 0x74, 0xd6, 0x59, 0x54, 0x7c, 0xa0,
0x2c, 0x7e, 0x0a, 0x16, 0x5f, 0x61, 0xea, 0x39, 0xd2, 0xfe, 0x3b, 0x2f, 0x48, 0xd8, 0xde, 0x70,
0x85, 0xe9, 0x57, 0x51, 0x26, 0x0a, 0x26, 0xaf, 0xbb, 0xef, 0x43, 0x7b, 0x12, 0x87, 0xb1, 0x48,
0x3d, 0xb8, 0xee, 0xd8, 0x21, 0xc9, 0x99, 0x56, 0xbb, 0x03, 0x68, 0x87, 0x38, 0xc7, 0x68, 0x2a,
0x99, 0xd1, 0xdb, 0xbf, 0x55, 0x5f, 0x3c, 0x96, 0x72, 0xa6, 0xf5, 0x77, 0x1e, 0x80, 0x53, 0xbd,
0x42, 0x44, 0x7f, 0x8e, 0x85, 0xcc, 0x99, 0xc3, 0xe8, 0xe8, 0xbe, 0x0b, 0xf6, 0x25, 0x0f, 0x73,
0x55, 0xef, 0xde, 0xfe, 0xcd, 0xda, 0xce, 0x70, 0x15, 0xa4, 0x4c, 0x29, 0x3f, 0x6f, 0x7d, 0x66,
0xf8, 0x73, 0xb0, 0xa5, 0x0f, 0x0d, 0xc6, 0x38, 0x25, 0x63, 0x64, 0x27, 0xb6, 0x1a, 0x9d, 0x78,
0x0b, 0xcc, 0xaf, 0x71, 0xa5, 0x9b, 0x93, 0x8e, 0x15, 0xaf, 0xac, 0x06, 0xaf, 0x76, 0xc1, 0x3e,
0x97, 0x8f, 0xab, 0x7a, 0x2b, 0xe0, 0xdf, 0x87, 0xb6, 0x8a, 0xa1, 0xb2, 0x6c, 0x34, 0x2c, 0xf7,
0xa1, 0xf7, 0x44, 0x04, 0x18, 0x65, 0x8a, 0x29, 0xea, 0xd1, 0xa6, 0xc8, 0xff, 0xd5, 0x00, 0x8b,
0x9c, 0x27, 0x56, 0x85, 0x38, 0xe7, 0x93, 0xe2, 0x20, 0xce, 0xa3, 0x69, 0xea, 0x19, 0x7d, 0x73,
0x60, 0xb2, 0x35, 0x99, 0xfb, 0x06, 0xb4, 0x2f, 0x94, 0xb6, 0xd5, 0x37, 0x07, 0x0e, 0xd3, 0x88,
0x5c, 0x0b, 0xf9, 0x05, 0x86, 0x3a, 0x04, 0x05, 0xe8, 0x76, 0x22, 0x70, 0x16, 0xac, 0x74, 0x18,
0x1a, 0x91, 0x3c, 0xcd, 0x67, 0x24, 0x57, 0x91, 0x68, 0x44, 0x01, 0x5c, 0xf0, 0xb4, 0xa2, 0x0f,
0x9d, 0xc9, 0x72, 0x3a, 0xe1, 0x61, 0xc9, 0x1f, 0x05, 0xfc, 0xdf, 0x0d, 0x9a, 0x2b, 0xaa, 0x1f,
0x36, 0x32, 0xfc, 0x26, 0x74, 0xa9, 0x57, 0x9e, 0x5d, 0x72, 0xa1, 0x03, 0xee, 0x10, 0x3e, 0xe7,
0xc2, 0xfd, 0x18, 0xda, 0xb2, 0x44, 0x5b, 0x7a, 0xb3, 0x34, 0x27, 0xb3, 0xca, 0xf4, 0xb5, 0x8a,
0xbd, 0x56, 0x83, 0xbd, 0x55, 0xb0, 0x76, 0x33, 0xd8, 0xbb, 0x60, 0x53, 0x1b, 0x14, 0xd2, 0xfb,
0xad, 0x96, 0x55, 0xb3, 0xa8, 0x5b, 0xfe, 0x19, 0xdc, 0x58, 0x7b, 0xb1, 0x7a, 0xc9, 0x58, 0x7f,
0xa9, 0xa6, 0x9b, 0xa3, 0xe9, 0x45, 0x33, 0x35, 0xc5, 0x10, 0x27, 0x19, 0x4e, 0x65, 0xbe, 0xbb,
0xac, 0xc2, 0xfe, 0x4f, 0x46, 0x6d, 0x57, 0xbe, 0x47, 0x53, 0x73, 0x12, 0x2f, 0x97, 0x3c, 0x9a,
0x6a, 0xd3, 0x25, 0xa4, 0xbc, 0x4d, 0x2f, 0xb4, 0xe9, 0xd6, 0xf4, 0x82, 0xb0, 0x48, 0x74, 0x05,
0x5b, 0x22, 0x21, 0xee, 0x2c, 0x91, 0xa7, 0xb9, 0xc0, 0x25, 0x46, 0x99, 0x4e, 0x41, 0x53, 0xe4,
0xde, 0x86, 0x4e, 0xc6, 0xe7, 0xcf, 0xa8, 0x49, 0x74, 0x25, 0x33, 0x3e, 0x7f, 0x88, 0x85, 0xfb,
0x16, 0x38, 0xb3, 0x00, 0xc3, 0xa9, 0x54, 0xa9, 0x72, 0x76, 0xa5, 0xe0, 0x21, 0x16, 0xfe, 0x1f,
0x06, 0xb4, 0xc7, 0x28, 0x2e, 0x51, 0xbc, 0xd2, 0x38, 0x6d, 0xae, 0x29, 0xf3, 0x25, 0x6b, 0xca,
0xda, 0xbe, 0xa6, 0xec, 0x7a, 0x4d, 0xed, 0x82, 0x3d, 0x16, 0x93, 0xa3, 0x91, 0xf4, 0xc8, 0x64,
0x0a, 0x10, 0x1b, 0x87, 0x93, 0x2c, 0xb8, 0x44, 0xbd, 0xbb, 0x34, 0xda, 0x98, 0xb2, 0xdd, 0x2d,
0x53, 0xf6, 0x47, 0x03, 0xda, 0xc7, 0xbc, 0x88, 0xf3, 0x6c, 0x83, 0x85, 0x7d, 0xe8, 0x0d, 0x93,
0x24, 0x0c, 0x26, 0x6b, 0x9d, 0xd7, 0x10, 0xd1, 0x8d, 0x47, 0x8d, 0xfc, 0xaa, 0xd8, 0x9a, 0x22,
0x1a, 0x37, 0x87, 0x72, 0x93, 0xa8, 0xb5, 0xd0, 0x18, 0x37, 0x6a, 0x81, 0x48, 0x25, 0x25, 0x61,
0x98, 0x67, 0xf1, 0x2c, 0x8c, 0xaf, 0x64, 0xb4, 0x5d, 0x56, 0x61, 0xff, 0xcf, 0x16, 0x58, 0xff,
0xd7, 0xf4, 0xdf, 0x01, 0x23, 0xd0, 0xc5, 0x36, 0x82, 0x6a, 0x17, 0x74, 0x1a, 0xbb, 0xc0, 0x83,
0x4e, 0x21, 0x78, 0x34, 0xc7, 0xd4, 0xeb, 0xca, 0xe9, 0x52, 0x42, 0xa9, 0x91, 0x7d, 0xa4, 0x96,
0x80, 0xc3, 0x4a, 0x58, 0xf5, 0x05, 0x34, 0xfa, 0xe2, 0x23, 0xbd, 0x2f, 0x7a, 0xd2, 0x23, 0x6f,
0x3d, 0x2d, 0xd7, 0xd7, 0xc4, 0x7f, 0x37, 0xd3, 0xff, 0x36, 0xc0, 0xae, 0x9a, 0xea, 0x70, 0xbd,
0xa9, 0x0e, 0xeb, 0xa6, 0x1a, 0x1d, 0x94, 0x4d, 0x35, 0x3a, 0x20, 0xcc, 0x4e, 0xca, 0xa6, 0x62,
0x27, 0x54, 0xac, 0x07, 0x22, 0xce, 0x93, 0x83, 0x42, 0x55, 0xd5, 0x61, 0x15, 0x26, 0x26, 0x7e,
0xbb, 0x40, 0xa1, 0x53, 0xed, 0x30, 0x8d, 0x88, 0xb7, 0xc7, 0x72, 0xe0, 0xa8, 0xe4, 0x2a, 0xe0,
0xbe, 0x07, 0x36, 0xa3, 0xe4, 0xc9, 0x0c, 0xaf, 0xd5, 0x45, 0x8a, 0x99, 0xd2, 0x92, 0x51, 0xf5,
0x9d, 0xa8, 0x09, 0x5c, 0x7e, 0x35, 0x7e, 0x08, 0xed, 0xf1, 0x22, 0x98, 0x65, 0xe5, 0xd6, 0x7d,
0xbd, 0x31, 0xb0, 0x82, 0x25, 0x4a, 0x1d, 0xd3, 0x57, 0xfc, 0xa7, 0xe0, 0x54, 0xc2, 0xda, 0x1d,
0xa3, 0xe9, 0x8e, 0x0b, 0xd6, 0x59, 0x14, 0x64, 0x65, 0xeb, 0xd2, 0x99, 0x82, 0x7d, 0x9a, 0xf3,
0x28, 0x0b, 0xb2, 0xa2, 0x6c, 0xdd, 0x12, 0xfb, 0xf7, 0xb4, 0xfb, 0x64, 0xee, 0x2c, 0x49, 0x50,
0xe8, 0x31, 0xa0, 0x80, 0x7c, 0x24, 0xbe, 0x42, 0x35, 0xc1, 0x4d, 0xa6, 0x80, 0xff, 0x1d, 0x38,
0xc3, 0x10, 0x45, 0xc6, 0xf2, 0x10, 0xb7, 0x6d, 0xd6, 0x6f, 0xc6, 0x4f, 0x1e, 0x97, 0x1e, 0xd0,
0xb9, 0x6e, 0x79, 0xf3, 0x5a, 0xcb, 0x3f, 0xe4, 0x09, 0x3f, 0x1a, 0x49, 0x9e, 0x9b, 0x4c, 0x23,
0xff, 0x67, 0x03, 0x2c, 0x9a, 0x2d, 0x0d, 0xd3, 0xd6, 0xcb, 0xe6, 0xd2, 0x89, 0x88, 0x2f, 0x83,
0x29, 0x8a, 0x32, 0xb8, 0x12, 0xcb, 0xa4, 0x4f, 0x16, 0x58, 0x2d, 0x70, 0x8d, 0x88, 0x6b, 0xf4,
0x51, 0x59, 0xf6, 0x52, 0x83, 0x6b, 0x24, 0x66, 0x4a, 0xe9, 0xbe, 0x0d, 0x30, 0xce, 0x13, 0x14,
0xc3, 0xe9, 0x32, 0x88, 0x64, 0xd1, 0xbb, 0xac, 0x21, 0xf1, 0xef, 0xab, 0xcf, 0xd4, 0x8d, 0x09,
0x65, 0x6c, 0xff, 0xa4, 0xbd, 0xee, 0xb9, 0xff, 0x8b, 0x01, 0x9d, 0x47, 0x3c, 0x49, 0x82, 0x68,
0xbe, 0x16, 0x85, 0xf1, 0xc2, 0x28, 0x5a, 0x6b, 0x51, 0xec, 0xc3, 0x6e, 0x79, 0x67, 0xed, 0x7d,
0x95, 0x85, 0xad, 0x3a, 0x9d, 0x51, 0xab, 0x2a, 0xd6, 0xab, 0x7c, 0xc3, 0x9e, 0xae, 0xdf, 0xd9,
0x56, 0xf0, 0x8d, 0xaa, 0xf4, 0xa1, 0xa7, 0xff, 0x7b, 0xc8, 0x2f, 0x79, 0x3d, 0x54, 0x1b, 0x22,
0x7f, 0x1f, 0xda, 0x87, 0x71, 0x34, 0x0b, 0xe6, 0xee, 0x00, 0xac, 0x61, 0x9e, 0x2d, 0xa4, 0xc5,
0xde, 0xfe, 0x6e, 0xa3, 0xf1, 0xf3, 0x6c, 0xa1, 0xee, 0x30, 0x79, 0xc3, 0xff, 0x02, 0xa0, 0x96,
0xd1, 0x1f, 0x97, 0xba, 0x1a, 0x8f, 0xf1, 0x8a, 0x28, 0x93, 0x4a, 0x2b, 0x5d, 0xb6, 0x45, 0xe3,
0x7f, 0x09, 0xce, 0x41, 0x1e, 0x84, 0xd3, 0xa3, 0x68, 0x16, 0xd3, 0xe8, 0x38, 0x47, 0x91, 0xd6,
0xf5, 0x2a, 0x21, 0xa5, 0x9b, 0xa6, 0x48, 0xd5, 0x43, 0x1a, 0x5d, 0xb4, 0xe5, 0x7f, 0xbf, 0x7b,
0xff, 0x06, 0x00, 0x00, 0xff, 0xff, 0xfe, 0xe9, 0xd1, 0x8f, 0x0d, 0x0e, 0x00, 0x00,
0x10, 0x97, 0x93, 0x38, 0x89, 0x27, 0xd7, 0xe3, 0x64, 0x4e, 0xad, 0x29, 0x12, 0x0a, 0x16, 0x7f,
0xc2, 0x9f, 0x1e, 0x55, 0x2a, 0xa4, 0xaa, 0x82, 0x4a, 0xb9, 0x0b, 0x2d, 0x47, 0xaf, 0xbd, 0xeb,
0xe6, 0xee, 0x78, 0x42, 0xd5, 0x26, 0x99, 0x24, 0x56, 0x1d, 0xdb, 0xac, 0xed, 0xbb, 0x98, 0x8f,
0xc0, 0x87, 0x40, 0x42, 0x82, 0x2f, 0x80, 0x78, 0xe1, 0x89, 0x77, 0x3e, 0x08, 0x5f, 0x01, 0x1e,
0xd1, 0xec, 0xae, 0x1d, 0xe7, 0x92, 0x56, 0x45, 0x42, 0xbc, 0xed, 0x6f, 0x66, 0x3c, 0xbb, 0xf3,
0x7f, 0x0c, 0xdb, 0x5e, 0x90, 0xa0, 0x08, 0xb8, 0xbf, 0x17, 0x89, 0x30, 0x09, 0xed, 0x66, 0x8e,
0xdd, 0x3f, 0x2b, 0x50, 0x1f, 0x84, 0xa9, 0x18, 0xa1, 0xbd, 0x0d, 0x95, 0xc3, 0xbe, 0x63, 0xb4,
0x8d, 0x4e, 0x95, 0x55, 0x0e, 0xfb, 0xb6, 0x0d, 0xb5, 0x27, 0x7c, 0x8e, 0x4e, 0xa5, 0x6d, 0x74,
0x2c, 0x26, 0xcf, 0x44, 0x3b, 0xcd, 0x22, 0x74, 0xaa, 0x8a, 0x46, 0x67, 0xfb, 0x26, 0x34, 0xcf,
0x62, 0xd2, 0x36, 0x47, 0xa7, 0x26, 0xe9, 0x05, 0x26, 0xde, 0x09, 0x8f, 0xe3, 0xcb, 0x50, 0x8c,
0x1d, 0x53, 0xf1, 0x72, 0x6c, 0xef, 0x40, 0xf5, 0x8c, 0x1d, 0x39, 0x75, 0x49, 0xa6, 0xa3, 0xed,
0x40, 0xa3, 0x8f, 0x13, 0x9e, 0xfa, 0x89, 0xd3, 0x68, 0x1b, 0x9d, 0x26, 0xcb, 0x21, 0xe9, 0x39,
0x45, 0x1f, 0xa7, 0x82, 0x4f, 0x9c, 0xa6, 0xd2, 0x93, 0x63, 0x7b, 0x0f, 0xec, 0xc3, 0x20, 0xc6,
0x51, 0x2a, 0x70, 0xf0, 0xdc, 0x8b, 0xce, 0x51, 0x78, 0x93, 0xcc, 0xb1, 0xa4, 0x82, 0x0d, 0x1c,
0xba, 0xe5, 0x31, 0x26, 0x9c, 0xee, 0x06, 0xa9, 0x2a, 0x87, 0xb6, 0x0b, 0x5b, 0x83, 0x19, 0x17,
0x38, 0x1e, 0xe0, 0x48, 0x60, 0xe2, 0xb4, 0x24, 0x7b, 0x85, 0x46, 0x32, 0xc7, 0x62, 0xca, 0x03,
0xef, 0x3b, 0x9e, 0x78, 0x61, 0xe0, 0x6c, 0x29, 0x99, 0x32, 0x8d, 0xbc, 0xc4, 0x42, 0x1f, 0x9d,
0x6b, 0xca, 0x4b, 0x74, 0x76, 0x7f, 0x35, 0xc0, 0xea, 0xf3, 0x78, 0x36, 0x0c, 0xb9, 0x18, 0xbf,
0x92, 0xaf, 0x6f, 0x81, 0x39, 0x42, 0xdf, 0x8f, 0x9d, 0x6a, 0xbb, 0xda, 0x69, 0x75, 0x6f, 0xec,
0x15, 0x41, 0x2c, 0xf4, 0x1c, 0xa0, 0xef, 0x33, 0x25, 0x65, 0xdf, 0x06, 0x2b, 0xc1, 0x79, 0xe4,
0xf3, 0x04, 0x63, 0xa7, 0x26, 0x3f, 0xb1, 0x97, 0x9f, 0x9c, 0x6a, 0x16, 0x5b, 0x0a, 0xad, 0x99,
0x62, 0xae, 0x9b, 0xe2, 0xfe, 0x56, 0x85, 0x6b, 0x2b, 0xd7, 0xd9, 0x5b, 0x60, 0x2c, 0xe4, 0xcb,
0x4d, 0x66, 0x2c, 0x08, 0x65, 0xf2, 0xd5, 0x26, 0x33, 0x32, 0x42, 0x97, 0x32, 0x37, 0x4c, 0x66,
0x5c, 0x12, 0x9a, 0xc9, 0x8c, 0x30, 0x99, 0x31, 0xb3, 0x3f, 0x80, 0xc6, 0xb7, 0x29, 0x0a, 0x0f,
0x63, 0xc7, 0x94, 0xaf, 0x7b, 0x6d, 0xf9, 0xba, 0xa7, 0x29, 0x8a, 0x8c, 0xe5, 0x7c, 0xf2, 0x86,
0xcc, 0x26, 0x95, 0x1a, 0xf2, 0x4c, 0xb4, 0x84, 0x32, 0xaf, 0xa1, 0x68, 0x74, 0xd6, 0x5e, 0x54,
0xf9, 0x40, 0x5e, 0xfc, 0x14, 0x6a, 0x7c, 0x81, 0xb1, 0x63, 0x49, 0xfd, 0x6f, 0xbf, 0xc0, 0x61,
0x7b, 0xbd, 0x05, 0xc6, 0x5f, 0x04, 0x89, 0xc8, 0x98, 0x14, 0xb7, 0xdf, 0x87, 0xfa, 0x28, 0xf4,
0x43, 0x11, 0x3b, 0x70, 0xf5, 0x61, 0x07, 0x44, 0x67, 0x9a, 0x6d, 0x77, 0xa0, 0xee, 0xe3, 0x14,
0x83, 0xb1, 0xcc, 0x8c, 0x56, 0x77, 0x67, 0x29, 0x78, 0x24, 0xe9, 0x4c, 0xf3, 0xed, 0x7b, 0xb0,
0x95, 0xf0, 0xa1, 0x8f, 0xc7, 0x11, 0x79, 0x31, 0x96, 0x59, 0xd2, 0xea, 0x5e, 0x2f, 0xc5, 0xa3,
0xc4, 0x65, 0x2b, 0xb2, 0x37, 0x1f, 0x82, 0x55, 0xbc, 0x90, 0x8a, 0xe4, 0x39, 0x66, 0xd2, 0xdf,
0x16, 0xa3, 0xa3, 0xfd, 0x0e, 0x98, 0x17, 0xdc, 0x4f, 0x55, 0xae, 0xb4, 0xba, 0xdb, 0x4b, 0x9d,
0xbd, 0x85, 0x17, 0x33, 0xc5, 0xbc, 0x57, 0xb9, 0x6b, 0xb8, 0xdf, 0x57, 0x60, 0xab, 0x7c, 0x8f,
0xfd, 0x16, 0x40, 0xe2, 0xcd, 0xf1, 0x41, 0x28, 0xe6, 0x3c, 0xd1, 0x3a, 0x4b, 0x14, 0xfb, 0x43,
0xd8, 0xb9, 0x40, 0x91, 0x78, 0x23, 0xee, 0x9f, 0x7a, 0x73, 0x24, 0x7d, 0xf2, 0x96, 0x26, 0x5b,
0xa3, 0xdb, 0xb7, 0xa1, 0x1e, 0x87, 0x22, 0xd9, 0xcf, 0x64, 0xbc, 0x5b, 0x5d, 0x67, 0xf9, 0x0e,
0x86, 0x01, 0x9f, 0xd3, 0xbd, 0x0f, 0x3c, 0xf4, 0xc7, 0x4c, 0xcb, 0x51, 0x0d, 0x5f, 0x0a, 0x1e,
0x45, 0x5e, 0x30, 0xcd, 0xfb, 0x44, 0x8e, 0xed, 0xbb, 0x00, 0x13, 0x12, 0xa6, 0xc4, 0xcf, 0xf3,
0xe3, 0xc5, 0x1a, 0x4b, 0xb2, 0xf6, 0x7b, 0xb0, 0x3d, 0xf1, 0x16, 0x0f, 0x3c, 0x11, 0x27, 0x07,
0xa1, 0x9f, 0xce, 0x03, 0x99, 0x35, 0x4d, 0x76, 0x85, 0xea, 0x46, 0xb0, 0xbd, 0xaa, 0x85, 0xd2,
0x3f, 0xbf, 0x40, 0xd6, 0x9e, 0xf2, 0xc7, 0x0a, 0xcd, 0x6e, 0x43, 0x6b, 0xec, 0xc5, 0x91, 0xcf,
0xb3, 0x52, 0x79, 0x96, 0x49, 0xd4, 0x4d, 0x2e, 0xbc, 0xd8, 0x1b, 0xfa, 0xaa, 0x29, 0x36, 0x59,
0x0e, 0xdd, 0x29, 0x98, 0x32, 0x7d, 0x4a, 0xc5, 0x6e, 0xe5, 0xc5, 0x2e, 0x9b, 0x68, 0xa5, 0xd4,
0x44, 0x77, 0xa0, 0xfa, 0x25, 0x2e, 0x74, 0x5f, 0xa5, 0x63, 0xd1, 0x12, 0x6a, 0xa5, 0x96, 0xb0,
0x0b, 0xe6, 0xb9, 0x8c, 0xbd, 0x2a, 0x55, 0x05, 0xdc, 0xfb, 0x50, 0x57, 0xe9, 0x57, 0x68, 0x36,
0x4a, 0x9a, 0xdb, 0xd0, 0x3a, 0x16, 0x1e, 0x06, 0x89, 0x2a, 0x72, 0x6d, 0x42, 0x89, 0xe4, 0xfe,
0x62, 0x40, 0x4d, 0xc6, 0xd4, 0x85, 0x2d, 0x1f, 0xa7, 0x7c, 0x94, 0xed, 0x87, 0x69, 0x30, 0x8e,
0x1d, 0xa3, 0x5d, 0xed, 0x54, 0xd9, 0x0a, 0xcd, 0xbe, 0x0e, 0xf5, 0xa1, 0xe2, 0x56, 0xda, 0xd5,
0x8e, 0xc5, 0x34, 0xa2, 0xa7, 0xf9, 0x7c, 0x88, 0xbe, 0x36, 0x41, 0x01, 0x92, 0x8e, 0x04, 0x4e,
0xbc, 0x85, 0x36, 0x43, 0x23, 0xa2, 0xc7, 0xe9, 0x84, 0xe8, 0xca, 0x12, 0x8d, 0xc8, 0x80, 0x21,
0x8f, 0x8b, 0xca, 0xa7, 0x33, 0x69, 0x8e, 0x47, 0xdc, 0xcf, 0x4b, 0x5f, 0x01, 0xf7, 0x77, 0x83,
0x46, 0x82, 0x6a, 0x65, 0x6b, 0x1e, 0x7e, 0x03, 0x9a, 0xd4, 0xe6, 0x9e, 0x5d, 0x70, 0xa1, 0x0d,
0x6e, 0x10, 0x3e, 0xe7, 0xc2, 0xfe, 0x04, 0xea, 0xb2, 0x42, 0x36, 0xb4, 0xd5, 0x5c, 0x9d, 0xf4,
0x2a, 0xd3, 0x62, 0x45, 0xe3, 0xa9, 0x95, 0x1a, 0x4f, 0x61, 0xac, 0x59, 0x36, 0xf6, 0x16, 0x98,
0xd4, 0xc1, 0x32, 0xf9, 0xfa, 0x8d, 0x9a, 0x55, 0x9f, 0x53, 0x52, 0xee, 0x19, 0x5c, 0x5b, 0xb9,
0xb1, 0xb8, 0xc9, 0x58, 0xbd, 0x69, 0x59, 0xed, 0x96, 0xae, 0x6e, 0x2a, 0xa5, 0x18, 0x7d, 0x1c,
0x25, 0x38, 0xd6, 0x59, 0x57, 0x60, 0xf7, 0x47, 0x63, 0xa9, 0x57, 0xde, 0x47, 0x29, 0x3a, 0x0a,
0xe7, 0x73, 0x1e, 0x8c, 0xb5, 0xea, 0x1c, 0x92, 0xdf, 0xc6, 0x43, 0xad, 0xba, 0x32, 0x1e, 0x12,
0x16, 0x91, 0x8e, 0x60, 0x45, 0x44, 0x94, 0x3b, 0x73, 0xe4, 0x71, 0x2a, 0x70, 0x8e, 0x41, 0xa2,
0x5d, 0x50, 0x26, 0xd9, 0x37, 0xa0, 0x91, 0xf0, 0xe9, 0x33, 0xea, 0x51, 0x3a, 0x92, 0x09, 0x9f,
0x3e, 0xc2, 0xcc, 0x7e, 0x13, 0x2c, 0x59, 0xa5, 0x92, 0xa5, 0xc2, 0xd9, 0x94, 0x84, 0x47, 0x98,
0xb9, 0x7f, 0x1b, 0x50, 0x1f, 0xa0, 0xb8, 0x40, 0xf1, 0x4a, 0x93, 0xb0, 0xbc, 0x61, 0x54, 0x5f,
0xb2, 0x61, 0xd4, 0x36, 0x6f, 0x18, 0xe6, 0x72, 0xc3, 0xd8, 0x05, 0x73, 0x20, 0x46, 0x87, 0x7d,
0xf9, 0xa2, 0x2a, 0x53, 0x80, 0xb2, 0xb1, 0x37, 0x4a, 0xbc, 0x0b, 0xd4, 0x6b, 0x87, 0x46, 0x6b,
0x03, 0xb2, 0xb9, 0x61, 0xd6, 0xff, 0xcb, 0xed, 0xc3, 0xfd, 0xc1, 0x80, 0xfa, 0x11, 0xcf, 0xc2,
0x34, 0x59, 0xcb, 0xda, 0x36, 0xb4, 0x7a, 0x51, 0xe4, 0x7b, 0xa3, 0x95, 0x4a, 0x2d, 0x91, 0x48,
0xe2, 0x71, 0x29, 0x1e, 0xca, 0x17, 0x65, 0x12, 0x4d, 0x87, 0x03, 0xb9, 0x34, 0xa8, 0x0d, 0xa0,
0x34, 0x1d, 0xd4, 0xae, 0x20, 0x99, 0xe4, 0xb4, 0x5e, 0x9a, 0x84, 0x13, 0x3f, 0xbc, 0x94, 0xde,
0x69, 0xb2, 0x02, 0xbb, 0x7f, 0x54, 0xa0, 0xf6, 0x7f, 0x0d, 0xfa, 0x2d, 0x30, 0x3c, 0x9d, 0x1c,
0x86, 0x57, 0x8c, 0xfd, 0x46, 0x69, 0xec, 0x3b, 0xd0, 0xc8, 0x04, 0x0f, 0xa6, 0x18, 0x3b, 0x4d,
0xd9, 0x8d, 0x72, 0x28, 0x39, 0xb2, 0xee, 0xd4, 0xbc, 0xb7, 0x58, 0x0e, 0x8b, 0x3a, 0x82, 0x52,
0x1d, 0x7d, 0xac, 0x57, 0x83, 0xd6, 0xd5, 0xd1, 0xb2, 0x69, 0x23, 0xf8, 0xef, 0x46, 0xf0, 0x5f,
0x06, 0x98, 0x45, 0x11, 0x1e, 0xac, 0x16, 0xe1, 0xc1, 0xb2, 0x08, 0xfb, 0xfb, 0x79, 0x11, 0xf6,
0xf7, 0x09, 0xb3, 0x93, 0xbc, 0x08, 0xd9, 0x09, 0x05, 0xeb, 0xa1, 0x08, 0xd3, 0x68, 0x3f, 0x53,
0x51, 0xb5, 0x58, 0x81, 0x29, 0x73, 0xbf, 0x9e, 0xa1, 0xd0, 0xae, 0xb6, 0x98, 0x46, 0x94, 0xe7,
0x47, 0xb2, 0x41, 0x29, 0xe7, 0x2a, 0x60, 0xbf, 0x0b, 0x26, 0x23, 0xe7, 0x49, 0x0f, 0xaf, 0xc4,
0x45, 0x92, 0x99, 0xe2, 0x92, 0x52, 0xf5, 0x4b, 0xa0, 0x13, 0x3e, 0xff, 0x41, 0xf8, 0x08, 0xea,
0x83, 0x99, 0x37, 0x49, 0xf2, 0x05, 0xeb, 0xf5, 0x52, 0x83, 0xf3, 0xe6, 0x28, 0x79, 0x4c, 0x8b,
0xb8, 0x4f, 0xc1, 0x2a, 0x88, 0xcb, 0xe7, 0x18, 0xe5, 0xe7, 0xd8, 0x50, 0x3b, 0x0b, 0xbc, 0x24,
0x2f, 0x75, 0x3a, 0x93, 0xb1, 0x4f, 0x53, 0x1e, 0x24, 0x5e, 0x92, 0xe5, 0xa5, 0x9e, 0x63, 0xf7,
0x8e, 0x7e, 0x3e, 0xa9, 0x3b, 0x8b, 0x22, 0x14, 0xba, 0x6d, 0x28, 0x20, 0x2f, 0x09, 0x2f, 0x51,
0x75, 0xfc, 0x2a, 0x53, 0xc0, 0xfd, 0x06, 0xac, 0x9e, 0x8f, 0x22, 0x61, 0xa9, 0x8f, 0x9b, 0x26,
0xf1, 0x57, 0x83, 0xe3, 0x27, 0xf9, 0x0b, 0xe8, 0xbc, 0x6c, 0x11, 0xd5, 0x2b, 0x2d, 0xe2, 0x11,
0x8f, 0xf8, 0x61, 0x5f, 0xe6, 0x79, 0x95, 0x69, 0xe4, 0xfe, 0x64, 0x40, 0x8d, 0x7a, 0x51, 0x49,
0x75, 0xed, 0x65, 0x7d, 0xec, 0x44, 0x84, 0x17, 0xde, 0x18, 0x45, 0x6e, 0x5c, 0x8e, 0xa5, 0xd3,
0x47, 0x33, 0x2c, 0x06, 0xbe, 0x46, 0x94, 0x6b, 0xf4, 0xff, 0x90, 0xd7, 0x52, 0x29, 0xd7, 0x88,
0xcc, 0x14, 0x93, 0x36, 0xbb, 0x41, 0x1a, 0xa1, 0xe8, 0x8d, 0xe7, 0x5e, 0xbe, 0x01, 0x95, 0x28,
0xee, 0x7d, 0xf5, 0x47, 0xb2, 0xd6, 0xd1, 0x8c, 0xcd, 0x7f, 0x2f, 0x57, 0x5f, 0xee, 0xfe, 0x6c,
0x40, 0xe3, 0xb1, 0xde, 0xd5, 0xca, 0x56, 0x18, 0x2f, 0xb4, 0xa2, 0xb2, 0x62, 0x45, 0x17, 0x76,
0x73, 0x99, 0x95, 0xfb, 0x95, 0x17, 0x36, 0xf2, 0xb4, 0x47, 0x6b, 0x45, 0xb0, 0x5e, 0xe5, 0x77,
0xe5, 0x74, 0x55, 0x66, 0x53, 0xc0, 0xd7, 0xa2, 0xd2, 0x86, 0x96, 0xfe, 0xcd, 0x94, 0x3f, 0x6d,
0xba, 0xa9, 0x96, 0x48, 0x6e, 0x17, 0xea, 0x07, 0x61, 0x30, 0xf1, 0xa6, 0x76, 0x07, 0x6a, 0xbd,
0x34, 0x99, 0x49, 0x8d, 0xad, 0xee, 0x6e, 0xa9, 0xf0, 0xd3, 0x64, 0xa6, 0x64, 0x98, 0x94, 0x70,
0x3f, 0x03, 0x58, 0xd2, 0x68, 0x4a, 0x2c, 0xa3, 0xf1, 0x04, 0x2f, 0x29, 0x65, 0x62, 0xa9, 0xa5,
0xc9, 0x36, 0x70, 0xdc, 0xcf, 0xc1, 0xda, 0x4f, 0x3d, 0x7f, 0x7c, 0x18, 0x4c, 0x42, 0x6a, 0x1d,
0xe7, 0x28, 0xe2, 0x65, 0xbc, 0x72, 0x48, 0xee, 0xa6, 0x2e, 0x52, 0xd4, 0x90, 0x46, 0xc3, 0xba,
0xfc, 0xcd, 0xbf, 0xf3, 0x4f, 0x00, 0x00, 0x00, 0xff, 0xff, 0xda, 0x7c, 0x0d, 0xab, 0xf8, 0x0f,
0x00, 0x00,
}

View File

@ -37,6 +37,22 @@ message DashboardCell {
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
TableOptions tableOptions = 12; // TableOptions for visualization of cell with type 'table'
}
message TableOptions {
string timeFormat = 1; // format for time
bool verticalTimeAxis = 2; // time axis should be a column not row
RenamableField sortBy = 3; // which column should a table be sorted by
string wrapping = 4; // option for text wrapping
repeated RenamableField fieldNames = 5; // names and renames for column/row fields
bool fixFirstColumn = 6; // first column should be fixed/frozen
}
message RenamableField {
string internalName = 1; // name of column
string displayName = 2; // what column is renamed to
bool visible = 3; // Represents whether RenamableField is visible
}
message Color {
@ -95,6 +111,7 @@ message Server {
int64 SrcID = 6; // SrcID is the ID of the data source
bool Active = 7; // is this the currently active server for the source
string Organization = 8; // Organization is the organization ID that resource belongs to
bool InsecureSkipVerify = 9; // InsecureSkipVerify accepts any certificate from the client
}
message Layout {

View File

@ -76,12 +76,13 @@ func TestMarshalSourceWithSecret(t *testing.T) {
func TestMarshalServer(t *testing.T) {
v := chronograf.Server{
ID: 12,
SrcID: 2,
Name: "Fountain of Truth",
Username: "docbrown",
Password: "1 point twenty-one g1g@w@tts",
URL: "http://oldmanpeabody.mall.io:9092",
ID: 12,
SrcID: 2,
Name: "Fountain of Truth",
Username: "docbrown",
Password: "1 point twenty-one g1g@w@tts",
URL: "http://oldmanpeabody.mall.io:9092",
InsecureSkipVerify: true,
}
var vv chronograf.Server
@ -193,6 +194,10 @@ func Test_MarshalDashboard(t *testing.T) {
Value: "100",
},
},
TableOptions: chronograf.TableOptions{
TimeFormat: "",
FieldNames: []chronograf.RenamableField{},
},
},
},
Templates: []chronograf.Template{},
@ -255,6 +260,9 @@ func Test_MarshalDashboard_WithLegacyBounds(t *testing.T) {
Type: "static",
Orientation: "bottom",
},
TableOptions: chronograf.TableOptions{
TimeFormat: "MM:DD:YYYY",
},
Type: "line",
},
},
@ -309,6 +317,10 @@ func Test_MarshalDashboard_WithLegacyBounds(t *testing.T) {
Type: "static",
Orientation: "bottom",
},
TableOptions: chronograf.TableOptions{
TimeFormat: "MM:DD:YYYY",
FieldNames: []chronograf.RenamableField{},
},
Type: "line",
},
},
@ -369,6 +381,9 @@ func Test_MarshalDashboard_WithEmptyLegacyBounds(t *testing.T) {
},
},
Type: "line",
TableOptions: chronograf.TableOptions{
TimeFormat: "MM:DD:YYYY",
},
},
},
Templates: []chronograf.Template{},
@ -418,6 +433,10 @@ func Test_MarshalDashboard_WithEmptyLegacyBounds(t *testing.T) {
Value: "100",
},
},
TableOptions: chronograf.TableOptions{
TimeFormat: "MM:DD:YYYY",
FieldNames: []chronograf.RenamableField{},
},
Type: "line",
},
},
@ -434,3 +453,40 @@ func Test_MarshalDashboard_WithEmptyLegacyBounds(t *testing.T) {
t.Fatalf("Dashboard protobuf copy error: diff follows:\n%s", cmp.Diff(expected, actual))
}
}
func Test_MarshalDashboard_WithEmptyCellType(t *testing.T) {
dashboard := chronograf.Dashboard{
ID: 1,
Cells: []chronograf.DashboardCell{
{
ID: "9b5367de-c552-4322-a9e8-7f384cbd235c",
},
},
}
expected := chronograf.Dashboard{
ID: 1,
Cells: []chronograf.DashboardCell{
{
ID: "9b5367de-c552-4322-a9e8-7f384cbd235c",
Type: "line",
Queries: []chronograf.DashboardQuery{},
Axes: map[string]chronograf.Axis{},
CellColors: []chronograf.CellColor{},
TableOptions: chronograf.TableOptions{
FieldNames: []chronograf.RenamableField{},
},
},
},
Templates: []chronograf.Template{},
}
var actual chronograf.Dashboard
if buf, err := internal.MarshalDashboard(dashboard); err != nil {
t.Fatal("Error marshaling dashboard: err", err)
} else if err := internal.UnmarshalDashboard(buf, &actual); err != nil {
t.Fatal("Error unmarshaling dashboard: err:", err)
} else if !cmp.Equal(expected, actual) {
t.Fatalf("Dashboard protobuf copy error: diff follows:\n%s", cmp.Diff(expected, actual))
}
}

View File

@ -20,22 +20,24 @@ func TestServerStore(t *testing.T) {
srcs := []chronograf.Server{
chronograf.Server{
Name: "Of Truth",
SrcID: 10,
Username: "marty",
Password: "I❤ jennifer parker",
URL: "toyota-hilux.lyon-estates.local",
Active: false,
Organization: "133",
Name: "Of Truth",
SrcID: 10,
Username: "marty",
Password: "I❤ jennifer parker",
URL: "toyota-hilux.lyon-estates.local",
Active: false,
Organization: "133",
InsecureSkipVerify: true,
},
chronograf.Server{
Name: "HipToBeSquare",
SrcID: 12,
Username: "calvinklein",
Password: "chuck b3rry",
URL: "toyota-hilux.lyon-estates.local",
Active: false,
Organization: "133",
Name: "HipToBeSquare",
SrcID: 12,
Username: "calvinklein",
Password: "chuck b3rry",
URL: "toyota-hilux.lyon-estates.local",
Active: false,
Organization: "133",
InsecureSkipVerify: false,
},
}

View File

@ -150,26 +150,26 @@ func (s *UsersStore) Add(ctx context.Context, u *chronograf.User) (*chronograf.U
}
// Delete a user from the UsersStore
func (s *UsersStore) Delete(ctx context.Context, usr *chronograf.User) error {
_, err := s.get(ctx, usr.ID)
func (s *UsersStore) Delete(ctx context.Context, u *chronograf.User) error {
_, err := s.get(ctx, u.ID)
if err != nil {
return err
}
return s.client.db.Update(func(tx *bolt.Tx) error {
return tx.Bucket(UsersBucket).Delete(u64tob(usr.ID))
return tx.Bucket(UsersBucket).Delete(u64tob(u.ID))
})
}
// Update a user
func (s *UsersStore) Update(ctx context.Context, usr *chronograf.User) error {
_, err := s.get(ctx, usr.ID)
func (s *UsersStore) Update(ctx context.Context, u *chronograf.User) error {
_, err := s.get(ctx, u.ID)
if err != nil {
return err
}
return s.client.db.Update(func(tx *bolt.Tx) error {
if v, err := internal.MarshalUser(usr); err != nil {
if v, err := internal.MarshalUser(u); err != nil {
return err
} else if err := tx.Bucket(UsersBucket).Put(u64tob(usr.ID), v); err != nil {
} else if err := tx.Bucket(UsersBucket).Put(u64tob(u.ID), v); err != nil {
return err
}
return nil

View File

@ -56,6 +56,7 @@
]
}
],
"colors": [],
"type": "single-stat"
},
{
@ -73,6 +74,7 @@
]
}
],
"colors": [],
"type": "single-stat"
},
{

View File

@ -117,6 +117,7 @@
"h": 4,
"i": "0fa47984-825b-46f1-9ca5-0366e3220008",
"name": "Mesos Master Uptime",
"colors": [],
"type": "single-stat",
"queries": [
{

View File

@ -34,6 +34,10 @@ const (
ErrOrganizationAlreadyExists = Error("organization already exists")
ErrCannotDeleteDefaultOrganization = Error("cannot delete default organization")
ErrConfigNotFound = Error("cannot find configuration")
ErrAnnotationNotFound = Error("annotation not found")
ErrInvalidCellOptionsText = Error("invalid text wrapping option. Valid wrappings are 'truncate', 'wrap', and 'single line'")
ErrInvalidCellOptionsSort = Error("cell options sortby cannot be empty'")
ErrInvalidCellOptionsColumns = Error("cell options columns cannot be empty'")
)
// Error is a domain error encountered while processing chronograf requests
@ -98,12 +102,24 @@ type TSDBStatus interface {
Type(context.Context) (string, error)
}
// Point is a field set in a series
type Point struct {
Database string
RetentionPolicy string
Measurement string
Time int64
Tags map[string]string
Fields map[string]interface{}
}
// TimeSeries represents a queryable time series database.
type TimeSeries interface {
// Query retrieves time series data from the database.
Query(context.Context, Query) (Response, error)
// Connect will connect to the time series using the information in `Source`.
Connect(context.Context, *Source) error
// Query retrieves time series data from the database.
Query(context.Context, Query) (Response, error)
// Write records points into a series
Write(context.Context, []Point) error
// UsersStore represents the user accounts within the TimeSeries database
Users(context.Context) UsersStore
// Permissions returns all valid names permissions in this database
@ -170,6 +186,7 @@ type Query struct {
Command string `json:"query"` // Command is the query itself
DB string `json:"db,omitempty"` // DB is optional and if empty will not be used.
RP string `json:"rp,omitempty"` // RP is a retention policy and optional; if empty will not be used.
Epoch string `json:"epoch,omitempty"` // Epoch is the time format for the return results
TemplateVars []TemplateVar `json:"tempVars,omitempty"` // TemplateVars are template variables to replace within an InfluxQL query
Wheres []string `json:"wheres,omitempty"` // Wheres restricts the query to certain attributes
GroupBys []string `json:"groupbys,omitempty"` // GroupBys collate the query by these tags
@ -235,7 +252,7 @@ type SourcesStore interface {
Update(context.Context, Source) error
}
// DBRP is a database and retention policy for a kapacitor task
// DBRP represents a database and retention policy for a time series source
type DBRP struct {
DB string `json:"db"`
RP string `json:"rp"`
@ -341,15 +358,15 @@ type KapacitorProperty struct {
// Server represents a proxy connection to an HTTP server
type Server struct {
ID int // ID is the unique ID of the server
SrcID int // SrcID of the data source
Name string // Name is the user-defined name for the server
Username string // Username is the username to connect to the server
Password string // Password is in CLEARTEXT
URL string // URL are the connections to the server
InsecureSkipVerify bool // InsecureSkipVerify as true means any certificate presented by the server is accepted.
Active bool // Is this the active server for the source?
Organization string // Organization is the organization ID that resource belongs to
ID int `json:"id,string"` // ID is the unique ID of the server
SrcID int `json:"srcId,string"` // SrcID of the data source
Name string `json:"name"` // Name is the user-defined name for the server
Username string `json:"username"` // Username is the username to connect to the server
Password string `json:"password"` // Password is in CLEARTEXT
URL string `json:"url"` // URL are the connections to the server
InsecureSkipVerify bool `json:"insecureSkipVerify"` // InsecureSkipVerify as true means any certificate presented by the server is accepted.
Active bool `json:"active"` // Is this the active server for the source?
Organization string `json:"organization"` // Organization is the organization ID that resource belongs to
}
// ServersStore stores connection information for a `Server`
@ -471,6 +488,24 @@ type Databases interface {
DropRP(context.Context, string, string) error
}
// Annotation represents a time-based metadata associated with a source
type Annotation struct {
ID string // ID is the unique annotation identifier
StartTime time.Time // StartTime starts the annotation
EndTime time.Time // EndTime ends the annotation
Text string // Text is the associated user-facing text describing the annotation
Type string // Type describes the kind of annotation
}
// AnnotationStore represents storage and retrieval of annotations
type AnnotationStore interface {
All(ctx context.Context, start, stop time.Time) ([]Annotation, error) // All lists all Annotations between start and stop
Add(context.Context, *Annotation) (*Annotation, error) // Add creates a new annotation in the store
Delete(ctx context.Context, id string) error // Delete removes the annotation from the store
Get(ctx context.Context, id string) (*Annotation, error) // Get retrieves an annotation
Update(context.Context, *Annotation) error // Update replaces annotation
}
// DashboardID is the dashboard ID
type DashboardID int
@ -511,17 +546,35 @@ type Legend struct {
// DashboardCell holds visual and query information for a cell
type DashboardCell struct {
ID string `json:"i"`
X int32 `json:"x"`
Y int32 `json:"y"`
W int32 `json:"w"`
H int32 `json:"h"`
Name string `json:"name"`
Queries []DashboardQuery `json:"queries"`
Axes map[string]Axis `json:"axes"`
Type string `json:"type"`
CellColors []CellColor `json:"colors"`
Legend Legend `json:"legend"`
ID string `json:"i"`
X int32 `json:"x"`
Y int32 `json:"y"`
W int32 `json:"w"`
H int32 `json:"h"`
Name string `json:"name"`
Queries []DashboardQuery `json:"queries"`
Axes map[string]Axis `json:"axes"`
Type string `json:"type"`
CellColors []CellColor `json:"colors"`
Legend Legend `json:"legend"`
TableOptions TableOptions `json:"tableOptions,omitempty"`
}
// RenamableField is a column/row field in a DashboardCell of type Table
type RenamableField struct {
InternalName string `json:"internalName"`
DisplayName string `json:"displayName"`
Visible bool `json:"visible"`
}
// TableOptions is a type of options for a DashboardCell with type Table
type TableOptions struct {
TimeFormat string `json:"timeFormat"`
VerticalTimeAxis bool `json:"verticalTimeAxis"`
SortBy RenamableField `json:"sortBy"`
Wrapping string `json:"wrapping"`
FieldNames []RenamableField `json:"fieldNames"`
FixFirstColumn bool `json:"fixFirstColumn"`
}
// DashboardsStore is the storage and retrieval of dashboards
@ -540,15 +593,16 @@ type DashboardsStore interface {
// Cell is a rectangle and multiple time series queries to visualize.
type Cell struct {
X int32 `json:"x"`
Y int32 `json:"y"`
W int32 `json:"w"`
H int32 `json:"h"`
I string `json:"i"`
Name string `json:"name"`
Queries []Query `json:"queries"`
Axes map[string]Axis `json:"axes"`
Type string `json:"type"`
X int32 `json:"x"`
Y int32 `json:"y"`
W int32 `json:"w"`
H int32 `json:"h"`
I string `json:"i"`
Name string `json:"name"`
Queries []Query `json:"queries"`
Axes map[string]Axis `json:"axes"`
Type string `json:"type"`
CellColors []CellColor `json:"colors"`
}
// Layout is a collection of Cells for visualization

View File

@ -1 +1 @@
**We've moved our documentation!** Check out the latest [authentication content](https://docs.influxdata.com/chronograf/latest/administration/security-best-practices/#chronograf-with-oauth-2-0-authentication) on InfluxData's [main docs site](https://docs.influxdata.com/chronograf/latest/).
**We've moved our documentation!** Check out the latest [authentication content](https://docs.influxdata.com/chronograf/latest/administration/managing-security/#oauth-2-0-providers-with-jwt-tokens) on InfluxData's [main docs site](https://docs.influxdata.com/chronograf/latest/).

View File

@ -144,6 +144,14 @@ func (c *Client) Query(ctx context.Context, q chronograf.Query) (chronograf.Resp
return c.nextDataNode().Query(ctx, q)
}
// Write records points into a time series
func (c *Client) Write(ctx context.Context, points []chronograf.Point) error {
if !c.opened {
return chronograf.ErrUninitialized
}
return c.nextDataNode().Write(ctx, points)
}
// Users is the interface to the users within Influx Enterprise
func (c *Client) Users(context.Context) chronograf.UsersStore {
return c.UsersStore

View File

@ -118,6 +118,10 @@ func (ts *TimeSeries) Connect(ctx context.Context, src *chronograf.Source) error
return nil
}
func (ts *TimeSeries) Write(ctx context.Context, points []chronograf.Point) error {
return nil
}
func (ts *TimeSeries) Users(ctx context.Context) chronograf.UsersStore {
return nil
}

View File

@ -59,6 +59,8 @@ func (d *Kapacitors) All(ctx context.Context) ([]chronograf.Server, error) {
}
var kapacitor chronograf.Server
if err := d.Load(path.Join(d.Dir, file.Name()), &kapacitor); err != nil {
var fmtErr = fmt.Errorf("Error loading kapacitor configuration from %v:\n%v", path.Join(d.Dir, file.Name()), err)
d.Logger.Error(fmtErr)
continue // We want to load all files we can.
} else {
kapacitors = append(kapacitors, kapacitor)

View File

@ -59,6 +59,8 @@ func (d *Sources) All(ctx context.Context) ([]chronograf.Source, error) {
}
var source chronograf.Source
if err := d.Load(path.Join(d.Dir, file.Name()), &source); err != nil {
var fmtErr = fmt.Errorf("Error loading source configuration from %v:\n%v", path.Join(d.Dir, file.Name()), err)
d.Logger.Error(fmtErr)
continue // We want to load all files we can.
} else {
sources = append(sources, source)

271
influx/annotations.go Normal file
View File

@ -0,0 +1,271 @@
package influx
import (
"bytes"
"context"
"encoding/json"
"fmt"
"sort"
"time"
"github.com/influxdata/chronograf/id"
"github.com/influxdata/chronograf"
)
const (
// AllAnnotations returns all annotations from the chronograf database
AllAnnotations = `SELECT "start_time", "modified_time_ns", "text", "type", "id" FROM "annotations" WHERE "deleted"=false AND time >= %dns and "start_time" <= %d ORDER BY time DESC`
// GetAnnotationID returns all annotations from the chronograf database where id is %s
GetAnnotationID = `SELECT "start_time", "modified_time_ns", "text", "type", "id" FROM "annotations" WHERE "id"='%s' AND "deleted"=false ORDER BY time DESC`
// AnnotationsDB is chronograf. Perhaps later we allow this to be changed
AnnotationsDB = "chronograf"
// DefaultRP is autogen. Perhaps later we allow this to be changed
DefaultRP = "autogen"
// DefaultMeasurement is annotations.
DefaultMeasurement = "annotations"
)
var _ chronograf.AnnotationStore = &AnnotationStore{}
// AnnotationStore stores annotations within InfluxDB
type AnnotationStore struct {
client chronograf.TimeSeries
id chronograf.ID
now Now
}
// NewAnnotationStore constructs an annoation store with a client
func NewAnnotationStore(client chronograf.TimeSeries) *AnnotationStore {
return &AnnotationStore{
client: client,
id: &id.UUID{},
now: time.Now,
}
}
// All lists all Annotations
func (a *AnnotationStore) All(ctx context.Context, start, stop time.Time) ([]chronograf.Annotation, error) {
return a.queryAnnotations(ctx, fmt.Sprintf(AllAnnotations, start.UnixNano(), stop.UnixNano()))
}
// Get retrieves an annotation
func (a *AnnotationStore) Get(ctx context.Context, id string) (*chronograf.Annotation, error) {
annos, err := a.queryAnnotations(ctx, fmt.Sprintf(GetAnnotationID, id))
if err != nil {
return nil, err
}
if len(annos) == 0 {
return nil, chronograf.ErrAnnotationNotFound
}
return &annos[0], nil
}
// Add creates a new annotation in the store
func (a *AnnotationStore) Add(ctx context.Context, anno *chronograf.Annotation) (*chronograf.Annotation, error) {
var err error
anno.ID, err = a.id.Generate()
if err != nil {
return nil, err
}
return anno, a.client.Write(ctx, []chronograf.Point{
toPoint(anno, a.now()),
})
}
// Delete removes the annotation from the store
func (a *AnnotationStore) Delete(ctx context.Context, id string) error {
cur, err := a.Get(ctx, id)
if err != nil {
return err
}
return a.client.Write(ctx, []chronograf.Point{
toDeletedPoint(cur, a.now()),
})
}
// Update replaces annotation; if the annotation's time is different, it
// also removes the previous annotation
func (a *AnnotationStore) Update(ctx context.Context, anno *chronograf.Annotation) error {
cur, err := a.Get(ctx, anno.ID)
if err != nil {
return err
}
if err := a.client.Write(ctx, []chronograf.Point{toPoint(anno, a.now())}); err != nil {
return err
}
// If the updated annotation has a different time, then, we must
// delete the previous annotation
if !cur.EndTime.Equal(anno.EndTime) {
return a.client.Write(ctx, []chronograf.Point{
toDeletedPoint(cur, a.now()),
})
}
return nil
}
// queryAnnotations queries the chronograf db and produces all annotations
func (a *AnnotationStore) queryAnnotations(ctx context.Context, query string) ([]chronograf.Annotation, error) {
res, err := a.client.Query(ctx, chronograf.Query{
Command: query,
DB: AnnotationsDB,
Epoch: "ns",
})
if err != nil {
return nil, err
}
octets, err := res.MarshalJSON()
if err != nil {
return nil, err
}
results := influxResults{}
d := json.NewDecoder(bytes.NewReader(octets))
d.UseNumber()
if err := d.Decode(&results); err != nil {
return nil, err
}
return results.Annotations()
}
func toPoint(anno *chronograf.Annotation, now time.Time) chronograf.Point {
return chronograf.Point{
Database: AnnotationsDB,
RetentionPolicy: DefaultRP,
Measurement: DefaultMeasurement,
Time: anno.EndTime.UnixNano(),
Tags: map[string]string{
"id": anno.ID,
},
Fields: map[string]interface{}{
"deleted": false,
"start_time": anno.StartTime.UnixNano(),
"modified_time_ns": int64(now.UnixNano()),
"text": anno.Text,
"type": anno.Type,
},
}
}
func toDeletedPoint(anno *chronograf.Annotation, now time.Time) chronograf.Point {
return chronograf.Point{
Database: AnnotationsDB,
RetentionPolicy: DefaultRP,
Measurement: DefaultMeasurement,
Time: anno.EndTime.UnixNano(),
Tags: map[string]string{
"id": anno.ID,
},
Fields: map[string]interface{}{
"deleted": true,
"start_time": int64(0),
"modified_time_ns": int64(now.UnixNano()),
"text": "",
"type": "",
},
}
}
type value []interface{}
func (v value) Int64(idx int) (int64, error) {
if idx >= len(v) {
return 0, fmt.Errorf("index %d does not exist in values", idx)
}
n, ok := v[idx].(json.Number)
if !ok {
return 0, fmt.Errorf("value at index %d is not int64, but, %T", idx, v[idx])
}
return n.Int64()
}
func (v value) Time(idx int) (time.Time, error) {
tm, err := v.Int64(idx)
if err != nil {
return time.Time{}, err
}
return time.Unix(0, tm), nil
}
func (v value) String(idx int) (string, error) {
if idx >= len(v) {
return "", fmt.Errorf("index %d does not exist in values", idx)
}
str, ok := v[idx].(string)
if !ok {
return "", fmt.Errorf("value at index %d is not string, but, %T", idx, v[idx])
}
return str, nil
}
type influxResults []struct {
Series []struct {
Values []value `json:"values"`
} `json:"series"`
}
// annotationResult is an intermediate struct to track the latest modified
// time of an annotation
type annotationResult struct {
chronograf.Annotation
// modTime is bookkeeping to handle the case when an update fails; the latest
// modTime will be the record returned
modTime int64
}
// Annotations converts AllAnnotations query to annotations
func (r *influxResults) Annotations() (res []chronograf.Annotation, err error) {
annos := map[string]annotationResult{}
for _, u := range *r {
for _, s := range u.Series {
for _, v := range s.Values {
anno := annotationResult{}
if anno.EndTime, err = v.Time(0); err != nil {
return
}
if anno.StartTime, err = v.Time(1); err != nil {
return
}
if anno.modTime, err = v.Int64(2); err != nil {
return
}
if anno.Text, err = v.String(3); err != nil {
return
}
if anno.Type, err = v.String(4); err != nil {
return
}
if anno.ID, err = v.String(5); err != nil {
return
}
// If there are two annotations with the same id, take
// the annotation with the latest modification time
// This is to prevent issues when an update or delete fails.
// Updates and deletes are multiple step queries.
prev, ok := annos[anno.ID]
if !ok || anno.modTime > prev.modTime {
annos[anno.ID] = anno
}
}
}
}
res = []chronograf.Annotation{}
for _, a := range annos {
res = append(res, a.Annotation)
}
sort.Slice(res, func(i int, j int) bool {
return res[i].StartTime.Before(res[j].StartTime) || res[i].ID < res[j].ID
})
return res, err
}

665
influx/annotations_test.go Normal file
View File

@ -0,0 +1,665 @@
package influx
import (
"context"
"encoding/json"
"fmt"
"reflect"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/mocks"
)
func Test_toPoint(t *testing.T) {
tests := []struct {
name string
anno *chronograf.Annotation
now time.Time
want chronograf.Point
}{
0: {
name: "convert annotation to point w/o start and end times",
anno: &chronograf.Annotation{
ID: "1",
Text: "mytext",
Type: "mytype",
},
now: time.Unix(0, 0),
want: chronograf.Point{
Database: AnnotationsDB,
RetentionPolicy: DefaultRP,
Measurement: DefaultMeasurement,
Time: time.Time{}.UnixNano(),
Tags: map[string]string{
"id": "1",
},
Fields: map[string]interface{}{
"deleted": false,
"start_time": time.Time{}.UnixNano(),
"modified_time_ns": int64(time.Unix(0, 0).UnixNano()),
"text": "mytext",
"type": "mytype",
},
},
},
1: {
name: "convert annotation to point with start/end time",
anno: &chronograf.Annotation{
ID: "1",
Text: "mytext",
Type: "mytype",
StartTime: time.Unix(100, 0),
EndTime: time.Unix(200, 0),
},
now: time.Unix(0, 0),
want: chronograf.Point{
Database: AnnotationsDB,
RetentionPolicy: DefaultRP,
Measurement: DefaultMeasurement,
Time: time.Unix(200, 0).UnixNano(),
Tags: map[string]string{
"id": "1",
},
Fields: map[string]interface{}{
"deleted": false,
"start_time": time.Unix(100, 0).UnixNano(),
"modified_time_ns": int64(time.Unix(0, 0).UnixNano()),
"text": "mytext",
"type": "mytype",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := toPoint(tt.anno, tt.now); !reflect.DeepEqual(got, tt.want) {
t.Errorf("toPoint() = %v, want %v", got, tt.want)
}
})
}
}
func Test_toDeletedPoint(t *testing.T) {
tests := []struct {
name string
anno *chronograf.Annotation
now time.Time
want chronograf.Point
}{
0: {
name: "convert annotation to point w/o start and end times",
anno: &chronograf.Annotation{
ID: "1",
EndTime: time.Unix(0, 0),
},
now: time.Unix(0, 0),
want: chronograf.Point{
Database: AnnotationsDB,
RetentionPolicy: DefaultRP,
Measurement: DefaultMeasurement,
Time: 0,
Tags: map[string]string{
"id": "1",
},
Fields: map[string]interface{}{
"deleted": true,
"start_time": int64(0),
"modified_time_ns": int64(0),
"text": "",
"type": "",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := toDeletedPoint(tt.anno, tt.now); !cmp.Equal(got, tt.want) {
t.Errorf("toDeletedPoint() = %s", cmp.Diff(got, tt.want))
}
})
}
}
func Test_value_Int64(t *testing.T) {
tests := []struct {
name string
v value
idx int
want int64
wantErr bool
}{
{
name: "index out of range returns error",
idx: 1,
wantErr: true,
},
{
name: "converts a string to int64",
v: value{
json.Number("1"),
},
idx: 0,
want: int64(1),
},
{
name: "when not a json.Number, return error",
v: value{
"howdy",
},
idx: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.v.Int64(tt.idx)
if (err != nil) != tt.wantErr {
t.Errorf("value.Int64() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("value.Int64() = %v, want %v", got, tt.want)
}
})
}
}
func Test_value_Time(t *testing.T) {
tests := []struct {
name string
v value
idx int
want time.Time
wantErr bool
}{
{
name: "index out of range returns error",
idx: 1,
wantErr: true,
},
{
name: "converts a string to int64",
v: value{
json.Number("1"),
},
idx: 0,
want: time.Unix(0, 1),
},
{
name: "when not a json.Number, return error",
v: value{
"howdy",
},
idx: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.v.Time(tt.idx)
if (err != nil) != tt.wantErr {
t.Errorf("value.Time() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("value.Time() = %v, want %v", got, tt.want)
}
})
}
}
func Test_value_String(t *testing.T) {
tests := []struct {
name string
v value
idx int
want string
wantErr bool
}{
{
name: "index out of range returns error",
idx: 1,
wantErr: true,
},
{
name: "converts a string",
v: value{
"howdy",
},
idx: 0,
want: "howdy",
},
{
name: "when not a string, return error",
v: value{
0,
},
idx: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.v.String(tt.idx)
if (err != nil) != tt.wantErr {
t.Errorf("value.String() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("value.String() = %v, want %v", got, tt.want)
}
})
}
}
func TestAnnotationStore_queryAnnotations(t *testing.T) {
type args struct {
ctx context.Context
query string
}
tests := []struct {
name string
client chronograf.TimeSeries
args args
want []chronograf.Annotation
wantErr bool
}{
{
name: "query error returns an error",
client: &mocks.TimeSeries{
QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
return nil, fmt.Errorf("error")
},
},
wantErr: true,
},
{
name: "response marshal error returns an error",
client: &mocks.TimeSeries{
QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
return mocks.NewResponse("", fmt.Errorf("")), nil
},
},
wantErr: true,
},
{
name: "Bad JSON returns an error",
client: &mocks.TimeSeries{
QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
return mocks.NewResponse(`{}`, nil), nil
},
},
wantErr: true,
},
{
name: "Incorrect fields returns error",
client: &mocks.TimeSeries{
QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
return mocks.NewResponse(`[{
"series": [
{
"name": "annotations",
"columns": [
"time",
"deleted",
"id",
"modified_time_ns",
"start_time",
"text",
"type"
],
"values": [
[
1516920117000000000,
true,
"4ba9f836-20e8-4b8e-af51-e1363edd7b6d",
1517425994487495051,
0,
"",
""
]
]
}
]
}
]}]`, nil), nil
},
},
wantErr: true,
},
{
name: "two annotation response",
client: &mocks.TimeSeries{
QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
return mocks.NewResponse(`[
{
"series": [
{
"name": "annotations",
"columns": [
"time",
"start_time",
"modified_time_ns",
"text",
"type",
"id"
],
"values": [
[
1516920177345000000,
0,
1516989242129417403,
"mytext",
"mytype",
"ecf3a75d-f1c0-40e8-9790-902701467e92"
],
[
1516920177345000000,
0,
1517425914433539296,
"mytext2",
"mytype2",
"ea0aa94b-969a-4cd5-912a-5db61d502268"
]
]
}
]
}
]`, nil), nil
},
},
want: []chronograf.Annotation{
{
EndTime: time.Unix(0, 1516920177345000000),
StartTime: time.Unix(0, 0),
Text: "mytext2",
Type: "mytype2",
ID: "ea0aa94b-969a-4cd5-912a-5db61d502268",
},
{
EndTime: time.Unix(0, 1516920177345000000),
StartTime: time.Unix(0, 0),
Text: "mytext",
Type: "mytype",
ID: "ecf3a75d-f1c0-40e8-9790-902701467e92",
},
},
},
{
name: "same id returns one",
client: &mocks.TimeSeries{
QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
return mocks.NewResponse(`[
{
"series": [
{
"name": "annotations",
"columns": [
"time",
"start_time",
"modified_time_ns",
"text",
"type",
"id"
],
"values": [
[
1516920177345000000,
0,
1516989242129417403,
"mytext",
"mytype",
"ea0aa94b-969a-4cd5-912a-5db61d502268"
],
[
1516920177345000000,
0,
1517425914433539296,
"mytext2",
"mytype2",
"ea0aa94b-969a-4cd5-912a-5db61d502268"
]
]
}
]
}
]`, nil), nil
},
},
want: []chronograf.Annotation{
{
EndTime: time.Unix(0, 1516920177345000000),
StartTime: time.Unix(0, 0),
Text: "mytext2",
Type: "mytype2",
ID: "ea0aa94b-969a-4cd5-912a-5db61d502268",
},
},
},
{
name: "no responses returns empty array",
client: &mocks.TimeSeries{
QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
return mocks.NewResponse(`[ { } ]`, nil), nil
},
},
want: []chronograf.Annotation{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &AnnotationStore{
client: tt.client,
}
got, err := a.queryAnnotations(tt.args.ctx, tt.args.query)
if (err != nil) != tt.wantErr {
t.Errorf("AnnotationStore.queryAnnotations() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("AnnotationStore.queryAnnotations() = %v, want %v", got, tt.want)
}
})
}
}
func TestAnnotationStore_Update(t *testing.T) {
type fields struct {
client chronograf.TimeSeries
now Now
}
type args struct {
ctx context.Context
anno *chronograf.Annotation
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
{
name: "no responses returns error",
fields: fields{
client: &mocks.TimeSeries{
QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
return mocks.NewResponse(`[ { } ]`, nil), nil
},
WriteF: func(context.Context, []chronograf.Point) error {
return nil
},
},
},
args: args{
ctx: context.Background(),
anno: &chronograf.Annotation{
ID: "1",
},
},
wantErr: true,
},
{
name: "error writing returns error",
fields: fields{
now: func() time.Time { return time.Time{} },
client: &mocks.TimeSeries{
QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
return mocks.NewResponse(`[
{
"series": [
{
"name": "annotations",
"columns": [
"time",
"start_time",
"modified_time_ns",
"text",
"type",
"id"
],
"values": [
[
1516920177345000000,
0,
1516989242129417403,
"mytext",
"mytype",
"ecf3a75d-f1c0-40e8-9790-902701467e92"
],
[
1516920177345000000,
0,
1517425914433539296,
"mytext2",
"mytype2",
"ea0aa94b-969a-4cd5-912a-5db61d502268"
]
]
}
]
}
]`, nil), nil
},
WriteF: func(context.Context, []chronograf.Point) error {
return fmt.Errorf("error")
},
},
},
args: args{
ctx: context.Background(),
anno: &chronograf.Annotation{
ID: "1",
},
},
wantErr: true,
},
{
name: "Update with delete",
fields: fields{
now: func() time.Time { return time.Time{} },
client: &mocks.TimeSeries{
QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
return mocks.NewResponse(`[
{
"series": [
{
"name": "annotations",
"columns": [
"time",
"start_time",
"modified_time_ns",
"text",
"type",
"id"
],
"values": [
[
1516920177345000000,
0,
1516989242129417403,
"mytext",
"mytype",
"ecf3a75d-f1c0-40e8-9790-902701467e92"
]
]
}
]
}
]`, nil), nil
},
WriteF: func(context.Context, []chronograf.Point) error {
return nil
},
},
},
args: args{
ctx: context.Background(),
anno: &chronograf.Annotation{
ID: "1",
},
},
},
{
name: "Update with delete no delete",
fields: fields{
now: func() time.Time { return time.Time{} },
client: &mocks.TimeSeries{
QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
return mocks.NewResponse(`[
{
"series": [
{
"name": "annotations",
"columns": [
"time",
"start_time",
"modified_time_ns",
"text",
"type",
"id"
],
"values": [
[
1516920177345000000,
0,
1516989242129417403,
"mytext",
"mytype",
"ecf3a75d-f1c0-40e8-9790-902701467e92"
]
]
}
]
}
]`, nil), nil
},
WriteF: func(context.Context, []chronograf.Point) error {
return nil
},
},
},
args: args{
ctx: context.Background(),
anno: &chronograf.Annotation{
ID: "ecf3a75d-f1c0-40e8-9790-902701467e92",
EndTime: time.Unix(0, 1516920177345000000),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &AnnotationStore{
client: tt.fields.client,
now: tt.fields.now,
}
if err := a.Update(tt.args.ctx, tt.args.anno); (err != nil) != tt.wantErr {
t.Errorf("AnnotationStore.Update() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View File

@ -77,9 +77,6 @@ func (b *BearerJWT) Token(username string) (string, error) {
return JWT(username, b.SharedSecret, b.Now)
}
// Now returns the current time
type Now func() time.Time
// JWT returns a token string accepted by InfluxDB using the sharedSecret as an Authorization: Bearer header
func JWT(username, sharedSecret string, now Now) (string, error) {
token := &jwt.Token{

View File

@ -4,6 +4,7 @@ import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
@ -73,7 +74,10 @@ func (c *Client) query(u *url.URL, q chronograf.Query) (chronograf.Response, err
params.Set("q", command)
params.Set("db", q.DB)
params.Set("rp", q.RP)
params.Set("epoch", "ms") // TODO(timraymond): set this based on analysis
params.Set("epoch", "ms")
if q.Epoch != "" {
params.Set("epoch", q.Epoch)
}
req.URL.RawQuery = params.Encode()
if c.Authorizer != nil {
@ -261,3 +265,105 @@ func (c *Client) ping(u *url.URL) (string, string, error) {
return version, chronograf.InfluxDB, nil
}
// Write POSTs line protocol to a database and retention policy
func (c *Client) Write(ctx context.Context, points []chronograf.Point) error {
for _, point := range points {
if err := c.writePoint(ctx, &point); err != nil {
return err
}
}
return nil
}
func (c *Client) writePoint(ctx context.Context, point *chronograf.Point) error {
lp, err := toLineProtocol(point)
if err != nil {
return err
}
err = c.write(ctx, c.URL, point.Database, point.RetentionPolicy, lp)
if err == nil {
return nil
}
// Some influxdb errors should not be treated as errors
if strings.Contains(err.Error(), "hinted handoff queue not empty") {
// This is an informational message
return nil
}
// If the database was not found, try to recreate it:
if strings.Contains(err.Error(), "database not found") {
_, err = c.CreateDB(ctx, &chronograf.Database{
Name: point.Database,
})
if err != nil {
return err
}
// retry the write
return c.write(ctx, c.URL, point.Database, point.RetentionPolicy, lp)
}
return err
}
func (c *Client) write(ctx context.Context, u *url.URL, db, rp, lp string) error {
u.Path = "write"
req, err := http.NewRequest("POST", u.String(), strings.NewReader(lp))
if err != nil {
return err
}
req.Header.Set("Content-Type", "text/plain; charset=utf-8")
if c.Authorizer != nil {
if err := c.Authorizer.Set(req); err != nil {
return err
}
}
params := req.URL.Query()
params.Set("db", db)
params.Set("rp", rp)
req.URL.RawQuery = params.Encode()
hc := &http.Client{}
if c.InsecureSkipVerify {
hc.Transport = skipVerifyTransport
} else {
hc.Transport = defaultTransport
}
errChan := make(chan (error))
go func() {
resp, err := hc.Do(req)
if err != nil {
errChan <- err
return
}
if resp.StatusCode == http.StatusNoContent {
errChan <- nil
return
}
defer resp.Body.Close()
var response Response
dec := json.NewDecoder(resp.Body)
err = dec.Decode(&response)
if err != nil && err.Error() != "EOF" {
errChan <- err
return
}
errChan <- errors.New(response.Err)
return
}()
select {
case err := <-errChan:
return err
case <-ctx.Done():
return chronograf.ErrUpstreamTimeout
}
}

View File

@ -14,6 +14,7 @@ import (
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/influx"
"github.com/influxdata/chronograf/log"
"github.com/influxdata/chronograf/mocks"
)
// NewClient initializes an HTTP Client for InfluxDB.
@ -395,3 +396,153 @@ func TestClient_Roles(t *testing.T) {
t.Errorf("Client.Roles() want error")
}
}
func TestClient_write(t *testing.T) {
type fields struct {
Authorizer influx.Authorizer
InsecureSkipVerify bool
Logger chronograf.Logger
}
type args struct {
ctx context.Context
point chronograf.Point
}
tests := []struct {
name string
fields fields
args args
body string
wantErr bool
}{
{
name: "write point to influxdb",
fields: fields{
Logger: mocks.NewLogger(),
},
args: args{
ctx: context.Background(),
point: chronograf.Point{
Database: "mydb",
RetentionPolicy: "myrp",
Measurement: "mymeas",
Time: 10,
Tags: map[string]string{
"tag1": "value1",
"tag2": "value2",
},
Fields: map[string]interface{}{
"field1": "value1",
},
},
},
},
{
name: "point without fields",
args: args{
ctx: context.Background(),
point: chronograf.Point{},
},
wantErr: true,
},
{
name: "hinted handoff errors are not errors really.",
fields: fields{
Logger: mocks.NewLogger(),
},
args: args{
ctx: context.Background(),
point: chronograf.Point{
Database: "mydb",
RetentionPolicy: "myrp",
Measurement: "mymeas",
Time: 10,
Tags: map[string]string{
"tag1": "value1",
"tag2": "value2",
},
Fields: map[string]interface{}{
"field1": "value1",
},
},
},
body: `{"error":"hinted handoff queue not empty"}`,
},
{
name: "database not found creates a new db",
fields: fields{
Logger: mocks.NewLogger(),
},
args: args{
ctx: context.Background(),
point: chronograf.Point{
Database: "mydb",
RetentionPolicy: "myrp",
Measurement: "mymeas",
Time: 10,
Tags: map[string]string{
"tag1": "value1",
"tag2": "value2",
},
Fields: map[string]interface{}{
"field1": "value1",
},
},
},
body: `{"error":"database not found"}`,
},
{
name: "error from database reported",
fields: fields{
Logger: mocks.NewLogger(),
},
args: args{
ctx: context.Background(),
point: chronograf.Point{
Database: "mydb",
RetentionPolicy: "myrp",
Measurement: "mymeas",
Time: 10,
Tags: map[string]string{
"tag1": "value1",
"tag2": "value2",
},
Fields: map[string]interface{}{
"field1": "value1",
},
},
},
body: `{"error":"oh no!"}`,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
retry := 0 // if the retry is > 0 then we don't error
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.RequestURI, "/write") {
if tt.body == "" || retry > 0 {
rw.WriteHeader(http.StatusNoContent)
return
}
retry++
rw.WriteHeader(http.StatusBadRequest)
rw.Write([]byte(tt.body))
return
}
rw.WriteHeader(http.StatusOK)
rw.Write([]byte(`{"results":[{}]}`))
}))
defer ts.Close()
u, _ := url.Parse(ts.URL)
c := &influx.Client{
URL: u,
Authorizer: tt.fields.Authorizer,
InsecureSkipVerify: tt.fields.InsecureSkipVerify,
Logger: tt.fields.Logger,
}
if err := c.Write(tt.args.ctx, []chronograf.Point{tt.args.point}); (err != nil) != tt.wantErr {
t.Errorf("Client.write() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

84
influx/lineprotocol.go Normal file
View File

@ -0,0 +1,84 @@
package influx
import (
"fmt"
"sort"
"strings"
"github.com/influxdata/chronograf"
)
var (
escapeMeasurement = strings.NewReplacer(
`,` /* to */, `\,`,
` ` /* to */, `\ `,
)
escapeKeys = strings.NewReplacer(
`,` /* to */, `\,`,
`"` /* to */, `\"`,
` ` /* to */, `\ `,
`=` /* to */, `\=`,
)
escapeTagValues = strings.NewReplacer(
`,` /* to */, `\,`,
`"` /* to */, `\"`,
` ` /* to */, `\ `,
`=` /* to */, `\=`,
)
escapeFieldStrings = strings.NewReplacer(
`"` /* to */, `\"`,
`\` /* to */, `\\`,
)
)
func toLineProtocol(point *chronograf.Point) (string, error) {
measurement := escapeMeasurement.Replace(point.Measurement)
if len(measurement) == 0 {
return "", fmt.Errorf("measurement required to write point")
}
if len(point.Fields) == 0 {
return "", fmt.Errorf("at least one field required to write point")
}
tags := []string{}
for tag, value := range point.Tags {
if value != "" {
t := fmt.Sprintf("%s=%s", escapeKeys.Replace(tag), escapeTagValues.Replace(value))
tags = append(tags, t)
}
}
// it is faster to insert data into influx db if the tags are sorted
sort.Strings(tags)
fields := []string{}
for field, value := range point.Fields {
var format string
switch v := value.(type) {
case int64, int32, int16, int8, int:
format = fmt.Sprintf("%s=%di", escapeKeys.Replace(field), v)
case uint64, uint32, uint16, uint8, uint:
format = fmt.Sprintf("%s=%du", escapeKeys.Replace(field), v)
case float64, float32:
format = fmt.Sprintf("%s=%f", escapeKeys.Replace(field), v)
case string:
format = fmt.Sprintf(`%s="%s"`, escapeKeys.Replace(field), escapeFieldStrings.Replace(v))
case bool:
format = fmt.Sprintf("%s=%t", escapeKeys.Replace(field), v)
}
if format != "" {
fields = append(fields, format)
}
}
sort.Strings(fields)
lp := measurement
if len(tags) > 0 {
lp += fmt.Sprintf(",%s", strings.Join(tags, ","))
}
lp += fmt.Sprintf(" %s", strings.Join(fields, ","))
if point.Time != 0 {
lp += fmt.Sprintf(" %d", point.Time)
}
return lp, nil
}

129
influx/lineprotocol_test.go Normal file
View File

@ -0,0 +1,129 @@
package influx
import (
"testing"
"time"
"github.com/influxdata/chronograf"
)
func Test_toLineProtocol(t *testing.T) {
tests := []struct {
name string
point *chronograf.Point
want string
wantErr bool
}{
0: {
name: "requires a measurement",
point: &chronograf.Point{},
wantErr: true,
},
1: {
name: "requires at least one field",
point: &chronograf.Point{
Measurement: "telegraf",
},
wantErr: true,
},
2: {
name: "no tags produces line protocol",
point: &chronograf.Point{
Measurement: "telegraf",
Fields: map[string]interface{}{
"myfield": 1,
},
},
want: "telegraf myfield=1i",
},
3: {
name: "test all influx data types",
point: &chronograf.Point{
Measurement: "telegraf",
Fields: map[string]interface{}{
"int": 19,
"uint": uint(85),
"float": 88.0,
"string": "mph",
"time_machine": true,
"invalidField": time.Time{},
},
},
want: `telegraf float=88.000000,int=19i,string="mph",time_machine=true,uint=85u`,
},
4: {
name: "test all influx data types",
point: &chronograf.Point{
Measurement: "telegraf",
Tags: map[string]string{
"marty": "mcfly",
"doc": "brown",
},
Fields: map[string]interface{}{
"int": 19,
"uint": uint(85),
"float": 88.0,
"string": "mph",
"time_machine": true,
"invalidField": time.Time{},
},
Time: 497115501000000000,
},
want: `telegraf,doc=brown,marty=mcfly float=88.000000,int=19i,string="mph",time_machine=true,uint=85u 497115501000000000`,
},
5: {
name: "measurements with comma or spaces are escaped",
point: &chronograf.Point{
Measurement: "O Romeo, Romeo, wherefore art thou Romeo",
Tags: map[string]string{
"part": "JULIET",
},
Fields: map[string]interface{}{
"act": 2,
"scene": 2,
"page": 2,
"line": 33,
},
},
want: `O\ Romeo\,\ Romeo\,\ wherefore\ art\ thou\ Romeo,part=JULIET act=2i,line=33i,page=2i,scene=2i`,
},
6: {
name: "tags with comma, quota, space, equal are escaped",
point: &chronograf.Point{
Measurement: "quotes",
Tags: map[string]string{
"comma,": "comma,",
`quote"`: `quote"`,
"space ": `space "`,
"equal=": "equal=",
},
Fields: map[string]interface{}{
"myfield": 1,
},
},
want: `quotes,comma\,=comma\,,equal\==equal\=,quote\"=quote\",space\ =space\ \" myfield=1i`,
},
7: {
name: "fields with quotes or backslashes are escaped",
point: &chronograf.Point{
Measurement: "quotes",
Fields: map[string]interface{}{
`quote"\`: `quote"\`,
},
},
want: `quotes quote\"\="quote\"\\"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := toLineProtocol(tt.point)
if (err != nil) != tt.wantErr {
t.Errorf("toLineProtocol() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("toLineProtocol() = %v, want %v", got, tt.want)
}
})
}
}

6
influx/now.go Normal file
View File

@ -0,0 +1,6 @@
package influx
import "time"
// Now returns the current time
type Now func() time.Time

View File

@ -10,6 +10,7 @@ import (
"github.com/influxdata/influxdb/influxql"
)
// TimeRangeAsEpochNano extracs the min and max epoch times from the expression
func TimeRangeAsEpochNano(expr influxql.Expr, now time.Time) (min, max int64, err error) {
tmin, tmax, err := influxql.TimeRange(expr)
if err != nil {
@ -28,8 +29,10 @@ func TimeRangeAsEpochNano(expr influxql.Expr, now time.Time) (min, max int64, er
return
}
// WhereToken is used to parse the time expression from an influxql query
const WhereToken = "WHERE"
// ParseTime extracts the duration of the time range of the query
func ParseTime(influxQL string, now time.Time) (time.Duration, error) {
start := strings.Index(strings.ToUpper(influxQL), WhereToken)
if start == -1 {

View File

@ -9,6 +9,7 @@ import (
"github.com/influxdata/chronograf"
)
// SortTemplates the templates by size, then type, then value.
func SortTemplates(ts []chronograf.TemplateVar) []chronograf.TemplateVar {
sort.Slice(ts, func(i, j int) bool {
if len(ts[i].Values) != len(ts[j].Values) {
@ -82,6 +83,8 @@ func RenderTemplate(query string, t chronograf.TemplateVar, now time.Time) (stri
return query, nil
}
// AutoGroupBy generates the time to group by in order to decimate the number of
// points returned in a query
func AutoGroupBy(resolution, pixelsPerPoint int64, duration time.Duration) string {
// The function is: ((total_seconds * millisecond_converstion) / group_by) = pixels / 3
// Number of points given the pixels

View File

@ -126,7 +126,7 @@ func (c *Client) All(ctx context.Context) ([]chronograf.User, error) {
return users, nil
}
// Number of users in Influx
// Num is the number of users in DB
func (c *Client) Num(ctx context.Context) (int, error) {
all, err := c.All(ctx)
if err != nil {

View File

@ -113,7 +113,8 @@ func TestServer(t *testing.T) {
"permissions": "/chronograf/v1/sources/5000/permissions",
"users": "/chronograf/v1/sources/5000/users",
"roles": "/chronograf/v1/sources/5000/roles",
"databases": "/chronograf/v1/sources/5000/dbs"
"databases": "/chronograf/v1/sources/5000/dbs",
"annotations": "/chronograf/v1/sources/5000/annotations"
}
}
`,
@ -163,7 +164,8 @@ func TestServer(t *testing.T) {
"id": "5000",
"name": "Kapa 1",
"url": "http://localhost:9092",
"active": true,
"active": true,
"insecureSkipVerify": false,
"links": {
"proxy": "/chronograf/v1/sources/5000/kapacitors/5000/proxy",
"self": "/chronograf/v1/sources/5000/kapacitors/5000",
@ -221,7 +223,8 @@ func TestServer(t *testing.T) {
"id": "5000",
"name": "Kapa 1",
"url": "http://localhost:9092",
"active": true,
"active": true,
"insecureSkipVerify": false,
"links": {
"proxy": "/chronograf/v1/sources/5000/kapacitors/5000/proxy",
"self": "/chronograf/v1/sources/5000/kapacitors/5000",
@ -296,7 +299,8 @@ func TestServer(t *testing.T) {
"permissions": "/chronograf/v1/sources/5000/permissions",
"users": "/chronograf/v1/sources/5000/users",
"roles": "/chronograf/v1/sources/5000/roles",
"databases": "/chronograf/v1/sources/5000/dbs"
"databases": "/chronograf/v1/sources/5000/dbs",
"annotations": "/chronograf/v1/sources/5000/annotations"
}
}
]
@ -538,7 +542,19 @@ func TestServer(t *testing.T) {
"legend":{
"type": "static",
"orientation": "bottom"
},
},
"tableOptions":{
"timeFormat": "",
"verticalTimeAxis": false,
"sortBy":{
"internalName": "",
"displayName": "",
"visible": false
},
"wrapping": "",
"fieldNames": null,
"fixFirstColumn": false
},
"links": {
"self": "/chronograf/v1/dashboards/1000/cells/8f61c619-dd9b-4761-8aa8-577f27247093"
}
@ -777,7 +793,19 @@ func TestServer(t *testing.T) {
"name": "comet",
"value": "100"
}
],
],
"tableOptions":{
"timeFormat":"",
"verticalTimeAxis":false,
"sortBy":{
"internalName":"",
"displayName":"",
"visible":false
},
"wrapping":"",
"fieldNames":null,
"fixFirstColumn":false
},
"legend":{
"type": "static",
"orientation": "bottom"

View File

@ -1,6 +1,6 @@
{
"id": 5000,
"srcID": 5000,
"id": "5000",
"srcID": "5000",
"name": "Kapa 1",
"url": "http://localhost:9092",
"active": true,

20
mocks/response.go Normal file
View File

@ -0,0 +1,20 @@
package mocks
// NewResponse returns a mocked chronograf.Response
func NewResponse(res string, err error) *Response {
return &Response{
res: res,
err: err,
}
}
// Response is a mocked chronograf.Response
type Response struct {
res string
err error
}
// MarshalJSON returns the res and err as the fake response.
func (r *Response) MarshalJSON() ([]byte, error) {
return []byte(r.res), r.err
}

View File

@ -10,10 +10,12 @@ var _ chronograf.TimeSeries = &TimeSeries{}
// TimeSeries is a mockable chronograf time series by overriding the functions.
type TimeSeries struct {
// Query retrieves time series data from the database.
QueryF func(context.Context, chronograf.Query) (chronograf.Response, error)
// Connect will connect to the time series using the information in `Source`.
ConnectF func(context.Context, *chronograf.Source) error
// Query retrieves time series data from the database.
QueryF func(context.Context, chronograf.Query) (chronograf.Response, error)
// Write records points into the TimeSeries
WriteF func(context.Context, []chronograf.Point) error
// UsersStore represents the user accounts within the TimeSeries database
UsersF func(context.Context) chronograf.UsersStore
// Permissions returns all valid names permissions in this database
@ -27,14 +29,19 @@ func (t *TimeSeries) New(chronograf.Source, chronograf.Logger) (chronograf.TimeS
return t, nil
}
// Connect will connect to the time series using the information in `Source`.
func (t *TimeSeries) Connect(ctx context.Context, src *chronograf.Source) error {
return t.ConnectF(ctx, src)
}
// Query retrieves time series data from the database.
func (t *TimeSeries) Query(ctx context.Context, query chronograf.Query) (chronograf.Response, error) {
return t.QueryF(ctx, query)
}
// Connect will connect to the time series using the information in `Source`.
func (t *TimeSeries) Connect(ctx context.Context, src *chronograf.Source) error {
return t.ConnectF(ctx, src)
// Write records a point into the time series
func (t *TimeSeries) Write(ctx context.Context, points []chronograf.Point) error {
return t.WriteF(ctx, points)
}
// Users represents the user accounts within the TimeSeries database

View File

@ -123,9 +123,7 @@ func (g *Generic) PrincipalID(provider *http.Client) (string, error) {
// Group returns the domain that a user belongs to in the
// the generic OAuth.
func (g *Generic) Group(provider *http.Client) (string, error) {
res := struct {
Email string `json:"email"`
}{}
res := map[string]interface{}{}
r, err := provider.Get(g.APIURL)
if err != nil {
@ -137,12 +135,27 @@ func (g *Generic) Group(provider *http.Client) (string, error) {
return "", err
}
email := strings.Split(res.Email, "@")
if len(email) != 2 {
return "", fmt.Errorf("malformed email address, expected %q to contain @ symbol", res.Email)
email := ""
value := res[g.APIKey]
if e, ok := value.(string); ok {
email = e
}
return email[1], nil
// If we did not receive an email address, try to lookup the email
// in a similar way as github
if email == "" {
email, err = g.getPrimaryEmail(provider)
if err != nil {
return "", err
}
}
domain := strings.Split(email, "@")
if len(domain) != 2 {
return "", fmt.Errorf("malformed email address, expected %q to contain @ symbol", email)
}
return domain[1], nil
}
// UserEmail represents user's email address

View File

@ -10,6 +10,98 @@ import (
"github.com/influxdata/chronograf/oauth2"
)
func TestGenericGroup_withNotEmail(t *testing.T) {
t.Parallel()
response := struct {
Email string `json:"not-email"`
}{
"martymcfly@pinheads.rok",
}
mockAPI := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
rw.WriteHeader(http.StatusNotFound)
return
}
enc := json.NewEncoder(rw)
rw.WriteHeader(http.StatusOK)
_ = enc.Encode(response)
}))
defer mockAPI.Close()
logger := clog.New(clog.ParseLevel("debug"))
prov := oauth2.Generic{
Logger: logger,
APIURL: mockAPI.URL,
APIKey: "not-email",
}
tt, err := oauth2.NewTestTripper(logger, mockAPI, http.DefaultTransport)
if err != nil {
t.Fatal("Error initializing TestTripper: err:", err)
}
tc := &http.Client{
Transport: tt,
}
got, err := prov.Group(tc)
if err != nil {
t.Fatal("Unexpected error while retrieiving PrincipalID: err:", err)
}
want := "pinheads.rok"
if got != want {
t.Fatal("Retrieved group was not as expected. Want:", want, "Got:", got)
}
}
func TestGenericGroup_withEmail(t *testing.T) {
t.Parallel()
response := struct {
Email string `json:"email"`
}{
"martymcfly@pinheads.rok",
}
mockAPI := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
rw.WriteHeader(http.StatusNotFound)
return
}
enc := json.NewEncoder(rw)
rw.WriteHeader(http.StatusOK)
_ = enc.Encode(response)
}))
defer mockAPI.Close()
logger := clog.New(clog.ParseLevel("debug"))
prov := oauth2.Generic{
Logger: logger,
APIURL: mockAPI.URL,
APIKey: "email",
}
tt, err := oauth2.NewTestTripper(logger, mockAPI, http.DefaultTransport)
if err != nil {
t.Fatal("Error initializing TestTripper: err:", err)
}
tc := &http.Client{
Transport: tt,
}
got, err := prov.Group(tc)
if err != nil {
t.Fatal("Unexpected error while retrieiving PrincipalID: err:", err)
}
want := "pinheads.rok"
if got != want {
t.Fatal("Retrieved group was not as expected. Want:", want, "Got:", got)
}
}
func TestGenericPrincipalID(t *testing.T) {
t.Parallel()

View File

@ -2,6 +2,7 @@ package oauth2
import (
"encoding/json"
"fmt"
"net/http"
"github.com/influxdata/chronograf"
@ -61,7 +62,19 @@ func (h *Heroku) PrincipalID(provider *http.Client) (string, error) {
DefaultOrganization DefaultOrg `json:"default_organization"`
}
resp, err := provider.Get(HerokuAccountRoute)
req, err := http.NewRequest("GET", HerokuAccountRoute, nil)
// Requests fail to Heroku unless this Accept header is set.
req.Header.Set("Accept", "application/vnd.heroku+json; version=3")
resp, err := provider.Do(req)
if resp.StatusCode/100 != 2 {
err := fmt.Errorf(
"Unable to GET user data from %s. Status: %s",
HerokuAccountRoute,
resp.Status,
)
h.Logger.Error("", err)
return "", err
}
if err != nil {
h.Logger.Error("Unable to communicate with Heroku. err:", err)
return "", err

View File

@ -118,13 +118,13 @@ func (j *AuthMux) Callback() http.Handler {
// if we received an extra id_token, inspect it
var id string
group := "DEFAULT"
if token.Extra("id_token") != "" {
log.Debug("token provides extra id_token")
var group string
if token.Extra("id_token") != nil && token.Extra("id_token") != "" {
log.Debug("token contains extra id_token")
if provider, ok := j.Provider.(ExtendedProvider); ok {
log.Debug("provider implements PrincipalIDFromClaims()")
var tokenString string
if tokenString, ok = token.Extra("id_token").(string); !ok {
tokenString, ok := token.Extra("id_token").(string)
if !ok {
log.Error("cannot cast id_token as string")
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
@ -136,12 +136,14 @@ func (j *AuthMux) Callback() http.Handler {
return
}
log.Debug("found claims: ", claims)
if id, err = provider.PrincipalIDFromClaims(claims); err != nil {
id, err = provider.PrincipalIDFromClaims(claims)
if err != nil {
log.Error("requested claim not found in id_token:", err)
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
}
if group, err = provider.GroupFromClaims(claims); err != nil {
group, err = provider.GroupFromClaims(claims)
if err != nil {
log.Error("requested claim not found in id_token:", err)
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
@ -149,19 +151,17 @@ func (j *AuthMux) Callback() http.Handler {
} else {
log.Debug("provider does not implement PrincipalIDFromClaims()")
}
}
// otherwise perform an additional lookup
if id == "" {
} else {
// otherwise perform an additional lookup
oauthClient := conf.Client(r.Context(), token)
// Using the token get the principal identifier from the provider
oauthClient := conf.Client(r.Context(), token)
id, err = j.Provider.PrincipalID(oauthClient)
id, err = j.Provider.PrincipalID(oauthClient)
if err != nil {
log.Error("Unable to get principal identifier ", err.Error())
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
}
group, err = j.Provider.Group(oauthClient)
group, err = j.Provider.Group(oauthClient)
if err != nil {
log.Error("Unable to get OAuth Group", err.Error())
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
@ -169,13 +169,6 @@ func (j *AuthMux) Callback() http.Handler {
}
}
group, err := j.Provider.Group(oauthClient)
if err != nil {
log.Error("Unable to get OAuth Group", err.Error())
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
}
p := Principal{
Subject: id,
Issuer: j.Provider.Name(),

View File

@ -1,6 +1,7 @@
package oauth2
import (
"encoding/json"
"net/http"
"net/http/cookiejar"
"net/http/httptest"
@ -13,19 +14,24 @@ import (
var testTime = time.Date(1985, time.October, 25, 18, 0, 0, 0, time.UTC)
type mockCallbackResponse struct {
AccessToken string `json:"access_token"`
}
// setupMuxTest produces an http.Client and an httptest.Server configured to
// use a particular http.Handler selected from a AuthMux. As this selection is
// done during the setup process, this configuration is performed by providing
// a function, and returning the desired handler. Cleanup is still the
// responsibility of the test writer, so the httptest.Server's Close() method
// should be deferred.
func setupMuxTest(selector func(*AuthMux) http.Handler, body string) (*http.Client, *httptest.Server, *httptest.Server) {
func setupMuxTest(response interface{}, selector func(*AuthMux) http.Handler) (*http.Client, *httptest.Server, *httptest.Server) {
provider := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if body != "" {
rw.Header().Set("Content-Type", "application/json")
}
rw.Header().Set("content-type", "application/json")
rw.WriteHeader(http.StatusOK)
rw.Write([]byte(body))
body, _ := json.Marshal(response)
rw.Write(body)
}))
now := func() time.Time {
@ -67,9 +73,11 @@ func teardownMuxTest(hc *http.Client, backend *httptest.Server, provider *httpte
func Test_AuthMux_Logout_DeletesSessionCookie(t *testing.T) {
t.Parallel()
hc, ts, prov := setupMuxTest(func(j *AuthMux) http.Handler {
var response interface{}
hc, ts, prov := setupMuxTest(response, func(j *AuthMux) http.Handler {
return j.Logout()
}, "")
})
defer teardownMuxTest(hc, ts, prov)
tsURL, _ := url.Parse(ts.URL)
@ -104,9 +112,11 @@ func Test_AuthMux_Logout_DeletesSessionCookie(t *testing.T) {
func Test_AuthMux_Login_RedirectsToCorrectURL(t *testing.T) {
t.Parallel()
hc, ts, prov := setupMuxTest(func(j *AuthMux) http.Handler {
var response interface{}
hc, ts, prov := setupMuxTest(response, func(j *AuthMux) http.Handler {
return j.Login() // Use Login handler for httptest server.
}, "")
})
defer teardownMuxTest(hc, ts, prov)
resp, err := hc.Get(ts.URL)
@ -130,9 +140,10 @@ func Test_AuthMux_Login_RedirectsToCorrectURL(t *testing.T) {
}
func Test_AuthMux_Callback_SetsCookie(t *testing.T) {
hc, ts, prov := setupMuxTest(func(j *AuthMux) http.Handler {
response := mockCallbackResponse{AccessToken: "123"}
hc, ts, prov := setupMuxTest(response, func(j *AuthMux) http.Handler {
return j.Callback()
}, "")
})
defer teardownMuxTest(hc, ts, prov)
tsURL, _ := url.Parse(ts.URL)
@ -169,10 +180,10 @@ func Test_AuthMux_Callback_SetsCookie(t *testing.T) {
func Test_AuthMux_Callback_HandlesIdToken(t *testing.T) {
// body taken from ADFS4
body := `{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IllEQlVocWRXa3NLWGRHdVgwc3l0amFVdXhoQSJ9.eyJhdWQiOiJ1cm46bWljcm9zb2Z0OnVzZXJpbmZvIiwiaXNzIjoiaHR0cDovL2RzdGNpbWFhZDFwLmRzdC1pdHMuZGUvYWRmcy9zZXJ2aWNlcy90cnVzdCIsImlhdCI6MTUxNTcwMDU2NSwiZXhwIjoxNTE1NzA0MTY1LCJhcHB0eXBlIjoiQ29uZmlkZW50aWFsIiwiYXBwaWQiOiJjaHJvbm9ncmFmIiwiYXV0aG1ldGhvZCI6InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkUHJvdGVjdGVkVHJhbnNwb3J0IiwiYXV0aF90aW1lIjoiMjAxOC0wMS0xMVQxOTo1MToyNS44MDZaIiwidmVyIjoiMS4wIiwic2NwIjoib3BlbmlkIiwic3ViIjoiZVlWM2pkbGROeUZZMXFGRkg0b0FkQnZERmZiVmZudUcyOUhpSGtTdWp3az0ifQ.sf1qJys9LMUp2S232IRK2aTXiPCE93O-cUdYQQz7kg2woyD46KLwwKIYJVqMaqLspTn3OmaIhKtgx5ZXyAEtihODB1GOBK7DBNRBYCS1iqY_v2-Qwjf7hgaNaCqBjs0DZJspfp5G9MTykvD1FOtQNjPOcBW-i2bblG9L9jlmMbOZ3F7wrZMrroTSkiSn_gRiw2SnN8K7w8WrMEXNK2_jg9ZJ7aSHeUSBwkRNFRds2QNho3HWHg-zcsZFdZ4UGSt-6Az_0LY3yENMLj5us5Rl6Qzk_Re2dhFrlnlXlY1v1DEp3icCvvjkv6AeZWjTfW4qETZaCXUKtSyZ7d5_V1CRDQ","token_type":"bearer","expires_in":3600,"resource":"urn:microsoft:userinfo","refresh_token":"X9ZGO4H1bMk2bFeOfpv18BzAuFBzUPKQNfOEfdp60FkAAQAALPEBfj23FPEzajle-hm4DrDXp8-Kj53OqoVGalyZeuR-lfJzxpQXQhRAXZOUTuuQ8AQycByh9AylQYDA0jdMFW4FL4WL_6JhNh2JrtXCv2HQ9ozbUq9F7u_O0cY7u0P2pfNujQfk3ckYn-CMVjXbuwJTve6bXUR0JDp5c195bAVA5eFWyI-2uh432t7viyaIjAVbWxQF4fvimcpF1Et9cGodZHVsrZzGxKRnzwjYkWHsqm9go4KOeSKN6MlcWbjvS1UdMjQXSvoqSI00JnSMC3hxJZFn5JcmAPB1AMnJf4VvXZ5b-aOnwdX09YT8KayWkWekAsuZqTAsFwhZPVCRGWAFAADy0e2fTe6l-U6Cj_2bWsq6Snm1QEpWHXuwOJKWZJH-9yQn8KK3KzRowSzRuACzEIpZS5skrqXs_-2aOaZibNpjCEVyw8fF8GTw3VRLufsSrMQ5pD0KL7TppTGFpaqgwIH1yq6T8aRY4DeyoJkNpnO9cw1wuqnY7oGF-J25sfZ4XNWhk6o5e9A45PXhTilClyDKDLqTfdoIsG1Koc2ywqTIb-XI_EbWR3e4ijy8Kmlehw1kU9_xAG0MmmD2HTyGHZCBRgrskYCcHd-UNgCMrNAb5dZQ8NwpKtEL46qIq4R0lheTRRK8sOWzzuJXmvDEoJiIxqSR3Ma4MOISi-vsIsAuiEL9G1aMOkDRj-kDVmqrdKRAwYnN78AWY5EFfkQJyVBbiG882wBh9S0q3HUUCxzFerOvl4eDlVn6m18rRMz7CVZYBBltGtHRhEOQ4gumICR5JRrXAC50aBmUlhDiiMdbEIwJrvWrkhKE0oAJznqC7gleP0E4EOEh9r6CEGZ7Oj8X9Cdzjbuq2G1JGBm_yUvkhAcV61DjOiIQl35BpOfshveNZf_caUtNMa2i07BBmezve17-2kWGzRunr1BD1vMTz41z-H62fy4McR47WJjdDJnuy4DH5AZYQ6ooVxWCtEqeqRPYpzO0XdOdJGXFqXs9JzDKVXTgnHU443hZBC5H-BJkZDuuJ_ZWNKXf03JhouWkxXcdaMbuaQYOZJsUySVyJ5X4usrBFjW4udZAzy7mua-nJncbvcwoyVXiFlRfZiySXolQ9865N7XUnEk_2PijMLoVDATDbA09XuRySvngNsdsQ27p21dPxChXdtpD5ofNqKJ2FBzFKmxCkuX7L01N1nDpWQTuxhHF0JfxSKG5m3jcTx8Bd7Un94mTuAB7RuglDqkdQB9o4X9NHNGSdqGQaK-xeKoNCFWevk3VZoDoY9w2NqSNV2VIuqhy7SxtDSMjZKC5kiQi5EfGeTYZAvTwMYwaXb7K4WWtscy_ZE15EOCVeYi0hM1Ma8iFFTANkSRyX83Ju4SRphxRKnpKcJ2pPYH784I5HOm5sclhUL3aLeAA161QgxRBSa9YVIZfyXHyWQTcbNucNdhmdUZnKfRv1xtXcS9VAx2yAkoKFehZivEINX0Y500-WZ1eT_RXp0BfCKmJQ8Fu50oTaI-c5h2Q3Gp_LTSODNnMrjJiJxCLD_LD1fd1e8jTYDV3NroGlpWTuTdjMUm-Z1SMXaaJzQGEnNT6F8b6un9228L6YrDC_3MJ5J80VAHL5EO1GesdEWblugCL7AQDtFjNXq0lK8Aoo8X9_hlvDwgfdR16l8QALPT1HJVzlHPG8G3dRe50TKZnl3obU0WXN1KYG1EC4Qa3LyaVCIuGJYOeFqjMINrf7PoM368nS9yhrY08nnoHZbQ7IeA1KsNq2kANeH1doCNfWrXDwn8KxjYxZPEnzvlQ5M1RIzArOqzWL8NbftW1q2yCZZ4RVg0vOTVXsqWFnQIvWK-mkELa7bvByFzbtVHOJpc_2EKBKBNv6IYUENRCu2TOf6w7u42yvng7ccoXRTiUFUlKgVmswf9FzISxFd-YKgrzp3bMhC3gReGqcJuqEwnXPvOAY_BAkVMSd_ZaCFuyclRjFvUxrAg1T_cqOvRIlJ2Qq7z4u7W3BAo9BtFdj8QNLKJXtvvzXTprglRPDNP_QEPAkwZ_Uxa13vdYFcG18WCx4GbWQXchl5B7DnISobcdCH34M-I0xDZN98VWQVmLAfPniDUD30C8pfiYF7tW_EVy958Eg_JWVy0SstYEhV-y-adrJ1Oimjv0ptsWv-yErKBUD14aex9A_QqdnTXZUg.tqMb72eWAkAIvInuLp57NDyGxfYvms3NnhN-mllkYb7Xpd8gVbQFc2mYdzOOhtnfGuakyXYF4rZdJonQwzBO6C9KYuARciUU1Ms4bWPC-aeNO5t-aO_bDZbwC9qMPmq5ZuxG633BARGaw26fr0Z7qhcJMiou_EuaIehYTKkPB-mxtRAhxxyX91qqe0-PJnCHWoxizC4hDCUwp9Jb54tNf34BG3vtkXFX-kUARNfGucgKUkh6RYkhWiMBsMVoyWmkFXB5fYxmCAH5c5wDW6srKdyIDEWZInliuKbYR0p66vg1FfoSi4bBfrsm5NtCtLKG9V6Q0FEIA6tRRgHmKUGpkw","refresh_token_expires_in":28519,"scope":"openid","id_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IllEQlVocWRXa3NLWGRHdVgwc3l0amFVdXhoQSIsImtpZCI6IllEQlVocWRXa3NLWGRHdVgwc3l0amFVdXhoQSJ9.eyJhdWQiOiJjaHJvbm9ncmFmIiwiaXNzIjoiaHR0cHM6Ly9kc3RjaW1hYWQxcC5kc3QtaXRzLmRlL2FkZnMiLCJpYXQiOjE1MTU3MDA1NjUsImV4cCI6MTUxNTcwNDE2NSwiYXV0aF90aW1lIjoxNTE1NzAwMjg1LCJzdWIiOiJlWVYzamRsZE55RlkxcUZGSDRvQWRCdkRGZmJWZm51RzI5SGlIa1N1andrPSIsInVwbiI6ImJzY0Bkc3QtaXRzLmRlIiwidW5pcXVlX25hbWUiOiJEU1RcXGJzYyIsInNpZCI6IlMtMS01LTIxLTI1MDUxNTEzOTgtMjY2MTAyODEwOS0zNzU0MjY1ODIwLTExMDQifQ.XD873K6NVRTJY1700NsflLJGZKFHJfNBjB81SlADVdAHbhnq7wkAZbGEEm8wFqvTKKysUl9EALzmDa2tR9nzohVvmHftIYBO0E-wPBzdzWWX0coEgpVAc-SysP-eIQWLsj8EaodaMkCgKO0FbTWOf4GaGIBZGklrr9EEk8VRSdbXbm6Sv9WVphezEzxq6JJBRBlCVibCnZjR5OYh1Vw_7E7P38ESPbpLY3hYYl2hz4y6dQJqCwGr7YP8KrDlYtbosZYgT7ayxokEJI1udEbX5PbAq5G6mj5rLfSOl85rMg-psZiivoM8dn9lEl2P7oT8rAvMWvQp-FIRQQHwqf9cxw"}`
hc, ts, prov := setupMuxTest(func(j *AuthMux) http.Handler {
response := mockCallbackResponse{AccessToken: `eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IllEQlVocWRXa3NLWGRHdVgwc3l0amFVdXhoQSJ9.eyJhdWQiOiJ1cm46bWljcm9zb2Z0OnVzZXJpbmZvIiwiaXNzIjoiaHR0cDovL2RzdGNpbWFhZDFwLmRzdC1pdHMuZGUvYWRmcy9zZXJ2aWNlcy90cnVzdCIsImlhdCI6MTUxNTcwMDU2NSwiZXhwIjoxNTE1NzA0MTY1LCJhcHB0eXBlIjoiQ29uZmlkZW50aWFsIiwiYXBwaWQiOiJjaHJvbm9ncmFmIiwiYXV0aG1ldGhvZCI6InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkUHJvdGVjdGVkVHJhbnNwb3J0IiwiYXV0aF90aW1lIjoiMjAxOC0wMS0xMVQxOTo1MToyNS44MDZaIiwidmVyIjoiMS4wIiwic2NwIjoib3BlbmlkIiwic3ViIjoiZVlWM2pkbGROeUZZMXFGRkg0b0FkQnZERmZiVmZudUcyOUhpSGtTdWp3az0ifQ.sf1qJys9LMUp2S232IRK2aTXiPCE93O-cUdYQQz7kg2woyD46KLwwKIYJVqMaqLspTn3OmaIhKtgx5ZXyAEtihODB1GOBK7DBNRBYCS1iqY_v2-Qwjf7hgaNaCqBjs0DZJspfp5G9MTykvD1FOtQNjPOcBW-i2bblG9L9jlmMbOZ3F7wrZMrroTSkiSn_gRiw2SnN8K7w8WrMEXNK2_jg9ZJ7aSHeUSBwkRNFRds2QNho3HWHg-zcsZFdZ4UGSt-6Az_0LY3yENMLj5us5Rl6Qzk_Re2dhFrlnlXlY1v1DEp3icCvvjkv6AeZWjTfW4qETZaCXUKtSyZ7d5_V1CRDQ", "token_type": "bearer", "expires_in": 3600, "resource": "urn:microsoft:userinfo", "refresh_token": "X9ZGO4H1bMk2bFeOfpv18BzAuFBzUPKQNfOEfdp60FkAAQAALPEBfj23FPEzajle-hm4DrDXp8-Kj53OqoVGalyZeuR-lfJzxpQXQhRAXZOUTuuQ8AQycByh9AylQYDA0jdMFW4FL4WL_6JhNh2JrtXCv2HQ9ozbUq9F7u_O0cY7u0P2pfNujQfk3ckYn-CMVjXbuwJTve6bXUR0JDp5c195bAVA5eFWyI-2uh432t7viyaIjAVbWxQF4fvimcpF1Et9cGodZHVsrZzGxKRnzwjYkWHsqm9go4KOeSKN6MlcWbjvS1UdMjQXSvoqSI00JnSMC3hxJZFn5JcmAPB1AMnJf4VvXZ5b-aOnwdX09YT8KayWkWekAsuZqTAsFwhZPVCRGWAFAADy0e2fTe6l-U6Cj_2bWsq6Snm1QEpWHXuwOJKWZJH-9yQn8KK3KzRowSzRuACzEIpZS5skrqXs_-2aOaZibNpjCEVyw8fF8GTw3VRLufsSrMQ5pD0KL7TppTGFpaqgwIH1yq6T8aRY4DeyoJkNpnO9cw1wuqnY7oGF-J25sfZ4XNWhk6o5e9A45PXhTilClyDKDLqTfdoIsG1Koc2ywqTIb-XI_EbWR3e4ijy8Kmlehw1kU9_xAG0MmmD2HTyGHZCBRgrskYCcHd-UNgCMrNAb5dZQ8NwpKtEL46qIq4R0lheTRRK8sOWzzuJXmvDEoJiIxqSR3Ma4MOISi-vsIsAuiEL9G1aMOkDRj-kDVmqrdKRAwYnN78AWY5EFfkQJyVBbiG882wBh9S0q3HUUCxzFerOvl4eDlVn6m18rRMz7CVZYBBltGtHRhEOQ4gumICR5JRrXAC50aBmUlhDiiMdbEIwJrvWrkhKE0oAJznqC7gleP0E4EOEh9r6CEGZ7Oj8X9Cdzjbuq2G1JGBm_yUvkhAcV61DjOiIQl35BpOfshveNZf_caUtNMa2i07BBmezve17-2kWGzRunr1BD1vMTz41z-H62fy4McR47WJjdDJnuy4DH5AZYQ6ooVxWCtEqeqRPYpzO0XdOdJGXFqXs9JzDKVXTgnHU443hZBC5H-BJkZDuuJ_ZWNKXf03JhouWkxXcdaMbuaQYOZJsUySVyJ5X4usrBFjW4udZAzy7mua-nJncbvcwoyVXiFlRfZiySXolQ9865N7XUnEk_2PijMLoVDATDbA09XuRySvngNsdsQ27p21dPxChXdtpD5ofNqKJ2FBzFKmxCkuX7L01N1nDpWQTuxhHF0JfxSKG5m3jcTx8Bd7Un94mTuAB7RuglDqkdQB9o4X9NHNGSdqGQaK-xeKoNCFWevk3VZoDoY9w2NqSNV2VIuqhy7SxtDSMjZKC5kiQi5EfGeTYZAvTwMYwaXb7K4WWtscy_ZE15EOCVeYi0hM1Ma8iFFTANkSRyX83Ju4SRphxRKnpKcJ2pPYH784I5HOm5sclhUL3aLeAA161QgxRBSa9YVIZfyXHyWQTcbNucNdhmdUZnKfRv1xtXcS9VAx2yAkoKFehZivEINX0Y500-WZ1eT_RXp0BfCKmJQ8Fu50oTaI-c5h2Q3Gp_LTSODNnMrjJiJxCLD_LD1fd1e8jTYDV3NroGlpWTuTdjMUm-Z1SMXaaJzQGEnNT6F8b6un9228L6YrDC_3MJ5J80VAHL5EO1GesdEWblugCL7AQDtFjNXq0lK8Aoo8X9_hlvDwgfdR16l8QALPT1HJVzlHPG8G3dRe50TKZnl3obU0WXN1KYG1EC4Qa3LyaVCIuGJYOeFqjMINrf7PoM368nS9yhrY08nnoHZbQ7IeA1KsNq2kANeH1doCNfWrXDwn8KxjYxZPEnzvlQ5M1RIzArOqzWL8NbftW1q2yCZZ4RVg0vOTVXsqWFnQIvWK-mkELa7bvByFzbtVHOJpc_2EKBKBNv6IYUENRCu2TOf6w7u42yvng7ccoXRTiUFUlKgVmswf9FzISxFd-YKgrzp3bMhC3gReGqcJuqEwnXPvOAY_BAkVMSd_ZaCFuyclRjFvUxrAg1T_cqOvRIlJ2Qq7z4u7W3BAo9BtFdj8QNLKJXtvvzXTprglRPDNP_QEPAkwZ_Uxa13vdYFcG18WCx4GbWQXchl5B7DnISobcdCH34M-I0xDZN98VWQVmLAfPniDUD30C8pfiYF7tW_EVy958Eg_JWVy0SstYEhV-y-adrJ1Oimjv0ptsWv-yErKBUD14aex9A_QqdnTXZUg.tqMb72eWAkAIvInuLp57NDyGxfYvms3NnhN-mllkYb7Xpd8gVbQFc2mYdzOOhtnfGuakyXYF4rZdJonQwzBO6C9KYuARciUU1Ms4bWPC-aeNO5t-aO_bDZbwC9qMPmq5ZuxG633BARGaw26fr0Z7qhcJMiou_EuaIehYTKkPB-mxtRAhxxyX91qqe0-PJnCHWoxizC4hDCUwp9Jb54tNf34BG3vtkXFX-kUARNfGucgKUkh6RYkhWiMBsMVoyWmkFXB5fYxmCAH5c5wDW6srKdyIDEWZInliuKbYR0p66vg1FfoSi4bBfrsm5NtCtLKG9V6Q0FEIA6tRRgHmKUGpkw", "refresh_token_expires_in": 28519, "scope": "openid", "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IllEQlVocWRXa3NLWGRHdVgwc3l0amFVdXhoQSIsImtpZCI6IllEQlVocWRXa3NLWGRHdVgwc3l0amFVdXhoQSJ9.eyJhdWQiOiJjaHJvbm9ncmFmIiwiaXNzIjoiaHR0cHM6Ly9kc3RjaW1hYWQxcC5kc3QtaXRzLmRlL2FkZnMiLCJpYXQiOjE1MTU3MDA1NjUsImV4cCI6MTUxNTcwNDE2NSwiYXV0aF90aW1lIjoxNTE1NzAwMjg1LCJzdWIiOiJlWVYzamRsZE55RlkxcUZGSDRvQWRCdkRGZmJWZm51RzI5SGlIa1N1andrPSIsInVwbiI6ImJzY0Bkc3QtaXRzLmRlIiwidW5pcXVlX25hbWUiOiJEU1RcXGJzYyIsInNpZCI6IlMtMS01LTIxLTI1MDUxNTEzOTgtMjY2MTAyODEwOS0zNzU0MjY1ODIwLTExMDQifQ.XD873K6NVRTJY1700NsflLJGZKFHJfNBjB81SlADVdAHbhnq7wkAZbGEEm8wFqvTKKysUl9EALzmDa2tR9nzohVvmHftIYBO0E-wPBzdzWWX0coEgpVAc-SysP-eIQWLsj8EaodaMkCgKO0FbTWOf4GaGIBZGklrr9EEk8VRSdbXbm6Sv9WVphezEzxq6JJBRBlCVibCnZjR5OYh1Vw_7E7P38ESPbpLY3hYYl2hz4y6dQJqCwGr7YP8KrDlYtbosZYgT7ayxokEJI1udEbX5PbAq5G6mj5rLfSOl85rMg-psZiivoM8dn9lEl2P7oT8rAvMWvQp-FIRQQHwqf9cxw`}
hc, ts, prov := setupMuxTest(response, func(j *AuthMux) http.Handler {
return j.Callback()
}, body)
})
defer teardownMuxTest(hc, ts, prov)
tsURL, _ := url.Parse(ts.URL)

452
server/annotations.go Normal file
View File

@ -0,0 +1,452 @@
package server
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/influx"
)
const (
since = "since"
until = "until"
timeMilliFormat = "2006-01-02T15:04:05.999Z07:00"
)
type annotationLinks struct {
Self string `json:"self"` // Self link mapping to this resource
}
type annotationResponse struct {
ID string `json:"id"` // ID is the unique annotation identifier
StartTime string `json:"startTime"` // StartTime in RFC3339 of the start of the annotation
EndTime string `json:"endTime"` // EndTime in RFC3339 of the end of the annotation
Text string `json:"text"` // Text is the associated user-facing text describing the annotation
Type string `json:"type"` // Type describes the kind of annotation
Links annotationLinks `json:"links"`
}
func newAnnotationResponse(src chronograf.Source, a *chronograf.Annotation) annotationResponse {
base := "/chronograf/v1/sources"
res := annotationResponse{
ID: a.ID,
StartTime: a.StartTime.UTC().Format(timeMilliFormat),
EndTime: a.EndTime.UTC().Format(timeMilliFormat),
Text: a.Text,
Type: a.Type,
Links: annotationLinks{
Self: fmt.Sprintf("%s/%d/annotations/%s", base, src.ID, a.ID),
},
}
if a.EndTime.IsZero() {
res.EndTime = ""
}
return res
}
type annotationsResponse struct {
Annotations []annotationResponse `json:"annotations"`
}
func newAnnotationsResponse(src chronograf.Source, as []chronograf.Annotation) annotationsResponse {
annotations := make([]annotationResponse, len(as))
for i, a := range as {
annotations[i] = newAnnotationResponse(src, &a)
}
return annotationsResponse{
Annotations: annotations,
}
}
func validAnnotationQuery(query url.Values) (startTime, stopTime time.Time, err error) {
start := query.Get(since)
if start == "" {
return time.Time{}, time.Time{}, fmt.Errorf("since parameter is required")
}
startTime, err = time.Parse(timeMilliFormat, start)
if err != nil {
return
}
// if until isn't stated, the default time is now
stopTime = time.Now()
stop := query.Get(until)
if stop != "" {
stopTime, err = time.Parse(timeMilliFormat, stop)
if err != nil {
return time.Time{}, time.Time{}, err
}
}
if startTime.After(stopTime) {
startTime, stopTime = stopTime, startTime
}
return startTime, stopTime, nil
}
// Annotations returns all annotations within the annotations store
func (s *Service) Annotations(w http.ResponseWriter, r *http.Request) {
id, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
start, stop, err := validAnnotationQuery(r.URL.Query())
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
ctx := r.Context()
src, err := s.Store.Sources(ctx).Get(ctx, id)
if err != nil {
notFound(w, id, s.Logger)
return
}
ts, err := s.TimeSeries(src)
if err != nil {
msg := fmt.Sprintf("Unable to connect to source %d: %v", id, err)
Error(w, http.StatusBadRequest, msg, s.Logger)
return
}
if err = ts.Connect(ctx, &src); err != nil {
msg := fmt.Sprintf("Unable to connect to source %d: %v", id, err)
Error(w, http.StatusBadRequest, msg, s.Logger)
return
}
store := influx.NewAnnotationStore(ts)
annotations, err := store.All(ctx, start, stop)
if err != nil {
msg := fmt.Errorf("Error loading annotations: %v", err)
unknownErrorWithMessage(w, msg, s.Logger)
return
}
res := newAnnotationsResponse(src, annotations)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// Annotation returns a specified annotation id within the annotations store
func (s *Service) Annotation(w http.ResponseWriter, r *http.Request) {
id, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
annoID, err := paramStr("aid", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
ctx := r.Context()
src, err := s.Store.Sources(ctx).Get(ctx, id)
if err != nil {
notFound(w, id, s.Logger)
return
}
ts, err := s.TimeSeries(src)
if err != nil {
msg := fmt.Sprintf("Unable to connect to source %d: %v", id, err)
Error(w, http.StatusBadRequest, msg, s.Logger)
return
}
if err = ts.Connect(ctx, &src); err != nil {
msg := fmt.Sprintf("Unable to connect to source %d: %v", id, err)
Error(w, http.StatusBadRequest, msg, s.Logger)
return
}
store := influx.NewAnnotationStore(ts)
anno, err := store.Get(ctx, annoID)
if err != nil {
if err != chronograf.ErrAnnotationNotFound {
msg := fmt.Errorf("Error loading annotation: %v", err)
unknownErrorWithMessage(w, msg, s.Logger)
return
}
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
res := newAnnotationResponse(src, anno)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
type newAnnotationRequest struct {
StartTime time.Time
EndTime time.Time
Text string `json:"text,omitempty"` // Text is the associated user-facing text describing the annotation
Type string `json:"type,omitempty"` // Type describes the kind of annotation
}
func (ar *newAnnotationRequest) UnmarshalJSON(data []byte) error {
type Alias newAnnotationRequest
aux := &struct {
StartTime string `json:"startTime"` // StartTime is the time in rfc3339 milliseconds
EndTime string `json:"endTime"` // EndTime is the time in rfc3339 milliseconds
*Alias
}{
Alias: (*Alias)(ar),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
var err error
ar.StartTime, err = time.Parse(timeMilliFormat, aux.StartTime)
if err != nil {
return err
}
ar.EndTime, err = time.Parse(timeMilliFormat, aux.EndTime)
if err != nil {
return err
}
if ar.StartTime.After(ar.EndTime) {
ar.StartTime, ar.EndTime = ar.EndTime, ar.StartTime
}
return nil
}
func (ar *newAnnotationRequest) Annotation() *chronograf.Annotation {
return &chronograf.Annotation{
StartTime: ar.StartTime,
EndTime: ar.EndTime,
Text: ar.Text,
Type: ar.Type,
}
}
// NewAnnotation adds the annotation from a POST body to the annotations store
func (s *Service) NewAnnotation(w http.ResponseWriter, r *http.Request) {
id, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
ctx := r.Context()
src, err := s.Store.Sources(ctx).Get(ctx, id)
if err != nil {
notFound(w, id, s.Logger)
return
}
ts, err := s.TimeSeries(src)
if err != nil {
msg := fmt.Sprintf("Unable to connect to source %d: %v", id, err)
Error(w, http.StatusBadRequest, msg, s.Logger)
return
}
if err = ts.Connect(ctx, &src); err != nil {
msg := fmt.Sprintf("Unable to connect to source %d: %v", id, err)
Error(w, http.StatusBadRequest, msg, s.Logger)
return
}
var req newAnnotationRequest
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, s.Logger)
return
}
store := influx.NewAnnotationStore(ts)
anno, err := store.Add(ctx, req.Annotation())
if err != nil {
if err == chronograf.ErrUpstreamTimeout {
msg := "Timeout waiting for response"
Error(w, http.StatusRequestTimeout, msg, s.Logger)
return
}
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
res := newAnnotationResponse(src, anno)
location(w, res.Links.Self)
encodeJSON(w, http.StatusCreated, res, s.Logger)
}
// RemoveAnnotation removes the annotation from the time series source
func (s *Service) RemoveAnnotation(w http.ResponseWriter, r *http.Request) {
id, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
annoID, err := paramStr("aid", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
ctx := r.Context()
src, err := s.Store.Sources(ctx).Get(ctx, id)
if err != nil {
notFound(w, id, s.Logger)
return
}
ts, err := s.TimeSeries(src)
if err != nil {
msg := fmt.Sprintf("Unable to connect to source %d: %v", id, err)
Error(w, http.StatusBadRequest, msg, s.Logger)
return
}
if err = ts.Connect(ctx, &src); err != nil {
msg := fmt.Sprintf("Unable to connect to source %d: %v", id, err)
Error(w, http.StatusBadRequest, msg, s.Logger)
return
}
store := influx.NewAnnotationStore(ts)
if err = store.Delete(ctx, annoID); err != nil {
if err == chronograf.ErrUpstreamTimeout {
msg := "Timeout waiting for response"
Error(w, http.StatusRequestTimeout, msg, s.Logger)
return
}
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
w.WriteHeader(http.StatusNoContent)
}
type updateAnnotationRequest struct {
StartTime *time.Time `json:"startTime,omitempty"` // StartTime is the time in rfc3339 milliseconds
EndTime *time.Time `json:"endTime,omitempty"` // EndTime is the time in rfc3339 milliseconds
Text *string `json:"text,omitempty"` // Text is the associated user-facing text describing the annotation
Type *string `json:"type,omitempty"` // Type describes the kind of annotation
}
// TODO: make sure that endtime is after starttime
func (u *updateAnnotationRequest) UnmarshalJSON(data []byte) error {
type Alias updateAnnotationRequest
aux := &struct {
StartTime *string `json:"startTime,omitempty"`
EndTime *string `json:"endTime,omitempty"`
*Alias
}{
Alias: (*Alias)(u),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
if aux.StartTime != nil {
tm, err := time.Parse(timeMilliFormat, *aux.StartTime)
if err != nil {
return err
}
u.StartTime = &tm
}
if aux.EndTime != nil {
tm, err := time.Parse(timeMilliFormat, *aux.EndTime)
if err != nil {
return err
}
u.EndTime = &tm
}
// Update must have at least one field set
if u.StartTime == nil && u.EndTime == nil && u.Text == nil && u.Type == nil {
return fmt.Errorf("update request must have at least one field")
}
return nil
}
// UpdateAnnotation overwrite an existing annotation
func (s *Service) UpdateAnnotation(w http.ResponseWriter, r *http.Request) {
id, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
annoID, err := paramStr("aid", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
ctx := r.Context()
src, err := s.Store.Sources(ctx).Get(ctx, id)
if err != nil {
notFound(w, id, s.Logger)
return
}
ts, err := s.TimeSeries(src)
if err != nil {
msg := fmt.Sprintf("Unable to connect to source %d: %v", id, err)
Error(w, http.StatusBadRequest, msg, s.Logger)
return
}
if err = ts.Connect(ctx, &src); err != nil {
msg := fmt.Sprintf("Unable to connect to source %d: %v", id, err)
Error(w, http.StatusBadRequest, msg, s.Logger)
return
}
store := influx.NewAnnotationStore(ts)
cur, err := store.Get(ctx, annoID)
if err != nil {
notFound(w, annoID, s.Logger)
return
}
var req updateAnnotationRequest
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, s.Logger)
return
}
if req.StartTime != nil {
cur.StartTime = *req.StartTime
}
if req.EndTime != nil {
cur.EndTime = *req.EndTime
}
if req.Text != nil {
cur.Text = *req.Text
}
if req.Type != nil {
cur.Type = *req.Type
}
if err = store.Update(ctx, cur); err != nil {
if err == chronograf.ErrUpstreamTimeout {
msg := "Timeout waiting for response"
Error(w, http.StatusRequestTimeout, msg, s.Logger)
return
}
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
res := newAnnotationResponse(src, cur)
location(w, res.Links.Self)
encodeJSON(w, http.StatusOK, res, s.Logger)
}

191
server/annotations_test.go Normal file
View File

@ -0,0 +1,191 @@
package server
import (
"bytes"
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/bouk/httprouter"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/mocks"
)
func TestService_Annotations(t *testing.T) {
type fields struct {
Store DataStore
TimeSeriesClient TimeSeriesClient
}
tests := []struct {
name string
fields fields
w *httptest.ResponseRecorder
r *http.Request
ID string
want string
}{
{
name: "error no id",
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "/chronograf/v1/sources/1/annotations", bytes.NewReader([]byte(`howdy`))),
want: `{"code":422,"message":"Error converting ID "}`,
},
{
name: "no since parameter",
ID: "1",
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "/chronograf/v1/sources/1/annotations", bytes.NewReader([]byte(`howdy`))),
want: `{"code":422,"message":"since parameter is required"}`,
},
{
name: "invalid since parameter",
ID: "1",
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "/chronograf/v1/sources/1/annotations?since=howdy", bytes.NewReader([]byte(`howdy`))),
want: `{"code":422,"message":"parsing time \"howdy\" as \"2006-01-02T15:04:05.999Z07:00\": cannot parse \"howdy\" as \"2006\""}`,
},
{
name: "error is returned when get is an error",
fields: fields{
Store: &mocks.Store{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, ID int) (chronograf.Source, error) {
return chronograf.Source{}, fmt.Errorf("error")
},
},
},
},
ID: "1",
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "/chronograf/v1/sources/1/annotations?since=1985-04-12T23:20:50.52Z", bytes.NewReader([]byte(`howdy`))),
want: `{"code":404,"message":"ID 1 not found"}`,
},
{
name: "error is returned connect is an error",
fields: fields{
Store: &mocks.Store{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, ID int) (chronograf.Source, error) {
return chronograf.Source{
ID: ID,
}, nil
},
},
},
TimeSeriesClient: &mocks.TimeSeries{
ConnectF: func(context.Context, *chronograf.Source) error {
return fmt.Errorf("error)")
},
},
},
ID: "1",
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "/chronograf/v1/sources/1/annotations?since=1985-04-12T23:20:50.52Z", bytes.NewReader([]byte(`howdy`))),
want: `{"code":400,"message":"Unable to connect to source 1: error)"}`,
},
{
name: "error returned when annotations are invalid",
fields: fields{
Store: &mocks.Store{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, ID int) (chronograf.Source, error) {
return chronograf.Source{
ID: ID,
}, nil
},
},
},
TimeSeriesClient: &mocks.TimeSeries{
ConnectF: func(context.Context, *chronograf.Source) error {
return nil
},
QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
return mocks.NewResponse(`{[]}`, nil), nil
},
},
},
ID: "1",
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "/chronograf/v1/sources/1/annotations?since=1985-04-12T23:20:50.52Z", bytes.NewReader([]byte(`howdy`))),
want: `{"code":500,"message":"Unknown error: Error loading annotations: invalid character '[' looking for beginning of object key string"}`,
},
{
name: "error is returned connect is an error",
fields: fields{
Store: &mocks.Store{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, ID int) (chronograf.Source, error) {
return chronograf.Source{
ID: ID,
}, nil
},
},
},
TimeSeriesClient: &mocks.TimeSeries{
ConnectF: func(context.Context, *chronograf.Source) error {
return nil
},
QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
return mocks.NewResponse(`[
{
"series": [
{
"name": "annotations",
"columns": [
"time",
"start_time",
"modified_time_ns",
"text",
"type",
"id"
],
"values": [
[
1516920177345000000,
0,
1516989242129417403,
"mytext",
"mytype",
"ea0aa94b-969a-4cd5-912a-5db61d502268"
]
]
}
]
}
]`, nil), nil
},
},
},
ID: "1",
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "/chronograf/v1/sources/1/annotations?since=1985-04-12T23:20:50.52Z", bytes.NewReader([]byte(`howdy`))),
want: `{"annotations":[{"id":"ea0aa94b-969a-4cd5-912a-5db61d502268","startTime":"1970-01-01T00:00:00Z","endTime":"2018-01-25T22:42:57.345Z","text":"mytext","type":"mytype","links":{"self":"/chronograf/v1/sources/1/annotations/ea0aa94b-969a-4cd5-912a-5db61d502268"}}]}
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.r = tt.r.WithContext(httprouter.WithParams(
context.Background(),
httprouter.Params{
{
Key: "id",
Value: tt.ID,
},
}))
s := &Service{
Store: tt.fields.Store,
TimeSeriesClient: tt.fields.TimeSeriesClient,
Logger: mocks.NewLogger(),
}
s.Annotations(tt.w, tt.r)
got := tt.w.Body.String()
if got != tt.want {
t.Errorf("Annotations() got != want:\n%s\n%s", got, tt.want)
}
})
}
}

View File

@ -45,7 +45,7 @@ func newCellResponse(dID chronograf.DashboardID, cell chronograf.DashboardCell)
for _, lbl := range []string{"x", "y", "y2"} {
if _, found := newAxes[lbl]; !found {
newAxes[lbl] = chronograf.Axis{
Bounds: []string{},
Bounds: []string{"", ""},
}
}
}
@ -354,6 +354,13 @@ func (s *Service) ReplaceDashboardCell(w http.ResponseWriter, r *http.Request) {
return
}
for i, a := range cell.Axes {
if len(a.Bounds) == 0 {
a.Bounds = []string{"", ""}
cell.Axes[i] = a
}
}
if err := ValidDashboardCellRequest(&cell); err != nil {
invalidData(w, err, s.Logger)
return

View File

@ -192,13 +192,13 @@ func Test_Service_DashboardCells(t *testing.T) {
CellColors: []chronograf.CellColor{},
Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{
Bounds: []string{},
Bounds: []string{"", ""},
},
"y": chronograf.Axis{
Bounds: []string{},
Bounds: []string{"", ""},
},
"y2": chronograf.Axis{
Bounds: []string{},
Bounds: []string{"", ""},
},
},
},
@ -420,13 +420,13 @@ func TestService_ReplaceDashboardCell(t *testing.T) {
},
Axes: map[string]chronograf.Axis{
"x": {
Bounds: []string{},
Bounds: []string{"", ""},
},
"y": {
Bounds: []string{},
Bounds: []string{"", ""},
},
"y2": {
Bounds: []string{},
Bounds: []string{"", ""},
},
},
Type: "line",
@ -491,7 +491,7 @@ func TestService_ReplaceDashboardCell(t *testing.T) {
],
"axes": {
"x": {
"bounds": [],
"bounds": ["",""],
"label": "",
"prefix": "",
"suffix": "",
@ -499,7 +499,7 @@ func TestService_ReplaceDashboardCell(t *testing.T) {
"scale": ""
},
"y": {
"bounds": [],
"bounds": ["",""],
"label": "",
"prefix": "",
"suffix": "",
@ -507,7 +507,7 @@ func TestService_ReplaceDashboardCell(t *testing.T) {
"scale": ""
},
"y2": {
"bounds": [],
"bounds": ["",""],
"label": "",
"prefix": "",
"suffix": "",
@ -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"}],"legend":{},"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":{},"tableOptions":{"timeFormat":"","verticalTimeAxis":false,"sortBy":{"internalName":"","displayName":"","visible":false},"wrapping":"","fieldNames":null,"fixFirstColumn":false},"links":{"self":"/chronograf/v1/dashboards/1/cells/3c5c4102-fa40-4585-a8f9-917c77e37192"}}
`,
},
{
@ -854,13 +854,13 @@ func Test_newCellResponses(t *testing.T) {
Queries: []chronograf.DashboardQuery{},
Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{
Bounds: []string{},
Bounds: []string{"", ""},
},
"y": chronograf.Axis{
Bounds: []string{},
Bounds: []string{"", ""},
},
"y2": chronograf.Axis{
Bounds: []string{},
Bounds: []string{"", ""},
},
},
CellColors: []chronograf.CellColor{},

View File

@ -299,7 +299,7 @@ func Test_newDashboardResponse(t *testing.T) {
Label: "foo",
},
"y2": chronograf.Axis{
Bounds: []string{},
Bounds: []string{"", ""},
},
},
},
@ -314,13 +314,13 @@ func Test_newDashboardResponse(t *testing.T) {
H: 4,
Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{
Bounds: []string{},
Bounds: []string{"", ""},
},
"y": chronograf.Axis{
Bounds: []string{},
Bounds: []string{"", ""},
},
"y2": chronograf.Axis{
Bounds: []string{},
Bounds: []string{"", ""},
},
},
CellColors: []chronograf.CellColor{},

View File

@ -16,7 +16,7 @@ type postKapacitorRequest struct {
URL *string `json:"url"` // URL for the kapacitor backend (e.g. http://localhost:9092);/ Required: true
Username string `json:"username,omitempty"` // Username for authentication to kapacitor
Password string `json:"password,omitempty"`
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` // InsecureSkipVerify as true means any certificate presented by the kapacitor is accepted.
InsecureSkipVerify bool `json:"insecureSkipVerify"` // InsecureSkipVerify as true means any certificate presented by the kapacitor is accepted.
Active bool `json:"active"`
Organization string `json:"organization"` // Organization is the organization ID that resource belongs to
}
@ -55,7 +55,7 @@ type kapacitor struct {
URL string `json:"url"` // URL for the kapacitor backend (e.g. http://localhost:9092)
Username string `json:"username,omitempty"` // Username for authentication to kapacitor
Password string `json:"password,omitempty"`
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` // InsecureSkipVerify as true means any certificate presented by the kapacitor is accepted.
InsecureSkipVerify bool `json:"insecureSkipVerify"` // InsecureSkipVerify as true means any certificate presented by the kapacitor is accepted.
Active bool `json:"active"`
Links kapaLinks `json:"links"` // Links are URI locations related to kapacitor
}
@ -225,7 +225,7 @@ type patchKapacitorRequest struct {
URL *string `json:"url,omitempty"` // URL for the kapacitor
Username *string `json:"username,omitempty"` // Username for kapacitor auth
Password *string `json:"password,omitempty"`
InsecureSkipVerify *bool `json:"insecureSkipVerify,omitempty"` // InsecureSkipVerify as true means any certificate presented by the kapacitor is accepted.
InsecureSkipVerify *bool `json:"insecureSkipVerify"` // InsecureSkipVerify as true means any certificate presented by the kapacitor is accepted.
Active *bool `json:"active"`
}

View File

@ -30,6 +30,10 @@ func newLayoutResponse(layout chronograf.Layout) layoutResponse {
layout.Cells[idx].Axes = make(map[string]chronograf.Axis, len(axes))
}
if cell.CellColors == nil {
layout.Cells[idx].CellColors = []chronograf.CellColor{}
}
for _, axis := range axes {
if _, found := cell.Axes[axis]; !found {
layout.Cells[idx].Axes[axis] = chronograf.Axis{

View File

@ -76,12 +76,13 @@ func Test_Layouts(t *testing.T) {
Measurement: "influxdb",
Cells: []chronograf.Cell{
{
X: 0,
Y: 0,
W: 4,
H: 4,
I: "3b0e646b-2ca3-4df2-95a5-fd80915459dd",
Name: "A Graph",
X: 0,
Y: 0,
W: 4,
H: 4,
I: "3b0e646b-2ca3-4df2-95a5-fd80915459dd",
Name: "A Graph",
CellColors: []chronograf.CellColor{},
Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{
Bounds: []string{},
@ -103,12 +104,13 @@ func Test_Layouts(t *testing.T) {
Measurement: "influxdb",
Cells: []chronograf.Cell{
{
X: 0,
Y: 0,
W: 4,
H: 4,
I: "3b0e646b-2ca3-4df2-95a5-fd80915459dd",
Name: "A Graph",
X: 0,
Y: 0,
W: 4,
H: 4,
I: "3b0e646b-2ca3-4df2-95a5-fd80915459dd",
CellColors: []chronograf.CellColor{},
Name: "A Graph",
},
},
},

View File

@ -13,6 +13,22 @@ import (
"github.com/influxdata/chronograf/oauth2"
)
func (s *Service) mapPrincipalToSuperAdmin(p oauth2.Principal) bool {
if p.Issuer != "auth0" {
return false
}
groups := strings.Split(p.Group, ",")
superAdmin := false
for _, group := range groups {
if group != "" && group == s.SuperAdminProviderGroups.auth0 {
superAdmin = true
break
}
}
return superAdmin
}
func (s *Service) mapPrincipalToRoles(ctx context.Context, p oauth2.Principal) ([]chronograf.Role, error) {
mappings, err := s.Store.Mappings(ctx).All(ctx)
if err != nil {
@ -131,8 +147,6 @@ func (s *Service) Mappings(w http.ResponseWriter, r *http.Request) {
return
}
fmt.Printf("mappings: %#v\n", mappings)
res := newMappingsResponse(mappings)
encodeJSON(w, http.StatusOK, res, s.Logger)

View File

@ -235,6 +235,16 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
// user exists
if usr != nil {
superAdmin := s.mapPrincipalToSuperAdmin(p)
if superAdmin && !usr.SuperAdmin {
usr.SuperAdmin = superAdmin
err := s.Store.Users(serverCtx).Update(serverCtx, usr)
if err != nil {
unknownErrorWithMessage(w, err, s.Logger)
return
}
}
currentOrg, err := s.Store.Organizations(serverCtx).Get(serverCtx, chronograf.OrganizationQuery{ID: &p.Organization})
if err == chronograf.ErrOrganizationNotFound {
// The intent is to force a the user to go through another auth flow
@ -271,17 +281,39 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
SuperAdmin: s.newUsersAreSuperAdmin(),
}
superAdmin := s.mapPrincipalToSuperAdmin(p)
if superAdmin {
user.SuperAdmin = superAdmin
}
roles, err := s.mapPrincipalToRoles(serverCtx, p)
if err != nil {
Error(w, http.StatusInternalServerError, err.Error(), s.Logger)
return
}
if len(roles) == 0 {
if !superAdmin && len(roles) == 0 {
Error(w, http.StatusForbidden, "This Chronograf is private. To gain access, you must be explicitly added by an administrator.", s.Logger)
return
}
// If the user is a superadmin, give them a role in the default organization
if user.SuperAdmin {
hasDefaultOrgRole := false
for _, role := range roles {
if role.Organization == defaultOrg.ID {
hasDefaultOrgRole = true
break
}
}
if !hasDefaultOrgRole {
roles = append(roles, chronograf.Role{
Name: defaultOrg.DefaultRole,
Organization: defaultOrg.ID,
})
}
}
user.Roles = roles
newUser, err := s.Store.Users(serverCtx).Add(serverCtx, user)

View File

@ -21,12 +21,13 @@ type MockUsers struct{}
func TestService_Me(t *testing.T) {
type fields struct {
UsersStore chronograf.UsersStore
OrganizationsStore chronograf.OrganizationsStore
MappingsStore chronograf.MappingsStore
ConfigStore chronograf.ConfigStore
Logger chronograf.Logger
UseAuth bool
UsersStore chronograf.UsersStore
OrganizationsStore chronograf.OrganizationsStore
MappingsStore chronograf.MappingsStore
ConfigStore chronograf.ConfigStore
SuperAdminProviderGroups superAdminProviderGroups
Logger chronograf.Logger
UseAuth bool
}
type args struct {
w *httptest.ResponseRecorder
@ -602,7 +603,7 @@ func TestService_Me(t *testing.T) {
},
},
{
name: "new user - default org is private",
name: "new user - Chronograf is private",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
@ -658,6 +659,455 @@ func TestService_Me(t *testing.T) {
wantContentType: "application/json",
wantBody: `{"code":403,"message":"This Chronograf is private. To gain access, you must be explicitly added by an administrator."}`,
},
{
name: "new user - Chronograf is private, user is in auth0 superadmin group",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
UseAuth: true,
SuperAdminProviderGroups: superAdminProviderGroups{
auth0: "example",
},
Logger: log.New(log.DebugLevel),
ConfigStore: mocks.ConfigStore{
Config: &chronograf.Config{
Auth: chronograf.AuthConfig{
SuperAdminNewUsers: false,
},
},
},
MappingsStore: &mocks.MappingsStore{
AllF: func(ctx context.Context) ([]chronograf.Mapping, error) {
return []chronograf.Mapping{}, nil
},
},
OrganizationsStore: &mocks.OrganizationsStore{
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: "0",
Name: "The Bad Place",
DefaultRole: roles.MemberRoleName,
}, nil
},
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: "0",
Name: "The Bad Place",
DefaultRole: roles.MemberRoleName,
}, nil
},
},
UsersStore: &mocks.UsersStore{
NumF: func(ctx context.Context) (int, error) {
// This function gets to verify that there is at least one first user
return 1, nil
},
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 nil, chronograf.ErrUserNotFound
},
AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
return u, nil
},
UpdateF: func(ctx context.Context, u *chronograf.User) error {
return nil
},
},
},
principal: oauth2.Principal{
Subject: "secret",
Issuer: "auth0",
Group: "not_example,example",
},
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"name":"secret","roles":[{"name":"member","organization":"0"}],"provider":"auth0","scheme":"oauth2","superAdmin":true,"links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"The Bad Place","defaultRole":"member"}],"currentOrganization":{"id":"0","name":"The Bad Place","defaultRole":"member"}}`,
},
{
name: "new user - Chronograf is private, user is not in auth0 superadmin group",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
UseAuth: true,
SuperAdminProviderGroups: superAdminProviderGroups{
auth0: "example",
},
Logger: log.New(log.DebugLevel),
ConfigStore: mocks.ConfigStore{
Config: &chronograf.Config{
Auth: chronograf.AuthConfig{
SuperAdminNewUsers: false,
},
},
},
MappingsStore: &mocks.MappingsStore{
AllF: func(ctx context.Context) ([]chronograf.Mapping, error) {
return []chronograf.Mapping{}, nil
},
},
OrganizationsStore: &mocks.OrganizationsStore{
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: "0",
Name: "The Bad Place",
DefaultRole: roles.MemberRoleName,
}, nil
},
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: "0",
Name: "The Bad Place",
DefaultRole: roles.MemberRoleName,
}, nil
},
},
UsersStore: &mocks.UsersStore{
NumF: func(ctx context.Context) (int, error) {
// This function gets to verify that there is at least one first user
return 1, nil
},
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 nil, chronograf.ErrUserNotFound
},
AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
return u, nil
},
UpdateF: func(ctx context.Context, u *chronograf.User) error {
return nil
},
},
},
principal: oauth2.Principal{
Subject: "secret",
Issuer: "auth0",
Group: "not_example",
},
wantStatus: http.StatusForbidden,
wantContentType: "application/json",
wantBody: `{"code":403,"message":"This Chronograf is private. To gain access, you must be explicitly added by an administrator."}`,
},
{
name: "new user - Chronograf is not private, user is in auth0 superadmin group",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
UseAuth: true,
SuperAdminProviderGroups: superAdminProviderGroups{
auth0: "example",
},
Logger: log.New(log.DebugLevel),
ConfigStore: mocks.ConfigStore{
Config: &chronograf.Config{
Auth: chronograf.AuthConfig{
SuperAdminNewUsers: false,
},
},
},
MappingsStore: &mocks.MappingsStore{
AllF: func(ctx context.Context) ([]chronograf.Mapping, error) {
return []chronograf.Mapping{
{
Organization: "0",
Provider: chronograf.MappingWildcard,
Scheme: chronograf.MappingWildcard,
ProviderOrganization: chronograf.MappingWildcard,
},
}, nil
},
},
OrganizationsStore: &mocks.OrganizationsStore{
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: "0",
Name: "The Bad Place",
DefaultRole: roles.MemberRoleName,
}, nil
},
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: "0",
Name: "The Bad Place",
DefaultRole: roles.MemberRoleName,
}, nil
},
},
UsersStore: &mocks.UsersStore{
NumF: func(ctx context.Context) (int, error) {
// This function gets to verify that there is at least one first user
return 1, nil
},
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 nil, chronograf.ErrUserNotFound
},
AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
return u, nil
},
UpdateF: func(ctx context.Context, u *chronograf.User) error {
return nil
},
},
},
principal: oauth2.Principal{
Subject: "secret",
Issuer: "auth0",
Group: "example",
},
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"name":"secret","roles":[{"name":"member","organization":"0"}],"provider":"auth0","scheme":"oauth2","superAdmin":true,"links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"The Bad Place","defaultRole":"member"}],"currentOrganization":{"id":"0","name":"The Bad Place","defaultRole":"member"}}`,
},
{
name: "new user - Chronograf is not private (has a fully open wildcard mapping to an org), user is not in auth0 superadmin group",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
UseAuth: true,
SuperAdminProviderGroups: superAdminProviderGroups{
auth0: "example",
},
Logger: log.New(log.DebugLevel),
ConfigStore: mocks.ConfigStore{
Config: &chronograf.Config{
Auth: chronograf.AuthConfig{
SuperAdminNewUsers: false,
},
},
},
MappingsStore: &mocks.MappingsStore{
AllF: func(ctx context.Context) ([]chronograf.Mapping, error) {
return []chronograf.Mapping{
{
Organization: "0",
Provider: chronograf.MappingWildcard,
Scheme: chronograf.MappingWildcard,
ProviderOrganization: chronograf.MappingWildcard,
},
}, nil
},
},
OrganizationsStore: &mocks.OrganizationsStore{
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: "0",
Name: "The Bad Place",
DefaultRole: roles.MemberRoleName,
}, nil
},
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: "0",
Name: "The Bad Place",
DefaultRole: roles.MemberRoleName,
}, nil
},
},
UsersStore: &mocks.UsersStore{
NumF: func(ctx context.Context) (int, error) {
// This function gets to verify that there is at least one first user
return 1, nil
},
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 nil, chronograf.ErrUserNotFound
},
AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
return u, nil
},
UpdateF: func(ctx context.Context, u *chronograf.User) error {
return nil
},
},
},
principal: oauth2.Principal{
Subject: "secret",
Issuer: "auth0",
Group: "not_example",
},
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"name":"secret","roles":[{"name":"member","organization":"0"}],"provider":"auth0","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"The Bad Place","defaultRole":"member"}],"currentOrganization":{"id":"0","name":"The Bad Place","defaultRole":"member"}}`,
},
{
name: "Existing user - Chronograf is not private, user doesn't have SuperAdmin status, user is in auth0 superadmin group",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
UseAuth: true,
SuperAdminProviderGroups: superAdminProviderGroups{
auth0: "example",
},
Logger: log.New(log.DebugLevel),
ConfigStore: mocks.ConfigStore{
Config: &chronograf.Config{},
},
MappingsStore: &mocks.MappingsStore{
AllF: func(ctx context.Context) ([]chronograf.Mapping, error) {
return []chronograf.Mapping{
{
Organization: "0",
Provider: chronograf.MappingWildcard,
Scheme: chronograf.MappingWildcard,
ProviderOrganization: chronograf.MappingWildcard,
},
}, nil
},
},
OrganizationsStore: &mocks.OrganizationsStore{
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: "0",
Name: "The Bad Place",
DefaultRole: roles.MemberRoleName,
}, nil
},
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: "0",
Name: "The Bad Place",
DefaultRole: roles.MemberRoleName,
}, nil
},
},
UsersStore: &mocks.UsersStore{
NumF: func(ctx context.Context) (int, error) {
// This function gets to verify that there is at least one first user
return 1, nil
},
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{
Name: "secret",
Provider: "auth0",
Scheme: "oauth2",
Roles: []chronograf.Role{
{
Name: roles.MemberRoleName,
Organization: "0",
},
},
}, nil
},
AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
return u, nil
},
UpdateF: func(ctx context.Context, u *chronograf.User) error {
return nil
},
},
},
principal: oauth2.Principal{
Subject: "secret",
Issuer: "auth0",
Group: "example",
},
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"name":"secret","roles":[{"name":"member","organization":"0"}],"provider":"auth0","scheme":"oauth2","superAdmin":true,"links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"The Bad Place","defaultRole":"member"}],"currentOrganization":{"id":"0","name":"The Bad Place","defaultRole":"member"}}`,
},
{
name: "Existing user - Chronograf is not private, user has SuperAdmin status, user is in auth0 superadmin group",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
UseAuth: true,
SuperAdminProviderGroups: superAdminProviderGroups{
auth0: "example",
},
Logger: log.New(log.DebugLevel),
ConfigStore: mocks.ConfigStore{
Config: &chronograf.Config{},
},
MappingsStore: &mocks.MappingsStore{
AllF: func(ctx context.Context) ([]chronograf.Mapping, error) {
return []chronograf.Mapping{
{
Organization: "0",
Provider: chronograf.MappingWildcard,
Scheme: chronograf.MappingWildcard,
ProviderOrganization: chronograf.MappingWildcard,
},
}, nil
},
},
OrganizationsStore: &mocks.OrganizationsStore{
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: "0",
Name: "The Bad Place",
DefaultRole: roles.MemberRoleName,
}, nil
},
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: "0",
Name: "The Bad Place",
DefaultRole: roles.MemberRoleName,
}, nil
},
},
UsersStore: &mocks.UsersStore{
NumF: func(ctx context.Context) (int, error) {
// This function gets to verify that there is at least one first user
return 1, nil
},
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{
Name: "secret",
Provider: "auth0",
Scheme: "oauth2",
Roles: []chronograf.Role{
{
Name: roles.MemberRoleName,
Organization: "0",
},
},
SuperAdmin: true,
}, nil
},
AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
return u, nil
},
UpdateF: func(ctx context.Context, u *chronograf.User) error {
return nil
},
},
},
principal: oauth2.Principal{
Subject: "secret",
Issuer: "auth0",
Group: "example",
},
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"name":"secret","roles":[{"name":"member","organization":"0"}],"provider":"auth0","scheme":"oauth2","superAdmin":true,"links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"The Bad Place","defaultRole":"member"}],"currentOrganization":{"id":"0","name":"The Bad Place","defaultRole":"member"}}`,
},
}
for _, tt := range tests {
tt.args.r = tt.args.r.WithContext(context.WithValue(context.Background(), oauth2.PrincipalKey, tt.principal))
@ -668,11 +1118,11 @@ func TestService_Me(t *testing.T) {
MappingsStore: tt.fields.MappingsStore,
ConfigStore: tt.fields.ConfigStore,
},
Logger: tt.fields.Logger,
UseAuth: tt.fields.UseAuth,
Logger: tt.fields.Logger,
UseAuth: tt.fields.UseAuth,
SuperAdminProviderGroups: tt.fields.SuperAdminProviderGroups,
}
fmt.Println(tt.name)
s.Me(tt.args.w, tt.args.r)
resp := tt.args.w.Result()

View File

@ -171,6 +171,13 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
// intended for Chronograf Users with the Viewer Role type.
router.POST("/chronograf/v1/sources/:id/queries", EnsureViewer(service.Queries))
// Annotations are user-defined events associated with this source
router.GET("/chronograf/v1/sources/:id/annotations", EnsureViewer(service.Annotations))
router.POST("/chronograf/v1/sources/:id/annotations", EnsureEditor(service.NewAnnotation))
router.GET("/chronograf/v1/sources/:id/annotations/:aid", EnsureViewer(service.Annotation))
router.DELETE("/chronograf/v1/sources/:id/annotations/:aid", EnsureEditor(service.RemoveAnnotation))
router.PATCH("/chronograf/v1/sources/:id/annotations/:aid", EnsureEditor(service.UpdateAnnotation))
// All possible permissions for users in this source
router.GET("/chronograf/v1/sources/:id/permissions", EnsureViewer(service.Permissions))
@ -420,3 +427,19 @@ func paramID(key string, r *http.Request) (int, error) {
}
return id, nil
}
func paramInt64(key string, r *http.Request) (int64, error) {
ctx := r.Context()
param := httprouter.GetParamFromContext(ctx, key)
v, err := strconv.ParseInt(param, 10, 64)
if err != nil {
return -1, fmt.Errorf("Error converting parameter %s", param)
}
return v, nil
}
func paramStr(key string, r *http.Request) (string, error) {
ctx := r.Context()
param := httprouter.GetParamFromContext(ctx, key)
return param, nil
}

View File

@ -87,6 +87,7 @@ type Server struct {
Auth0ClientID string `long:"auth0-client-id" description:"Auth0 Client ID for OAuth2 support" env:"AUTH0_CLIENT_ID"`
Auth0ClientSecret string `long:"auth0-client-secret" description:"Auth0 Client Secret for OAuth2 support" env:"AUTH0_CLIENT_SECRET"`
Auth0Organizations []string `long:"auth0-organizations" description:"Auth0 organizations permitted to access Chronograf (comma separated)" env:"AUTH0_ORGS" env-delim:","`
Auth0SuperAdminOrg string `long:"auth0-superadmin-org" description:"Auth0 organization from which users are automatically granted SuperAdmin status" env:"AUTH0_SUPERADMIN_ORG"`
StatusFeedURL string `long:"status-feed-url" description:"URL of a JSON Feed to display as a News Feed on the client Status page." default:"https://www.influxdata.com/feed/json" env:"STATUS_FEED_URL"`
CustomLinks map[string]string `long:"custom-link" description:"Custom link to be added to the client User menu. Multiple links can be added by using multiple of the same flag with different 'name:url' values, or as an environment variable with comma-separated 'name:url' values. E.g. via flags: '--custom-link=InfluxData:https://www.influxdata.com --custom-link=Chronograf:https://github.com/influxdata/chronograf'. E.g. via environment variable: 'export CUSTOM_LINKS=InfluxData:https://www.influxdata.com,Chronograf:https://github.com/influxdata/chronograf'" env:"CUSTOM_LINKS" env-delim:","`
@ -328,6 +329,9 @@ func (s *Server) Serve(ctx context.Context) error {
return err
}
service := openService(ctx, s.BuildInfo, s.BoltPath, s.newBuilders(logger), logger, s.useAuth())
service.SuperAdminProviderGroups = superAdminProviderGroups{
auth0: s.Auth0SuperAdminOrg,
}
service.Env = chronograf.Environment{
TelegrafSystemInterval: s.TelegrafSystemInterval,
}

View File

@ -11,12 +11,17 @@ import (
// Service handles REST calls to the persistence
type Service struct {
Store DataStore
TimeSeriesClient TimeSeriesClient
Logger chronograf.Logger
UseAuth bool
Env chronograf.Environment
Databases chronograf.Databases
Store DataStore
TimeSeriesClient TimeSeriesClient
Logger chronograf.Logger
UseAuth bool
SuperAdminProviderGroups superAdminProviderGroups
Env chronograf.Environment
Databases chronograf.Databases
}
type superAdminProviderGroups struct {
auth0 string
}
// TimeSeriesClient returns the correct client for a time series database.

View File

@ -7,6 +7,8 @@ import (
"net/http"
"net/url"
"github.com/influxdata/chronograf/organizations"
"github.com/bouk/httprouter"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/influx"
@ -21,7 +23,8 @@ type sourceLinks struct {
Permissions string `json:"permissions"` // URL for all allowed permissions for this source
Users string `json:"users"` // URL for all users associated with this source
Roles string `json:"roles,omitempty"` // URL for all users associated with this source
Databases string `json:"databases"` // URL for the databases contained within this soure
Databases string `json:"databases"` // URL for the databases contained within this source
Annotations string `json:"annotations"` // URL for the annotations of this source
}
type sourceResponse struct {
@ -51,6 +54,7 @@ func newSourceResponse(src chronograf.Source) sourceResponse {
Permissions: fmt.Sprintf("%s/%d/permissions", httpAPISrcs, src.ID),
Users: fmt.Sprintf("%s/%d/users", httpAPISrcs, src.ID),
Databases: fmt.Sprintf("%s/%d/dbs", httpAPISrcs, src.ID),
Annotations: fmt.Sprintf("%s/%d/annotations", httpAPISrcs, src.ID),
},
}
@ -328,6 +332,8 @@ func (s *Service) HandleNewSources(ctx context.Context, input string) error {
return nil
}
s.Logger.Error("--new-sources is deprecated and will be removed in a future version.")
var srcsKaps []struct {
Source chronograf.Source `json:"influxdb"`
Kapacitor chronograf.Server `json:"kapacitor"`
@ -340,6 +346,7 @@ func (s *Service) HandleNewSources(ctx context.Context, input string) error {
return err
}
ctx = context.WithValue(ctx, organizations.ContextKey, "default")
defaultOrg, err := s.Store.Organizations(ctx).DefaultOrganization(ctx)
if err != nil {
return err

View File

@ -182,6 +182,7 @@ func Test_newSourceResponse(t *testing.T) {
Users: "/chronograf/v1/sources/1/users",
Permissions: "/chronograf/v1/sources/1/permissions",
Databases: "/chronograf/v1/sources/1/dbs",
Annotations: "/chronograf/v1/sources/1/annotations",
},
},
},
@ -205,6 +206,7 @@ func Test_newSourceResponse(t *testing.T) {
Users: "/chronograf/v1/sources/1/users",
Permissions: "/chronograf/v1/sources/1/permissions",
Databases: "/chronograf/v1/sources/1/dbs",
Annotations: "/chronograf/v1/sources/1/annotations",
},
},
},

View File

@ -3,7 +3,7 @@
"info": {
"title": "Chronograf",
"description": "API endpoints for Chronograf",
"version": "1.4.1.3"
"version": "1.4.2.3"
},
"schemes": ["http"],
"basePath": "/chronograf/v1",
@ -3140,7 +3140,7 @@
"rp": "autogen",
"tempVars": [
{
"tempVar": "$myfield",
"tempVar": ":myfield:",
"values": [
{
"type": "fieldKey",
@ -3161,6 +3161,11 @@
"rp": {
"type": "string"
},
"epoch": {
"description": "timestamp return format",
"type": "string",
"enum": ["h", "m", "s", "ms", "u", "ns"]
},
"tempVars": {
"type": "array",
"description":
@ -3983,6 +3988,48 @@
}
}
},
"tableOptions": {
"description":
"visualization options for a cell with table type",
"type": "object",
"properties": {
"timeFormat": {
"description":
"timeFormat describes the display format for time values according to moment.js date formatting",
"type": "string"
},
"verticalTimeAxis": {
"description":
"verticalTimeAxis describes the orientation of the table by indicating whether the time axis will be displayed vertically",
"type": "boolean"
},
"sortBy": {
"description":
"sortBy contains the name of the series that is used for sorting the table",
"type": "object",
"$ref": "#/definitions/RenamableField"
},
"wrapping": {
"description":
"wrapping describes the text wrapping style to be used in table cells",
"type": "string",
"enum": ["truncate", "wrap", "single-line"]
},
"fieldNames": {
"description":
"fieldNames represent the fields retrieved by the query with customization options",
"type": "array",
"items": {
"$ref": "#/definitions/RenamableField"
}
},
"fixFirstColumn": {
"description":
"fixFirstColumn indicates whether this field should be visible on the table",
"type": "boolean"
}
}
},
"links": {
"type": "object",
"properties": {
@ -4124,6 +4171,27 @@
}
}
},
"RenamableField": {
"description":
"renamableField describes a field that can be renamed and made visible or invisible",
"type": "object",
"properties": {
"internalName": {
"description": "internalName is the calculated name of a field",
"type": "string"
},
"displayName": {
"description":
"displayName is the name that a field is renamed to by the user",
"type": "string"
},
"visible": {
"description":
"visible indicates whether this field should be visible on the table",
"type": "boolean"
}
}
},
"Routes": {
"type": "object",
"properties": {

View File

@ -6,7 +6,7 @@
"transform-runtime",
"lodash"
],
"presets": ["es2015", "react", "stage-0"],
"presets": ["env", "react", "stage-0"],
"env": {
"production": {
"plugins": [

View File

@ -1,250 +1,292 @@
{
parser: 'babel-eslint',
plugins: [
'react',
'prettier',
'babel',
],
env: {
browser: true,
mocha: true,
node: true,
es6: true,
},
globals: {
expect: true,
},
"parserOptions": {
ecmaFeatures: {
arrowFunctions: true,
binaryLiterals: true,
blockBindings: true,
classes: true,
defaultParams: false,
destructuring: true,
forOf: false,
generators: false,
modules: true,
objectLiteralComputedProperties: true,
objectLiteralDuplicateProperties: false,
objectLiteralShorthandMethods: true,
objectLiteralShorthandProperties: true,
octalLiterals: false,
regexUFlag: false,
regexYFlag: false,
restParams: true,
spread: true,
superInFunctions: false,
templateStrings: true,
unicodeCodePointEscapes: false,
globalReturn: false,
jsx: true,
},
},
rules: {
'quotes': [1, 'single'],
'func-style': 0,
'func-names': 0,
'arrow-parens': 0,
'comma-dangle': [2, 'always-multiline'],
'no-cond-assign': 2,
'no-console': ['error', {allow: ['error', 'warn']}],
'no-constant-condition': 2,
'no-control-regex': 2,
'no-debugger': 2,
'no-dupe-args': 2,
'no-dupe-keys': 2,
'no-duplicate-case': 2,
'no-empty-character-class': 2,
'no-empty': 2,
'no-ex-assign': 2,
'no-extra-boolean-cast': 2,
'no-extra-parens': 0,
'no-extra-semi': 2,
'no-func-assign': 2,
'no-inner-declarations': [2, 'both'],
'no-invalid-regexp': 2,
'no-irregular-whitespace': 2,
'no-negated-in-lhs': 2,
'no-obj-calls': 2,
'no-regex-spaces': 2,
'no-sparse-arrays': 2,
'no-unexpected-multiline': 2,
'no-unreachable': 2,
'use-isnan': 2,
'valid-jsdoc': 0,
'valid-typeof': 2,
'accessor-pairs': 2,
'block-scoped-var': 2,
'complexity': 0, // TODO: revisit
'consistent-return': 0,
'curly': 2,
'default-case': 0, // TODO: revisit
'dot-location': [2, 'property'],
'dot-notation': 2,
'eqeqeq': 2,
'no-alert': 2,
'no-caller': 2,
'no-case-declarations': 2,
'no-div-regex': 2,
'no-else-return': 2,
'no-labels': 2,
'no-empty-pattern': 2,
'no-eq-null': 2,
'no-eval': 2,
'no-extend-native': 2,
'no-extra-bind': 2,
'no-fallthrough': 2,
'no-floating-decimal': 2,
'no-implicit-coercion': 0,
'no-implied-eval': 2,
'no-iterator': 2,
'no-lone-blocks': 2,
'no-loop-func': 2,
'no-magic-numbers': [0, {ignore: [-1, 0, 1, 2]}],
'no-multi-spaces': 2,
'no-multi-str': 2,
'no-native-reassign': 2,
'no-new-func': 2,
'no-new-wrappers': 2,
'no-new': 2,
'no-octal-escape': 2,
'no-octal': 2,
'no-proto': 2,
'no-redeclare': 2,
'no-script-url': 2,
'no-self-compare': 2,
'no-sequences': 2,
'no-throw-literal': 2,
'no-unused-expressions': 2,
'no-useless-call': 2,
'no-useless-concat': 2,
'no-void': 2,
'no-warning-comments': 0,
'no-with': 2,
'radix': 2,
'vars-on-top': 2,
'strict': [2, 'never'],
'init-declarations': 0,
'no-catch-shadow': 2,
'no-delete-var': 2,
'no-label-var': 2,
'no-shadow-restricted-names': 2,
'no-shadow': 2,
'no-undef-init': 2,
'no-undef': 2,
'no-unused-vars': [2, {args: 'after-used', 'argsIgnorePattern': '^_'}],
'no-use-before-define': [2, 'nofunc'],
'array-bracket-spacing': [2, 'never'],
'block-spacing': [2, 'always'],
'brace-style': [2, '1tbs'],
'camelcase': [2, {properties: 'never'}],
'comma-spacing': [2, {before: false, after: true}],
'comma-style': [2, 'last'],
'computed-property-spacing': [2, 'never'],
'consistent-this': [2, 'self'],
'eol-last': 0, // TODO: revisit
'id-length': 0,
'id-match': 0,
'indent': [0, 2, {SwitchCase: 1}],
'key-spacing': [2, {beforeColon: false, afterColon: true}],
'linebreak-style': [2, 'unix'],
'lines-around-comment': 0,
'max-depth': 0,
'max-len': 0,
'max-nested-callbacks': 0,
'max-params': 0,
'max-statements': 0,
'new-cap': 0,
'new-parens': 2,
'newline-after-var': 0,
'no-array-constructor': 2,
'no-negated-condition': 2,
'no-inline-comments': 0,
'no-lonely-if': 2,
'no-mixed-spaces-and-tabs': 2,
'no-multiple-empty-lines': 2,
'no-nested-ternary': 2,
'no-new-object': 2,
'no-plusplus': [2, {allowForLoopAfterthoughts: true}],
'no-spaced-func': 2,
'no-ternary': 0,
'no-trailing-spaces': 2,
'no-underscore-dangle': 0,
'no-unneeded-ternary': 2,
'object-curly-spacing': [2, 'never'],
'one-var': 0,
'operator-assignment': [2, 'always'],
'padded-blocks': [2, 'never'],
'quote-props': [2, 'as-needed', {keywords: false, numbers: false }],
'require-jsdoc': 0,
'semi-spacing': [2, {before: false, after: true}],
'semi': [2, 'never'],
'sort-vars': 0,
'keyword-spacing': 'error',
'space-before-blocks': [2, 'always'],
'space-before-function-paren': [2, 'never'],
'space-in-parens': [2, 'never'],
'space-infix-ops': 2,
'space-unary-ops': 2,
'spaced-comment': [2, 'always'],
'wrap-regex': 0,
'arrow-body-style': 0,
'arrow-spacing': [2, {before: true, after: true}],
'no-confusing-arrow': 0,
'no-class-assign': 2,
'no-const-assign': 2,
'no-dupe-class-members': 2,
'no-this-before-super': 2,
'no-var': 2,
'object-shorthand': [2, 'always'],
'prefer-arrow-callback': 0,
'prefer-const': 2,
'prefer-template': 2,
// React
'jsx-quotes': [1, "prefer-double"],
'react/display-name': 0,
'react/jsx-no-bind': [2, {ignoreRefs: true}],
'react/jsx-boolean-value': [2, 'always'],
'react/jsx-curly-spacing': [2, 'never'],
'react/jsx-equals-spacing': [2, 'never'],
'react/jsx-key': 2,
'react/jsx-no-duplicate-props': 2,
'react/jsx-no-undef': 2,
'react/jsx-sort-props': 0,
'react/jsx-sort-prop-types': 0,
'react/jsx-uses-react': 2,
'react/jsx-uses-vars': 2,
'react/no-danger': 2,
'react/no-did-mount-set-state': 0,
'react/no-did-update-set-state': 2,
'react/no-direct-mutation-state': 2,
'react/no-is-mounted': 2,
'react/no-multi-comp': 0,
'react/no-set-state': 0,
'react/no-string-refs': 0, // TODO: 2
'react/no-unknown-property': 2,
'react/prop-types': 2,
'react/prefer-es6-class': [0, 'never'],
'react/react-in-jsx-scope': 2,
'react/require-extension': 0,
'react/self-closing-comp': 0, // TODO: we can re-enable this if some brave soul wants to update the code (mostly spans acting as icons)
'react/sort-comp': 0, // TODO: 2
// Prettier
'prettier/prettier': ['error', {
'singleQuote': true,
'trailingComma': 'es5',
'bracketSpacing': false,
'semi': false,
}],
// Babel
'babel/no-invalid-this': 1
},
"parser": "babel-eslint",
"plugins": [
"react",
"prettier",
"babel",
"jest"
],
"extends": [
"prettier",
"prettier/react"
],
"env": {
"browser": true,
"mocha": true,
"jest": true,
"node": true,
"es6": true
},
"globals": {
"expect": true
},
"parserOptions": {
"ecmaFeatures": {
"arrowFunctions": true,
"binaryLiterals": true,
"blockBindings": true,
"classes": true,
"defaultParams": false,
"destructuring": true,
"forOf": false,
"generators": false,
"modules": true,
"objectLiteralComputedProperties": true,
"objectLiteralDuplicateProperties": false,
"objectLiteralShorthandMethods": true,
"objectLiteralShorthandProperties": true,
"octalLiterals": false,
"regexUFlag": false,
"regexYFlag": false,
"restParams": true,
"spread": true,
"superInFunctions": false,
"templateStrings": true,
"unicodeCodePointEscapes": false,
"globalReturn": false,
"jsx": true
}
},
"rules": {
"func-style": 0,
"func-names": 0,
"arrow-parens": 0,
"no-cond-assign": 2,
"no-console": [
"error",
{
"allow": [
"error",
"warn"
]
}
],
"no-constant-condition": 2,
"no-control-regex": 2,
"no-debugger": 2,
"no-dupe-args": 2,
"no-dupe-keys": 2,
"no-duplicate-case": 2,
"no-empty-character-class": 2,
"no-empty": 2,
"no-ex-assign": 2,
"no-extra-boolean-cast": 2,
"no-extra-parens": 0,
"no-func-assign": 2,
"no-inner-declarations": [
2,
"both"
],
"no-invalid-regexp": 2,
"no-irregular-whitespace": 2,
"no-negated-in-lhs": 2,
"no-obj-calls": 2,
"no-regex-spaces": 2,
"no-sparse-arrays": 2,
"no-unreachable": 2,
"use-isnan": 2,
"valid-jsdoc": 0,
"valid-typeof": 2,
"accessor-pairs": 2,
"block-scoped-var": 2,
"complexity": 0,
"consistent-return": 0,
"curly": 2,
"default-case": 0,
"dot-notation": 2,
"eqeqeq": 2,
"no-alert": 2,
"no-caller": 2,
"no-case-declarations": 2,
"no-div-regex": 2,
"no-else-return": 2,
"no-labels": 2,
"no-empty-pattern": 2,
"no-eq-null": 2,
"no-eval": 2,
"no-extend-native": 2,
"no-extra-bind": 2,
"no-fallthrough": 2,
"no-implicit-coercion": 0,
"no-implied-eval": 2,
"no-iterator": 2,
"no-lone-blocks": 2,
"no-loop-func": 2,
"no-magic-numbers": [
0,
{
"ignore": [
-1,
0,
1,
2
]
}
],
"no-multi-str": 2,
"no-native-reassign": 2,
"no-new-func": 2,
"no-new-wrappers": 2,
"no-new": 2,
"no-octal-escape": 2,
"no-octal": 2,
"no-proto": 2,
"no-redeclare": 2,
"no-script-url": 2,
"no-self-compare": 2,
"no-sequences": 2,
"no-throw-literal": 2,
"no-unused-expressions": 2,
"no-useless-call": 2,
"no-useless-concat": 2,
"no-void": 2,
"no-warning-comments": 0,
"no-with": 2,
"radix": 2,
"vars-on-top": 2,
"strict": [
2,
"never"
],
"init-declarations": 0,
"no-catch-shadow": 2,
"no-delete-var": 2,
"no-label-var": 2,
"no-shadow-restricted-names": 2,
"no-shadow": 2,
"no-undef-init": 2,
"no-undef": 2,
"no-unused-vars": [
2,
{
"args": "after-used",
"argsIgnorePattern": "^_"
}
],
"no-use-before-define": [
2,
"nofunc"
],
"camelcase": [
2,
{
"properties": "never"
}
],
"consistent-this": [
2,
"self"
],
"eol-last": 0,
"id-length": 0,
"id-match": 0,
"indent": [
0,
2,
{
"SwitchCase": 1
}
],
"linebreak-style": [
2,
"unix"
],
"lines-around-comment": 0,
"max-depth": 0,
"max-len": 0,
"max-nested-callbacks": 0,
"max-params": 0,
"max-statements": 0,
"new-cap": 0,
"newline-after-var": 0,
"no-array-constructor": 2,
"no-negated-condition": 2,
"no-inline-comments": 0,
"no-lonely-if": 2,
"no-nested-ternary": 2,
"no-new-object": 2,
"no-plusplus": [
2,
{
"allowForLoopAfterthoughts": true
}
],
"no-ternary": 0,
"no-underscore-dangle": 0,
"no-unneeded-ternary": 2,
"one-var": 0,
"operator-assignment": [
2,
"always"
],
"require-jsdoc": 0,
"sort-vars": 0,
"spaced-comment": [
2,
"always"
],
"wrap-regex": 0,
"arrow-body-style": 0,
"no-confusing-arrow": 0,
"no-class-assign": 2,
"no-const-assign": 2,
"no-dupe-class-members": 2,
"no-this-before-super": 2,
"no-var": 2,
"object-shorthand": [
2,
"always"
],
"prefer-arrow-callback": 0,
"prefer-const": 2,
"prefer-template": 2,
"react/display-name": 0,
"react/jsx-no-bind": [
2,
{
"ignoreRefs": true
}
],
"react/jsx-boolean-value": [
2,
"always"
],
"react/jsx-key": 2,
"react/jsx-no-duplicate-props": 2,
"react/jsx-no-undef": 2,
"react/jsx-sort-props": 0,
"react/jsx-sort-prop-types": 0,
"react/jsx-uses-react": 2,
"react/jsx-uses-vars": 2,
"react/no-danger": 2,
"react/no-did-mount-set-state": 0,
"react/no-did-update-set-state": 2,
"react/no-direct-mutation-state": 2,
"react/no-is-mounted": 2,
"react/no-multi-comp": 0,
"react/no-set-state": 0,
"react/no-string-refs": 0,
"react/no-unknown-property": 2,
"react/prop-types": 2,
"react/prefer-es6-class": [
0,
"never"
],
"react/react-in-jsx-scope": 2,
"react/require-extension": 0,
"react/self-closing-comp": 0,
"react/sort-comp": 0,
"prettier/prettier": [
"error",
{
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": false,
"semi": false
}
],
"jest/no-disabled-tests": "warn",
"jest/no-focused-tests": "error",
"babel/no-invalid-this": 1
}
}

1
ui/.gitignore vendored
View File

@ -6,3 +6,4 @@ dist/
bower_components/
log/
.tern-project
yarn-error.log

34
ui/jest.config.js Normal file
View File

@ -0,0 +1,34 @@
module.exports = {
projects: [
{
displayName: 'test',
testPathIgnorePatterns: [
'build',
'<rootDir>/node_modules/(?!(jest-test))',
],
modulePaths: ['<rootDir>', '<rootDir>/node_modules/'],
moduleDirectories: ['src'],
setupFiles: ['<rootDir>/test/setupTests.js'],
transform: {
'^.+\\.tsx?$': 'ts-jest',
'^.+\\.js$': 'babel-jest',
},
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
},
{
runner: 'jest-runner-eslint',
displayName: 'eslint',
testMatch: ['<rootDir>/test/**/*.test.js'],
},
{
runner: 'jest-runner-tslint',
displayName: 'tslint',
moduleFileExtensions: ['ts', 'tsx'],
testMatch: [
'<rootDir>/test/**/*.test.ts',
'<rootDir>/test/**/*.test.tsx',
],
},
],
}

View File

@ -1,11 +1,10 @@
var webpack = require('webpack')
var path = require('path')
const webpack = require('webpack')
const path = require('path')
module.exports = function(config) {
config.set({
browsers: ['PhantomJS'],
singleRun: true,
frameworks: ['mocha', 'sinon-chai'],
frameworks: ['mocha'],
files: [
'node_modules/babel-polyfill/dist/polyfill.js',
'spec/spec-helper.js',
@ -42,10 +41,6 @@ module.exports = function(config) {
test: /sinon\/pkg\/sinon\.js/,
loader: 'imports?define=>false,require=>false',
},
{
test: /\.json$/,
loader: 'json',
},
],
},
externals: {
@ -61,7 +56,6 @@ module.exports = function(config) {
shared: path.resolve(__dirname, 'src', 'shared'),
style: path.resolve(__dirname, 'src', 'style'),
utils: path.resolve(__dirname, 'src', 'utils'),
sinon: 'sinon/pkg/sinon',
},
},
},

54
ui/mocks/dummy.ts Normal file
View File

@ -0,0 +1,54 @@
export const source = {
id: '2',
name: 'minikube-influx',
type: 'influx',
url: 'http://192.168.99.100:30400',
default: true,
telegraf: 'telegraf',
organization: 'default',
role: 'viewer',
links: {
self: '/chronograf/v1/sources/2',
kapacitors: '/chronograf/v1/sources/2/kapacitors',
proxy: '/chronograf/v1/sources/2/proxy',
queries: '/chronograf/v1/sources/2/queries',
write: '/chronograf/v1/sources/2/write',
permissions: '/chronograf/v1/sources/2/permissions',
users: '/chronograf/v1/sources/2/users',
databases: '/chronograf/v1/sources/2/dbs',
annotations: '/chronograf/v1/sources/2/annotations',
},
}
export const kapacitor = {
id: '1',
name: 'Test Kapacitor',
url: 'http://localhost:9092',
insecureSkipVerify: false,
active: true,
links: {
self: '/chronograf/v1/sources/47/kapacitors/1',
proxy: '/chronograf/v1/sources/47/kapacitors/1/proxy',
},
}
export const createKapacitorBody = {
name: 'Test Kapacitor',
url: 'http://localhost:9092',
insecureSkipVerify: false,
username: 'user',
password: 'pass',
}
export const updateKapacitorBody = {
name: 'Test Kapacitor',
url: 'http://localhost:9092',
insecureSkipVerify: false,
username: 'user',
password: 'pass',
active: true,
links: {
self: '/chronograf/v1/sources/47/kapacitors/1',
proxy: '/chronograf/v1/sources/47/kapacitors/1/proxy',
},
}

View File

@ -0,0 +1,6 @@
import {kapacitor} from 'mocks/dummy'
export const getKapacitor = jest.fn(() => Promise.resolve(kapacitor))
export const createKapacitor = jest.fn(() => Promise.resolve({data: kapacitor}))
export const updateKapacitor = jest.fn(() => Promise.resolve({data: kapacitor}))
export const pingKapacitor = jest.fn(() => Promise.resolve())

1
ui/mocks/utils/ajax.ts Normal file
View File

@ -0,0 +1 @@
export default jest.fn(() => Promise.resolve())

View File

@ -1,37 +0,0 @@
{
"src_folders": ["tests"],
"output_folder": "reports",
"custom_commands_path": "",
"custom_assertions_path": "",
"page_objects_path": "",
"globals_path": "",
"selenium": {
"start_process": false,
"host": "hub-cloud.browserstack.com",
"port": 80
},
"live_output" : true,
"test_settings": {
"default": {
"selenium_port": 80,
"selenium_host": "hub-cloud.browserstack.com",
"silent": false,
"screenshots": {
"enabled": true,
"path": "screenshots"
},
"desiredCapabilities": {
"browser": "chrome",
"build": "nightwatch-browserstack",
"browserstack.user": "${BROWSERSTACK_USER}",
"browserstack.key": "${BROWSERSTACK_KEY}",
"browserstack.debug": true,
"browserstack.local": true,
"resolution": "1280x1024"
}
}
}
}

View File

@ -1,6 +1,6 @@
{
"name": "chronograf-ui",
"version": "1.4.1-3",
"version": "1.4.2-3",
"private": false,
"license": "AGPL-3.0",
"description": "",
@ -9,18 +9,22 @@
"url": "github:influxdata/chronograf"
},
"scripts": {
"build": "yarn run clean && env NODE_ENV=production webpack --optimize-minimize --config ./webpack/prodConfig.js",
"build:dev": "webpack --config ./webpack/devConfig.js",
"start": "yarn run clean && webpack --watch --config ./webpack/devConfig.js",
"start:hmr": "webpack-dev-server --open --config ./webpack/devConfig.js",
"build": "yarn run clean && webpack --config ./webpack/prod.config.js",
"build:dev": "webpack --config ./webpack/dev.config.js",
"build:vendor": "webpack --config webpack/vendor.config.js",
"start": "yarn run clean && yarn run build:vendor && webpack --watch --config ./webpack/dev.config.js --progress",
"start:fast": "webpack --watch --config ./webpack/dev.config.js",
"start:hmr": "webpack-dev-server --open --config ./webpack/dev.config.js",
"lint": "esw src/",
"test": "karma start",
"test:integration": "nightwatch tests --skip",
"test": "jest",
"test:lint": "yarn run lint; yarn run test",
"test:dev": "concurrently \"yarn run lint --watch\" \"yarn run test --no-single-run --reporters=verbose\"",
"clean": "rm -rf build/*",
"storybook": "node ./storybook.js",
"prettier": "prettier --single-quote --trailing-comma es5 --bracket-spacing false --semi false --write \"{src,spec}/**/*.js\"; eslint src --fix"
"test:watch": "jest --watch",
"clean": "rm -rf ./build/*",
"eslint:fix": "eslint src --fix",
"tslint:fix": "tslint --fix -c ./tslint.json '{src,test}/**/*.ts?(x)'",
"prettier": "prettier --single-quote --trailing-comma es5 --bracket-spacing false --semi false --write \"{src,spec}/**/*.js\"",
"lint:fix": "yarn run prettier && yarn run eslint:fix && yarn run tslint:fix",
"eslint-check": "eslint --print-config .eslintrc | eslint-config-prettier-check"
},
"author": "",
"eslintConfig": {
@ -29,11 +33,18 @@
}
},
"devDependencies": {
"@kadira/storybook": "^2.21.0",
"@types/chai": "^4.1.2",
"@types/enzyme": "^3.1.9",
"@types/jest": "^22.1.4",
"@types/lodash": "^4.14.104",
"@types/node": "^9.4.6",
"@types/prop-types": "^15.5.2",
"@types/react": "^16.0.38",
"autoprefixer": "^6.3.1",
"babel-core": "^6.5.1",
"babel-eslint": "6.1.2",
"babel-loader": "^6.2.2",
"babel-jest": "^22.4.1",
"babel-loader": "^7.1.2",
"babel-plugin-lodash": "^2.0.1",
"babel-plugin-syntax-trailing-function-commas": "^6.5.0",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
@ -41,42 +52,37 @@
"babel-plugin-transform-react-remove-prop-types": "^0.2.1",
"babel-plugin-transform-runtime": "^6.5.0",
"babel-polyfill": "^6.13.0",
"babel-preset-es2015": "^6.5.0",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.5.0",
"babel-preset-stage-0": "^6.16.0",
"babel-runtime": "^6.5.0",
"bower": "^1.7.7",
"chai": "^3.5.0",
"compression-webpack-plugin": "^1.1.8",
"concurrently": "^3.5.0",
"core-js": "^2.1.3",
"css-loader": "^0.23.1",
"envify": "^3.4.0",
"enzyme": "^2.4.1",
"enzyme": "^3.3.0",
"enzyme-adapter-react-15": "^1.0.5",
"eslint": "^3.14.1",
"eslint-loader": "1.6.1",
"eslint-plugin-prettier": "^2.1.2",
"eslint-config-prettier": "^2.9.0",
"eslint-loader": "^2.0.0",
"eslint-plugin-jest": "^21.12.2",
"eslint-plugin-prettier": "^2.6.0",
"eslint-plugin-react": "6.6.0",
"eslint-watch": "^3.1.2",
"express": "^4.14.0",
"extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.8.5",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^1.1.7",
"fork-ts-checker-webpack-plugin": "^0.3.0",
"hanson": "^1.1.1",
"hson-loader": "^1.0.0",
"html-webpack-plugin": "^2.22.0",
"html-webpack-include-assets-plugin": "^1.0.2",
"html-webpack-plugin": "^2.30.1",
"imports-loader": "^0.6.5",
"jest": "^22.4.2",
"jest-runner-eslint": "^0.4.0",
"jest-runner-tslint": "^1.0.3",
"jsdom": "^9.0.0",
"json-loader": "^0.5.4",
"karma": "^1.3.0",
"karma-cli": "^1.0.1",
"karma-mocha": "^1.1.1",
"karma-phantomjs-launcher": "^1.0.2",
"karma-sinon-chai": "^1.2.4",
"karma-sourcemap-loader": "^0.3.7",
"karma-verbose-reporter": "^0.0.6",
"karma-webpack": "^1.8.0",
"mocha": "^2.4.5",
"mocha-loader": "^0.7.1",
"mustache": "^2.2.1",
"json-loader": "^0.5.7",
"node-sass": "^4.5.3",
"on-build-webpack": "^0.1.0",
"postcss-browser-reporter": "^0.4.0",
@ -84,23 +90,31 @@
"postcss-loader": "^0.8.0",
"postcss-reporter": "^1.3.1",
"precss": "^1.4.0",
"prettier": "^1.5.3",
"prettier": "^1.11.1",
"react-addons-test-utils": "^15.0.2",
"resolve-url-loader": "^1.6.0",
"sass-loader": "^3.2.0",
"sinon": "^1.17.4",
"sinon-chai": "^2.8.0",
"react-test-renderer": "^15.6.1",
"resolve-url-loader": "^2.2.1",
"sass-loader": "^6.0.6",
"style-loader": "^0.13.0",
"testem": "^1.2.1",
"uglify-js": "^2.6.1",
"webpack": "^1.13.0",
"webpack-dev-server": "^1.14.1"
"thread-loader": "^1.1.5",
"ts-jest": "^22.4.1",
"ts-loader": "^3.5.0",
"tslib": "^1.9.0",
"tslint": "^5.9.1",
"tslint-config-prettier": "^1.10.0",
"tslint-loader": "^3.6.0",
"tslint-plugin-prettier": "^1.3.0",
"tslint-react": "^3.5.1",
"typescript": "^2.7.2",
"uglifyjs-webpack-plugin": "^1.2.2",
"webpack": "^3.11.0",
"webpack-bundle-analyzer": "^2.10.1",
"webpack-dev-server": "^2.11.1"
},
"dependencies": {
"@skidding/react-codemirror": "^1.0.1",
"axios": "^0.13.1",
"bignumber.js": "^4.0.2",
"bootstrap": "^3.3.7",
"calculate-size": "^1.1.1",
"classnames": "^2.2.3",
"dygraphs": "2.1.0",
@ -108,30 +122,29 @@
"fast.js": "^0.1.1",
"fixed-data-table": "^0.6.1",
"he": "^1.1.1",
"jquery": "^3.1.0",
"lodash": "^4.3.0",
"moment": "^2.13.0",
"nano-date": "^2.0.1",
"node-uuid": "^1.4.7",
"prop-types": "^15.6.1",
"query-string": "^5.0.0",
"react": "^15.0.2",
"react-addons-shallow-compare": "^15.0.2",
"react-codemirror": "^1.0.0",
"react-component-resizable": "^1.1.0-rc1",
"react-custom-scrollbars": "^4.1.1",
"react-dimensions": "^1.2.0",
"react-dom": "^15.0.2",
"react-grid-layout": "^0.13.9",
"react-grid-layout": "^0.16.6",
"react-onclickoutside": "^5.2.0",
"react-redux": "^4.4.0",
"react-resizable": "^1.7.5",
"react-router": "^3.0.2",
"react-router-redux": "^4.0.8",
"react-sparklines": "^1.4.2",
"react-tooltip": "^3.2.1",
"react-virtualized": "^9.18.5",
"redux": "^3.3.1",
"redux-auth-wrapper": "^1.0.0",
"redux-thunk": "^1.0.3",
"rome": "^2.1.22",
"updeep": "^0.13.0"
"uuid": "^3.2.1"
}
}
}

View File

@ -1,3 +0,0 @@
const context = require.context('./', true, /Spec\.js$/)
context.keys().forEach(context)
module.exports = context

View File

@ -1,13 +0,0 @@
window.then = function(cb, done) {
window.setTimeout(function() {
cb()
if (typeof done === 'function') {
done()
}
}, 0)
}
const chai = require('chai')
chai.use(require('sinon-chai'))
global.expect = chai.expect

View File

@ -1,42 +1,21 @@
import React, {PropTypes} from 'react'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import React from 'react'
import PropTypes from 'prop-types'
import SideNav from 'src/side_nav'
import Notifications from 'shared/components/Notifications'
import {publishNotification} from 'shared/actions/notifications'
const App = ({children}) => (
<div className="chronograf-root">
<Notifications />
<SideNav />
{children}
</div>
)
const {func, node} = PropTypes
const {node} = PropTypes
const App = React.createClass({
propTypes: {
children: node.isRequired,
notify: func.isRequired,
},
App.propTypes = {
children: node.isRequired,
}
handleAddFlashMessage({type, text}) {
const {notify} = this.props
notify(type, text)
},
render() {
return (
<div className="chronograf-root">
<Notifications />
<SideNav />
{this.props.children &&
React.cloneElement(this.props.children, {
addFlashMessage: this.handleAddFlashMessage,
})}
</div>
)
},
})
const mapDispatchToProps = dispatch => ({
notify: bindActionCreators(publishNotification, dispatch),
})
export default connect(null, mapDispatchToProps)(App)
export default App

View File

@ -1,4 +1,5 @@
import React, {Component, PropTypes} from 'react'
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {withRouter} from 'react-router'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
@ -14,9 +15,17 @@ import {showDatabases} from 'shared/apis/metaQuery'
import {getSourcesAsync} from 'shared/actions/sources'
import {errorThrown as errorThrownAction} from 'shared/actions/errors'
import {publishNotification} from 'shared/actions/notifications'
import {notify as notifyAction} from 'shared/actions/notifications'
import {DEFAULT_HOME_PAGE} from 'shared/constants'
import {
notifySourceNoLongerAvailable,
notifyNoSourcesAvailable,
notifyUnableToRetrieveSources,
notifyUserRemovedFromAllOrgs,
notifyUserRemovedFromCurrentOrg,
notifyOrgHasNoSources,
} from 'shared/copy/notifications'
// Acts as a 'router middleware'. The main `App` component is responsible for
// getting the list of data nodes, but not every page requires them to function.
@ -84,10 +93,7 @@ class CheckSources extends Component {
}
if (!isFetching && isUsingAuth && !organizations.length) {
notify(
'error',
'You have been removed from all organizations. Please contact your administrator.'
)
notify(notifyUserRemovedFromAllOrgs())
return router.push('/purgatory')
}
@ -95,7 +101,7 @@ class CheckSources extends Component {
me.superAdmin &&
!organizations.find(o => o.id === currentOrganization.id)
) {
notify('error', 'You were removed from your current organization')
notify(notifyUserRemovedFromCurrentOrg())
return router.push('/purgatory')
}
@ -117,7 +123,7 @@ class CheckSources extends Component {
return router.push(`/sources/${sources[0].id}/${restString}`)
}
// if you're a viewer and there are no sources, go to purgatory.
notify('error', 'Organization has no sources configured')
notify(notifyOrgHasNoSources())
return router.push('/purgatory')
}
@ -142,18 +148,12 @@ class CheckSources extends Component {
try {
const newSources = await getSources()
if (newSources.length) {
errorThrown(
error,
`Source ${source.name} is no longer available. Successfully connected to another source.`
)
errorThrown(error, notifySourceNoLongerAvailable(source.name))
} else {
errorThrown(
error,
`Unable to connect to source ${source.name}. No other sources available.`
)
errorThrown(error, notifyNoSourcesAvailable(source.name))
}
} catch (error2) {
errorThrown(error2, 'Unable to retrieve sources')
errorThrown(error2, notifyUnableToRetrieveSources())
}
}
}
@ -247,7 +247,7 @@ const mapStateToProps = ({sources, auth}) => ({
const mapDispatchToProps = dispatch => ({
getSources: bindActionCreators(getSourcesAsync, dispatch),
errorThrown: bindActionCreators(errorThrownAction, dispatch),
notify: bindActionCreators(publishNotification, dispatch),
notify: bindActionCreators(notifyAction, dispatch),
})
export default connect(mapStateToProps, mapDispatchToProps)(

View File

@ -1,5 +1,5 @@
import _ from 'lodash'
import uuid from 'node-uuid'
import uuid from 'uuid'
import {
getUsers as getUsersAJAX,
@ -16,8 +16,14 @@ import {
deleteMapping as deleteMappingAJAX,
} from 'src/admin/apis/chronograf'
import {publishAutoDismissingNotification} from 'shared/dispatchers'
import {notify} from 'shared/actions/notifications'
import {errorThrown} from 'shared/actions/errors'
import {
notifyMappingDeleted,
notifyChronografOrgDeleted,
notifyChronografUserUpdated,
notifyChronografUserDeleted,
} from 'shared/copy/notifications'
import {REVERT_STATE_DELAY} from 'shared/constants'
@ -162,9 +168,9 @@ export const createMappingAsync = (url, mapping) => async dispatch => {
const {data} = await createMappingAJAX(url, mapping)
dispatch(updateMapping(mappingWithTempId, data))
} catch (error) {
const message = `${_.upperFirst(
_.toLower(error.data.message)
)}: Scheme: ${mapping.scheme} Provider: ${mapping.provider}`
const message = `${_.upperFirst(_.toLower(error.data.message))}: Scheme: ${
mapping.scheme
} Provider: ${mapping.provider}`
dispatch(errorThrown(error, message))
setTimeout(
() => dispatch(removeMapping(mappingWithTempId)),
@ -177,12 +183,7 @@ export const deleteMappingAsync = mapping => async dispatch => {
dispatch(removeMapping(mapping))
try {
await deleteMappingAJAX(mapping)
dispatch(
publishAutoDismissingNotification(
'success',
`Mapping deleted: ${mapping.id} ${mapping.scheme}`
)
)
dispatch(notify(notifyMappingDeleted(mapping.id, mapping.scheme)))
} catch (error) {
dispatch(errorThrown(error))
dispatch(addMapping(mapping))
@ -211,9 +212,9 @@ export const createUserAsync = (url, user) => async dispatch => {
const {data} = await createUserAJAX(url, user)
dispatch(syncUser(userWithTempID, data))
} catch (error) {
const message = `${_.upperFirst(
_.toLower(error.data.message)
)}: ${user.scheme}::${user.provider}::${user.name}`
const message = `${_.upperFirst(_.toLower(error.data.message))}: ${
user.scheme
}::${user.provider}::${user.name}`
dispatch(errorThrown(error, message))
// undo optimistic update
setTimeout(() => dispatch(removeUser(userWithTempID)), REVERT_STATE_DELAY)
@ -238,7 +239,7 @@ export const updateUserAsync = (
provider: null,
scheme: null,
})
dispatch(publishAutoDismissingNotification('success', successMessage))
dispatch(notify(notifyChronografUserUpdated(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))
@ -255,14 +256,7 @@ export const deleteUserAsync = (
dispatch(removeUser(user))
try {
await deleteUserAJAX(user)
dispatch(
publishAutoDismissingNotification(
'success',
`${user.name} has been removed from ${isAbsoluteDelete
? 'all organizations and deleted'
: 'the current organization'}`
)
)
dispatch(notify(notifyChronografUserDeleted(user.name, isAbsoluteDelete)))
} catch (error) {
dispatch(errorThrown(error))
dispatch(addUser(user))
@ -281,9 +275,9 @@ export const createOrganizationAsync = (
const {data} = await createOrganizationAJAX(url, organization)
dispatch(syncOrganization(organization, data))
} catch (error) {
const message = `${_.upperFirst(
_.toLower(error.data.message)
)}: ${organization.name}`
const message = `${_.upperFirst(_.toLower(error.data.message))}: ${
organization.name
}`
dispatch(errorThrown(error, message))
// undo optimistic update
setTimeout(
@ -313,12 +307,7 @@ export const deleteOrganizationAsync = organization => async dispatch => {
dispatch(removeOrganization(organization))
try {
await deleteOrganizationAJAX(organization)
dispatch(
publishAutoDismissingNotification(
'success',
`Organization deleted: ${organization.name}`
)
)
dispatch(notify(notifyChronografOrgDeleted(organization.name)))
} catch (error) {
dispatch(errorThrown(error))
dispatch(addOrganization(organization))

View File

@ -18,9 +18,40 @@ import {
import {killQuery as killQueryProxy} from 'shared/apis/metaQuery'
import {publishAutoDismissingNotification} from 'shared/dispatchers'
import {notify} from 'shared/actions/notifications'
import {errorThrown} from 'shared/actions/errors'
import {
notifyDBUserCreated,
notifyDBUserCreationFailed,
notifyDBUserDeleted,
notifyDBUserDeleteFailed,
notifyDBUserPermissionsUpdated,
notifyDBUserPermissionsUpdateFailed,
notifyDBUserRolesUpdated,
notifyDBUserRolesUpdateFailed,
notifyDBUserPasswordUpdated,
notifyDBUserPasswordUpdateFailed,
notifyDatabaseCreated,
notifyDBCreationFailed,
notifyDBDeleted,
notifyDBDeleteFailed,
notifyRoleCreated,
notifyRoleCreationFailed,
notifyRoleDeleted,
notifyRoleDeleteFailed,
notifyRoleUsersUpdated,
notifyRoleUsersUpdateFailed,
notifyRolePermissionsUpdated,
notifyRolePermissionsUpdateFailed,
notifyRetentionPolicyCreated,
notifyRetentionPolicyCreationFailed,
notifyRetentionPolicyDeleted,
notifyRetentionPolicyDeleteFailed,
notifyRetentionPolicyUpdated,
notifyRetentionPolicyUpdateFailed,
} from 'shared/copy/notifications'
import {REVERT_STATE_DELAY} from 'shared/constants'
import _ from 'lodash'
@ -276,12 +307,10 @@ export const loadDBsAndRPsAsync = url => async dispatch => {
export const createUserAsync = (url, user) => async dispatch => {
try {
const {data} = await createUserAJAX(url, user)
dispatch(
publishAutoDismissingNotification('success', 'User created successfully')
)
dispatch(notify(notifyDBUserCreated()))
dispatch(syncUser(user, data))
} catch (error) {
dispatch(errorThrown(error, `Failed to create user: ${error.data.message}`))
dispatch(errorThrown(error, notifyDBUserCreationFailed(error.data.message)))
// undo optimistic update
setTimeout(() => dispatch(deleteUser(user)), REVERT_STATE_DELAY)
}
@ -290,12 +319,10 @@ export const createUserAsync = (url, user) => async dispatch => {
export const createRoleAsync = (url, role) => async dispatch => {
try {
const {data} = await createRoleAJAX(url, role)
dispatch(
publishAutoDismissingNotification('success', 'Role created successfully')
)
dispatch(notify(notifyRoleCreated()))
dispatch(syncRole(role, data))
} catch (error) {
dispatch(errorThrown(error, `Failed to create role: ${error.data.message}`))
dispatch(errorThrown(error, notifyRoleCreationFailed(error.data.message)))
// undo optimistic update
setTimeout(() => dispatch(deleteRole(role)), REVERT_STATE_DELAY)
}
@ -305,16 +332,9 @@ export const createDatabaseAsync = (url, database) => async dispatch => {
try {
const {data} = await createDatabaseAJAX(url, database)
dispatch(syncDatabase(database, data))
dispatch(
publishAutoDismissingNotification(
'success',
'Database created successfully'
)
)
dispatch(notify(notifyDatabaseCreated()))
} catch (error) {
dispatch(
errorThrown(error, `Failed to create database: ${error.data.message}`)
)
dispatch(errorThrown(error, notifyDBCreationFailed(error.data.message)))
// undo optimistic update
setTimeout(() => dispatch(removeDatabase(database)), REVERT_STATE_DELAY)
}
@ -329,19 +349,11 @@ export const createRetentionPolicyAsync = (
database.links.retentionPolicies,
retentionPolicy
)
dispatch(
publishAutoDismissingNotification(
'success',
'Retention policy created successfully'
)
)
dispatch(notify(notifyRetentionPolicyCreated()))
dispatch(syncRetentionPolicy(database, retentionPolicy, data))
} catch (error) {
dispatch(
errorThrown(
error,
`Failed to create retention policy: ${error.data.message}`
)
errorThrown(notifyRetentionPolicyCreationFailed(error.data.message))
)
// undo optimistic update
setTimeout(
@ -360,19 +372,11 @@ export const updateRetentionPolicyAsync = (
dispatch(editRetentionPolicyRequested(database, oldRP, newRP))
const {data} = await updateRetentionPolicyAJAX(oldRP.links.self, newRP)
dispatch(editRetentionPolicyCompleted(database, oldRP, data))
dispatch(
publishAutoDismissingNotification(
'success',
'Retention policy updated successfully'
)
)
dispatch(notify(notifyRetentionPolicyUpdated()))
} catch (error) {
dispatch(editRetentionPolicyFailed(database, oldRP))
dispatch(
errorThrown(
error,
`Failed to update retention policy: ${error.data.message}`
)
errorThrown(error, notifyRetentionPolicyUpdateFailed(error.data.message))
)
}
}
@ -394,9 +398,9 @@ export const deleteRoleAsync = role => async dispatch => {
dispatch(deleteRole(role))
try {
await deleteRoleAJAX(role.links.self)
dispatch(publishAutoDismissingNotification('success', 'Role deleted'))
dispatch(notify(notifyRoleDeleted(role.name)))
} catch (error) {
dispatch(errorThrown(error, `Failed to delete role: ${error.data.message}`))
dispatch(errorThrown(error, notifyRoleDeleteFailed(error.data.message)))
}
}
@ -404,9 +408,9 @@ export const deleteUserAsync = user => async dispatch => {
dispatch(deleteUser(user))
try {
await deleteUserAJAX(user.links.self)
dispatch(publishAutoDismissingNotification('success', 'User deleted'))
dispatch(notify(notifyDBUserDeleted(user.name)))
} catch (error) {
dispatch(errorThrown(error, `Failed to delete user: ${error.data.message}`))
dispatch(errorThrown(error, notifyDBUserDeleteFailed(error.data.message)))
}
}
@ -414,11 +418,9 @@ export const deleteDatabaseAsync = database => async dispatch => {
dispatch(removeDatabase(database))
try {
await deleteDatabaseAJAX(database.links.self)
dispatch(publishAutoDismissingNotification('success', 'Database deleted'))
dispatch(notify(notifyDBDeleted(database.name)))
} catch (error) {
dispatch(
errorThrown(error, `Failed to delete database: ${error.data.message}`)
)
dispatch(errorThrown(error, notifyDBDeleteFailed(error.data.message)))
}
}
@ -429,18 +431,10 @@ export const deleteRetentionPolicyAsync = (
dispatch(removeRetentionPolicy(database, retentionPolicy))
try {
await deleteRetentionPolicyAJAX(retentionPolicy.links.self)
dispatch(
publishAutoDismissingNotification(
'success',
`Retention policy ${retentionPolicy.name} deleted`
)
)
dispatch(notify(notifyRetentionPolicyDeleted(retentionPolicy.name)))
} catch (error) {
dispatch(
errorThrown(
error,
`Failed to delete retentionPolicy: ${error.data.message}`
)
errorThrown(error, notifyRetentionPolicyDeleteFailed(error.data.message))
)
}
}
@ -452,10 +446,12 @@ export const updateRoleUsersAsync = (role, users) => async dispatch => {
users,
role.permissions
)
dispatch(publishAutoDismissingNotification('success', 'Role users updated'))
dispatch(notify(notifyRoleUsersUpdated()))
dispatch(syncRole(role, data))
} catch (error) {
dispatch(errorThrown(error, `Failed to update role: ${error.data.message}`))
dispatch(
errorThrown(error, notifyRoleUsersUpdateFailed(error.data.message))
)
}
}
@ -469,13 +465,11 @@ export const updateRolePermissionsAsync = (
role.users,
permissions
)
dispatch(
publishAutoDismissingNotification('success', 'Role permissions updated')
)
dispatch(notify(notifyRolePermissionsUpdated()))
dispatch(syncRole(role, data))
} catch (error) {
dispatch(
errorThrown(error, `Failed to update role: ${error.data.message}`)
errorThrown(error, notifyRolePermissionsUpdateFailed(error.data.message))
)
}
}
@ -486,13 +480,14 @@ export const updateUserPermissionsAsync = (
) => async dispatch => {
try {
const {data} = await updateUserAJAX(user.links.self, {permissions})
dispatch(
publishAutoDismissingNotification('success', 'User permissions updated')
)
dispatch(notify(notifyDBUserPermissionsUpdated()))
dispatch(syncUser(user, data))
} catch (error) {
dispatch(
errorThrown(error, `Failed to update user: ${error.data.message}`)
errorThrown(
error,
notifyDBUserPermissionsUpdateFailed(error.data.message)
)
)
}
}
@ -500,11 +495,11 @@ export const updateUserPermissionsAsync = (
export const updateUserRolesAsync = (user, roles) => async dispatch => {
try {
const {data} = await updateUserAJAX(user.links.self, {roles})
dispatch(publishAutoDismissingNotification('success', 'User roles updated'))
dispatch(notify(notifyDBUserRolesUpdated()))
dispatch(syncUser(user, data))
} catch (error) {
dispatch(
errorThrown(error, `Failed to update user: ${error.data.message}`)
errorThrown(error, notifyDBUserRolesUpdateFailed(error.data.message))
)
}
}
@ -512,13 +507,11 @@ export const updateUserRolesAsync = (user, roles) => async dispatch => {
export const updateUserPasswordAsync = (user, password) => async dispatch => {
try {
const {data} = await updateUserAJAX(user.links.self, {password})
dispatch(
publishAutoDismissingNotification('success', 'User password updated')
)
dispatch(notify(notifyDBUserPasswordUpdated()))
dispatch(syncUser(user, data))
} catch (error) {
dispatch(
errorThrown(error, `Failed to update user: ${error.data.message}`)
errorThrown(error, notifyDBUserPasswordUpdateFailed(error.data.message))
)
}
}

View File

@ -1,4 +1,5 @@
import React, {PropTypes} from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import {Tab, Tabs, TabPanel, TabPanels, TabList} from 'shared/components/Tabs'
import UsersTable from 'src/admin/components/UsersTable'
import RolesTable from 'src/admin/components/RolesTable'
@ -88,18 +89,12 @@ const AdminTabs = ({
return (
<Tabs className="row">
<TabList customClass="col-md-2 admin-tabs">
{tabs.map((t, i) =>
<Tab key={tabs[i].type}>
{tabs[i].type}
</Tab>
)}
{tabs.map((t, i) => <Tab key={tabs[i].type}>{tabs[i].type}</Tab>)}
</TabList>
<TabPanels customClass="col-md-10 admin-tabs--content">
{tabs.map((t, i) =>
<TabPanel key={tabs[i].type}>
{t.component}
</TabPanel>
)}
{tabs.map((t, i) => (
<TabPanel key={tabs[i].type}>{t.component}</TabPanel>
))}
</TabPanels>
</Tabs>
)

View File

@ -1,4 +1,5 @@
import React, {Component, PropTypes} from 'react'
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import OnClickOutside from 'shared/components/OnClickOutside'
import ConfirmButtons from 'shared/components/ConfirmButtons'
@ -9,18 +10,13 @@ class ChangePassRow extends Component {
this.state = {
showForm: false,
}
this.showForm = ::this.showForm
this.handleCancel = ::this.handleCancel
this.handleKeyPress = ::this.handleKeyPress
this.handleEdit = ::this.handleEdit
this.handleSubmit = ::this.handleSubmit
}
showForm() {
showForm = () => {
this.setState({showForm: true})
}
handleCancel() {
handleCancel = () => {
this.setState({showForm: false})
}
@ -28,12 +24,12 @@ class ChangePassRow extends Component {
this.setState({showForm: false})
}
handleSubmit(user) {
handleSubmit = user => {
this.props.onApply(user)
this.setState({showForm: false})
}
handleKeyPress(user) {
handleKeyPress = user => {
return e => {
if (e.key === 'Enter') {
this.handleSubmit(user)
@ -41,7 +37,7 @@ class ChangePassRow extends Component {
}
}
handleEdit(user) {
handleEdit = user => {
return e => {
this.props.onEdit(user, {[e.target.name]: e.target.value})
}

View File

@ -1,10 +1,10 @@
import React, {PropTypes} from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import DatabaseTable from 'src/admin/components/DatabaseTable'
const DatabaseManager = ({
databases,
notify,
isRFDisplayed,
isAddDBDisabled,
addDatabase,
@ -25,8 +25,8 @@ const DatabaseManager = ({
onDeleteRetentionPolicy,
}) => {
return (
<div className="panel panel-default">
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
<div className="panel panel-solid">
<div className="panel-heading">
<h2 className="panel-title">
{databases.length === 1
? '1 Database'
@ -41,11 +41,10 @@ const DatabaseManager = ({
</button>
</div>
<div className="panel-body">
{databases.map(db =>
{databases.map(db => (
<DatabaseTable
key={db.links.self}
database={db}
notify={notify}
isRFDisplayed={isRFDisplayed}
onEditDatabase={onEditDatabase}
onKeyDownDatabase={onKeyDownDatabase}
@ -63,7 +62,7 @@ const DatabaseManager = ({
onRemoveRetentionPolicy={onRemoveRetentionPolicy}
onDeleteRetentionPolicy={onDeleteRetentionPolicy}
/>
)}
))}
</div>
</div>
)
@ -73,7 +72,6 @@ const {arrayOf, bool, func, shape} = PropTypes
DatabaseManager.propTypes = {
databases: arrayOf(shape()),
notify: func,
addDatabase: func,
isRFDisplayed: bool,
isAddDBDisabled: bool,

View File

@ -1,9 +1,16 @@
import React, {PropTypes, Component} from 'react'
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import onClickOutside from 'react-onclickoutside'
import {notify as notifyAction} from 'shared/actions/notifications'
import {formatRPDuration} from 'utils/formatting'
import YesNoButtons from 'shared/components/YesNoButtons'
import {DATABASE_TABLE} from 'src/admin/constants/tableSizing'
import {notifyRetentionPolicyCantHaveEmptyFields} from 'shared/copy/notifications'
class DatabaseRow extends Component {
constructor(props) {
@ -109,7 +116,7 @@ class DatabaseRow extends Component {
const replication = isRFDisplayed ? +this.replication.value.trim() : 1
if (!duration || (isRFDisplayed && !replication)) {
notify('error', 'Fields cannot be empty')
notify(notifyRetentionPolicyCantHaveEmptyFields())
return
}
@ -141,19 +148,21 @@ class DatabaseRow extends Component {
return (
<tr>
<td style={{width: `${DATABASE_TABLE.colRetentionPolicy}px`}}>
{isNew
? <input
className="form-control input-xs"
type="text"
defaultValue={name}
placeholder="Name this RP"
onKeyDown={this.handleKeyDown}
ref={r => (this.name = r)}
autoFocus={true}
spellCheck={false}
autoComplete={false}
/>
: name}
{isNew ? (
<input
className="form-control input-xs"
type="text"
defaultValue={name}
placeholder="Name this RP"
onKeyDown={this.handleKeyDown}
ref={r => (this.name = r)}
autoFocus={true}
spellCheck={false}
autoComplete={false}
/>
) : (
name
)}
</td>
<td style={{width: `${DATABASE_TABLE.colDuration}px`}}>
<input
@ -169,22 +178,22 @@ class DatabaseRow extends Component {
autoComplete={false}
/>
</td>
{isRFDisplayed
? <td style={{width: `${DATABASE_TABLE.colReplication}px`}}>
<input
className="form-control input-xs"
name="name"
type="number"
min="1"
defaultValue={replication || 1}
placeholder="# of Nodes"
onKeyDown={this.handleKeyDown}
ref={r => (this.replication = r)}
spellCheck={false}
autoComplete={false}
/>
</td>
: null}
{isRFDisplayed ? (
<td style={{width: `${DATABASE_TABLE.colReplication}px`}}>
<input
className="form-control input-xs"
name="name"
type="number"
min="1"
defaultValue={replication || 1}
placeholder="# of Nodes"
onKeyDown={this.handleKeyDown}
ref={r => (this.replication = r)}
spellCheck={false}
autoComplete={false}
/>
</td>
) : null}
<td
className="text-right"
style={{width: `${DATABASE_TABLE.colDelete}px`}}
@ -203,9 +212,9 @@ class DatabaseRow extends Component {
<tr>
<td>
{`${name} `}
{isDefault
? <span className="default-source-label">default</span>
: null}
{isDefault ? (
<span className="default-source-label">default</span>
) : null}
</td>
<td
onClick={this.handleStartEdit}
@ -213,31 +222,33 @@ class DatabaseRow extends Component {
>
{formattedDuration}
</td>
{isRFDisplayed
? <td
onClick={this.handleStartEdit}
style={{width: `${DATABASE_TABLE.colReplication}px`}}
>
{replication}
</td>
: null}
{isRFDisplayed ? (
<td
onClick={this.handleStartEdit}
style={{width: `${DATABASE_TABLE.colReplication}px`}}
>
{replication}
</td>
) : null}
<td
className="text-right"
style={{width: `${DATABASE_TABLE.colDelete}px`}}
>
{isDeleting
? <YesNoButtons
onConfirm={onDelete(database, retentionPolicy)}
onCancel={this.handleEndDelete}
buttonSize="btn-xs"
/>
: <button
className="btn btn-danger btn-xs table--show-on-row-hover"
style={isDeletable ? {} : {visibility: 'hidden'}}
onClick={this.handleStartDelete}
>
{`Delete ${name}`}
</button>}
{isDeleting ? (
<YesNoButtons
onConfirm={onDelete(database, retentionPolicy)}
onCancel={this.handleEndDelete}
buttonSize="btn-xs"
/>
) : (
<button
className="btn btn-danger btn-xs table--show-on-row-hover"
style={isDeletable ? {} : {visibility: 'hidden'}}
onClick={this.handleStartDelete}
>
{`Delete ${name}`}
</button>
)}
</td>
</tr>
)
@ -260,8 +271,12 @@ DatabaseRow.propTypes = {
onCreate: func,
onUpdate: func,
onDelete: func,
notify: func,
notify: func.isRequired,
isRFDisplayed: bool,
}
export default onClickOutside(DatabaseRow)
const mapDispatchToProps = dispatch => ({
notify: bindActionCreators(notifyAction, dispatch),
})
export default connect(null, mapDispatchToProps)(onClickOutside(DatabaseRow))

View File

@ -1,4 +1,5 @@
import React, {PropTypes} from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import _ from 'lodash'
import classnames from 'classnames'
@ -11,7 +12,6 @@ const {func, shape, bool} = PropTypes
const DatabaseTable = ({
database,
notify,
isRFDisplayed,
onEditDatabase,
onKeyDownDatabase,
@ -35,7 +35,6 @@ const DatabaseTable = ({
>
<DatabaseTableHeader
database={database}
notify={notify}
onEdit={onEditDatabase}
onCancel={onCancelDatabase}
onDelete={onDeleteDatabase}
@ -58,11 +57,11 @@ const DatabaseTable = ({
<th style={{width: `${DATABASE_TABLE.colDuration}px`}}>
Duration
</th>
{isRFDisplayed
? <th style={{width: `${DATABASE_TABLE.colReplication}px`}}>
Replication Factor
</th>
: null}
{isRFDisplayed ? (
<th style={{width: `${DATABASE_TABLE.colReplication}px`}}>
Replication Factor
</th>
) : null}
<th style={{width: `${DATABASE_TABLE.colDelete}px`}} />
</tr>
</thead>
@ -73,7 +72,6 @@ const DatabaseTable = ({
return (
<DatabaseRow
key={rp.links.self}
notify={notify}
database={database}
retentionPolicy={rp}
onCreate={onCreateRetentionPolicy}
@ -95,7 +93,6 @@ const DatabaseTable = ({
DatabaseTable.propTypes = {
onEditDatabase: func,
database: shape(),
notify: func,
isRFDisplayed: bool,
isAddRPDisabled: bool,
onKeyDownDatabase: func,

View File

@ -1,5 +1,12 @@
import React, {PropTypes} from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import {notify as notifyAction} from 'shared/actions/notifications'
import ConfirmButtons from 'shared/components/ConfirmButtons'
import {notifyDatabaseDeleteConfirmationRequired} from 'shared/copy/notifications'
const DatabaseTableHeader = ({
database,
@ -53,7 +60,7 @@ const Header = ({
onDatabaseDeleteConfirm,
}) => {
const buttons = (
<div className="text-right db-manager-header--actions">
<div className="db-manager-header--actions text-right">
<button
className="btn btn-xs btn-primary"
disabled={isAddRPDisabled}
@ -61,20 +68,20 @@ const Header = ({
>
<span className="icon plus" /> Add Retention Policy
</button>
{database.name === '_internal'
? null
: <button
className="btn btn-xs btn-danger"
onClick={onStartDelete(database)}
>
Delete
</button>}
{database.name === '_internal' ? null : (
<button
className="btn btn-xs btn-danger"
onClick={onStartDelete(database)}
>
Delete
</button>
)}
</div>
)
const onConfirm = db => {
function onConfirm(db) {
if (database.deleteCode !== `DELETE ${database.name}`) {
return notify('error', `Type DELETE ${database.name} to confirm`)
return notify(notifyDatabaseDeleteConfirmationRequired(database.name))
}
onDelete(db)
@ -105,15 +112,13 @@ const Header = ({
return (
<div className="db-manager-header">
<h4>
{database.name}
</h4>
<h4>{database.name}</h4>
{database.hasOwnProperty('deleteCode') ? deleteConfirmation : buttons}
</div>
)
}
const EditHeader = ({database, onEdit, onKeyDown, onConfirm, onCancel}) =>
const EditHeader = ({database, onEdit, onKeyDown, onConfirm, onCancel}) => (
<div className="db-manager-header db-manager-header--edit">
<input
className="form-control input-sm"
@ -129,12 +134,13 @@ const EditHeader = ({database, onEdit, onKeyDown, onConfirm, onCancel}) =>
/>
<ConfirmButtons item={database} onConfirm={onConfirm} onCancel={onCancel} />
</div>
)
const {func, shape, bool} = PropTypes
DatabaseTableHeader.propTypes = {
onEdit: func,
notify: func,
notify: func.isRequired,
database: shape(),
onKeyDown: func,
onCancel: func,
@ -148,7 +154,7 @@ DatabaseTableHeader.propTypes = {
}
Header.propTypes = {
notify: func,
notify: func.isRequired,
onConfirm: func,
onCancel: func,
onDelete: func,
@ -168,4 +174,8 @@ EditHeader.propTypes = {
isRFDisplayed: bool,
}
export default DatabaseTableHeader
const mapDispatchToProps = dispatch => ({
notify: bindActionCreators(notifyAction, dispatch),
})
export default connect(null, mapDispatchToProps)(DatabaseTableHeader)

View File

@ -1,6 +1,7 @@
import React, {PropTypes} from 'react'
import React from 'react'
import PropTypes from 'prop-types'
const EmptyRow = ({tableName}) =>
const EmptyRow = ({tableName}) => (
<tr className="table-empty-state">
<th colSpan="5">
<p>
@ -8,6 +9,7 @@ const EmptyRow = ({tableName}) =>
</p>
</th>
</tr>
)
const {string} = PropTypes

View File

@ -1,4 +1,5 @@
import React, {Component, PropTypes} from 'react'
import React, {Component} from 'react'
import PropTypes from 'prop-types'
class FilterBar extends Component {
constructor(props) {
@ -26,8 +27,8 @@ class FilterBar extends Component {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()
})
return (
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
<div className="users__search-widget input-group admin__search-widget">
<div className="panel-heading">
<div className="search-widget" style={{width: '300px'}}>
<input
type="text"
className="form-control input-sm"
@ -35,9 +36,7 @@ class FilterBar extends Component {
value={this.state.filterText}
onChange={this.handleText}
/>
<div className="input-group-addon">
<span className="icon search" aria-hidden="true" />
</div>
<span className="icon search" />
</div>
<button
className="btn btn-sm btn-primary"

View File

@ -1,11 +1,12 @@
import React, {PropTypes} from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import QueryRow from 'src/admin/components/QueryRow'
import {QUERIES_TABLE} from 'src/admin/constants/tableSizing'
const QueriesTable = ({queries, onKillQuery}) =>
const QueriesTable = ({queries, onKillQuery}) => (
<div>
<div className="panel panel-default">
<div className="panel panel-solid">
<div className="panel-body">
<table className="table v-center admin-table table-highlight">
<thead>
@ -19,14 +20,15 @@ const QueriesTable = ({queries, onKillQuery}) =>
</tr>
</thead>
<tbody>
{queries.map(q =>
{queries.map(q => (
<QueryRow key={q.id} query={q} onKill={onKillQuery} />
)}
))}
</tbody>
</table>
</div>
</div>
</div>
)
const {arrayOf, func, shape} = PropTypes

View File

@ -1,4 +1,5 @@
import React, {PropTypes, Component} from 'react'
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import ConfirmButtons from 'shared/components/ConfirmButtons'
import {QUERIES_TABLE} from 'src/admin/constants/tableSizing'
@ -7,24 +8,20 @@ class QueryRow extends Component {
constructor(props) {
super(props)
this.handleInitiateKill = ::this.handleInitiateKill
this.handleFinishHim = ::this.handleFinishHim
this.handleShowMercy = ::this.handleShowMercy
this.state = {
confirmingKill: false,
}
}
handleInitiateKill() {
handleInitiateKill = () => {
this.setState({confirmingKill: true})
}
handleFinishHim() {
handleFinishHim = () => {
this.props.onKill(this.props.query.id)
}
handleShowMercy() {
handleShowMercy = () => {
this.setState({confirmingKill: false})
}
@ -40,9 +37,7 @@ class QueryRow extends Component {
{database}
</td>
<td>
<code>
{query}
</code>
<code>{query}</code>
</td>
<td
style={{width: `${QUERIES_TABLE.colRunning}px`}}
@ -54,18 +49,20 @@ class QueryRow extends Component {
style={{width: `${QUERIES_TABLE.colKillQuery}px`}}
className="text-right"
>
{this.state.confirmingKill
? <ConfirmButtons
onConfirm={this.handleFinishHim}
onCancel={this.handleShowMercy}
buttonSize="btn-xs"
/>
: <button
className="btn btn-xs btn-danger table--show-on-row-hover"
onClick={this.handleInitiateKill}
>
Kill
</button>}
{this.state.confirmingKill ? (
<ConfirmButtons
onConfirm={this.handleFinishHim}
onCancel={this.handleShowMercy}
buttonSize="btn-xs"
/>
) : (
<button
className="btn btn-xs btn-danger table--show-on-row-hover"
onClick={this.handleInitiateKill}
>
Kill
</button>
)}
</td>
</tr>
)

View File

@ -1,16 +1,14 @@
import React, {Component, PropTypes} from 'react'
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {ROLES_TABLE} from 'src/admin/constants/tableSizing'
class RoleEditingRow extends Component {
constructor(props) {
super(props)
this.handleKeyPress = ::this.handleKeyPress
this.handleEdit = ::this.handleEdit
}
handleKeyPress(role) {
handleKeyPress = role => {
return e => {
if (e.key === 'Enter') {
this.props.onSave(role)
@ -18,7 +16,7 @@ class RoleEditingRow extends Component {
}
}
handleEdit(role) {
handleEdit = role => {
return e => {
this.props.onEdit(role, {[e.target.name]: e.target.value})
}

View File

@ -1,4 +1,5 @@
import React, {PropTypes} from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import _ from 'lodash'
import classnames from 'classnames'
@ -23,11 +24,11 @@ const RoleRow = ({
onUpdateRoleUsers,
onUpdateRolePermissions,
}) => {
const handleUpdateUsers = usrs => {
function handleUpdateUsers(usrs) {
onUpdateRoleUsers(role, usrs)
}
const handleUpdatePermissions = allowed => {
function handleUpdatePermissions(allowed) {
onUpdateRolePermissions(role, [
{scope: 'all', allowed: allowed.map(({name}) => name)},
])
@ -63,41 +64,38 @@ const RoleRow = ({
return (
<tr>
<td style={{width: `${ROLES_TABLE.colName}px`}}>
{roleName}
<td style={{width: `${ROLES_TABLE.colName}px`}}>{roleName}</td>
<td>
{allPermissions && allPermissions.length ? (
<MultiSelectDropdown
items={allPermissions.map(name => ({name}))}
selectedItems={perms.map(name => ({name}))}
label={perms.length ? '' : 'Select Permissions'}
onApply={handleUpdatePermissions}
buttonSize="btn-xs"
buttonColor="btn-primary"
customClass={classnames(`dropdown-${ROLES_TABLE.colPermissions}`, {
'admin-table--multi-select-empty': !permissions.length,
})}
resetStateOnReceiveProps={false}
/>
) : null}
</td>
<td>
{allPermissions && allPermissions.length
? <MultiSelectDropdown
items={allPermissions.map(name => ({name}))}
selectedItems={perms.map(name => ({name}))}
label={perms.length ? '' : 'Select Permissions'}
onApply={handleUpdatePermissions}
buttonSize="btn-xs"
buttonColor="btn-primary"
customClass={classnames(
`dropdown-${ROLES_TABLE.colPermissions}`,
{
'admin-table--multi-select-empty': !permissions.length,
}
)}
/>
: null}
</td>
<td>
{allUsers && allUsers.length
? <MultiSelectDropdown
items={allUsers}
selectedItems={users}
label={users.length ? '' : 'Select Users'}
onApply={handleUpdateUsers}
buttonSize="btn-xs"
buttonColor="btn-primary"
customClass={classnames(`dropdown-${ROLES_TABLE.colUsers}`, {
'admin-table--multi-select-empty': !users.length,
})}
/>
: null}
{allUsers && allUsers.length ? (
<MultiSelectDropdown
items={allUsers}
selectedItems={users}
label={users.length ? '' : 'Select Users'}
onApply={handleUpdateUsers}
buttonSize="btn-xs"
buttonColor="btn-primary"
customClass={classnames(`dropdown-${ROLES_TABLE.colUsers}`, {
'admin-table--multi-select-empty': !users.length,
})}
resetStateOnReceiveProps={false}
/>
) : null}
</td>
<DeleteConfirmTableCell
onDelete={onDelete}

View File

@ -1,4 +1,5 @@
import React, {PropTypes} from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import RoleRow from 'src/admin/components/RoleRow'
import EmptyRow from 'src/admin/components/EmptyRow'
import FilterBar from 'src/admin/components/FilterBar'
@ -16,8 +17,8 @@ const RolesTable = ({
onFilter,
onUpdateRoleUsers,
onUpdateRolePermissions,
}) =>
<div className="panel panel-default">
}) => (
<div className="panel panel-solid">
<FilterBar
type="roles"
onFilter={onFilter}
@ -35,30 +36,33 @@ const RolesTable = ({
</tr>
</thead>
<tbody>
{roles.length
? roles
.filter(r => !r.hidden)
.map(role =>
<RoleRow
key={role.links.self}
allUsers={allUsers}
allPermissions={permissions}
role={role}
onEdit={onEdit}
onSave={onSave}
onCancel={onCancel}
onDelete={onDelete}
onUpdateRoleUsers={onUpdateRoleUsers}
onUpdateRolePermissions={onUpdateRolePermissions}
isEditing={role.isEditing}
isNew={role.isNew}
/>
)
: <EmptyRow tableName={'Roles'} />}
{roles.length ? (
roles
.filter(r => !r.hidden)
.map(role => (
<RoleRow
key={role.links.self}
allUsers={allUsers}
allPermissions={permissions}
role={role}
onEdit={onEdit}
onSave={onSave}
onCancel={onCancel}
onDelete={onDelete}
onUpdateRoleUsers={onUpdateRoleUsers}
onUpdateRolePermissions={onUpdateRolePermissions}
isEditing={role.isEditing}
isNew={role.isNew}
/>
))
) : (
<EmptyRow tableName={'Roles'} />
)}
</tbody>
</table>
</div>
</div>
)
const {arrayOf, bool, func, shape, string} = PropTypes

View File

@ -1,16 +1,14 @@
import React, {Component, PropTypes} from 'react'
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {USERS_TABLE} from 'src/admin/constants/tableSizing'
class UserEditName extends Component {
constructor(props) {
super(props)
this.handleKeyPress = ::this.handleKeyPress
this.handleEdit = ::this.handleEdit
}
handleKeyPress(user) {
handleKeyPress = user => {
return e => {
if (e.key === 'Enter') {
this.props.onSave(user)
@ -18,7 +16,7 @@ class UserEditName extends Component {
}
}
handleEdit(user) {
handleEdit = user => {
return e => {
this.props.onEdit(user, {[e.target.name]: e.target.value})
}

View File

@ -1,16 +1,10 @@
import React, {Component, PropTypes} from 'react'
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {USERS_TABLE} from 'src/admin/constants/tableSizing'
class UserNewPassword extends Component {
constructor(props) {
super(props)
this.handleKeyPress = ::this.handleKeyPress
this.handleEdit = ::this.handleEdit
}
handleKeyPress(user) {
handleKeyPress = user => {
return e => {
if (e.key === 'Enter') {
this.props.onSave(user)
@ -18,7 +12,7 @@ class UserNewPassword extends Component {
}
}
handleEdit(user) {
handleEdit = user => {
return e => {
this.props.onEdit(user, {[e.target.name]: e.target.value})
}
@ -28,19 +22,21 @@ class UserNewPassword extends Component {
const {user, isNew} = this.props
return (
<td style={{width: `${USERS_TABLE.colPassword}px`}}>
{isNew
? <input
className="form-control input-xs"
name="password"
type="password"
value={user.password || ''}
placeholder="Password"
onChange={this.handleEdit(user)}
onKeyPress={this.handleKeyPress(user)}
spellCheck={false}
autoComplete={false}
/>
: '--'}
{isNew ? (
<input
className="form-control input-xs"
name="password"
type="password"
value={user.password || ''}
placeholder="Password"
onChange={this.handleEdit(user)}
onKeyPress={this.handleKeyPress(user)}
spellCheck={false}
autoComplete={false}
/>
) : (
'--'
)}
</td>
)
}

View File

@ -1,4 +1,5 @@
import React, {PropTypes} from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import _ from 'lodash'
import classnames from 'classnames'
@ -27,19 +28,19 @@ const UserRow = ({
onUpdateRoles,
onUpdatePassword,
}) => {
const handleUpdatePermissions = perms => {
function handleUpdatePermissions(perms) {
const allowed = perms.map(p => p.name)
onUpdatePermissions(user, [{scope: 'all', allowed}])
}
const handleUpdateRoles = roleNames => {
function handleUpdateRoles(roleNames) {
onUpdateRoles(
user,
allRoles.filter(r => roleNames.find(rn => rn.name === r.name))
)
}
const handleUpdatePassword = () => {
function handleUpdatePassword() {
onUpdatePassword(user, password)
}
@ -74,9 +75,7 @@ const UserRow = ({
return (
<tr>
<td style={{width: `${USERS_TABLE.colUsername}px`}}>
{name}
</td>
<td style={{width: `${USERS_TABLE.colUsername}px`}}>{name}</td>
<td style={{width: `${USERS_TABLE.colPassword}px`}}>
<ChangePassRow
onEdit={onEdit}
@ -85,40 +84,39 @@ const UserRow = ({
buttonSize="btn-xs"
/>
</td>
{hasRoles
? <td>
<MultiSelectDropdown
items={allRoles}
selectedItems={roles.map(r => ({name: r.name}))}
label={roles.length ? '' : 'Select Roles'}
onApply={handleUpdateRoles}
buttonSize="btn-xs"
buttonColor="btn-primary"
customClass={classnames(`dropdown-${USERS_TABLE.colRoles}`, {
'admin-table--multi-select-empty': !roles.length,
})}
/>
</td>
: null}
{hasRoles ? (
<td>
<MultiSelectDropdown
items={allRoles}
selectedItems={roles.map(r => ({name: r.name}))}
label={roles.length ? '' : 'Select Roles'}
onApply={handleUpdateRoles}
buttonSize="btn-xs"
buttonColor="btn-primary"
customClass={classnames(`dropdown-${USERS_TABLE.colRoles}`, {
'admin-table--multi-select-empty': !roles.length,
})}
resetStateOnReceiveProps={false}
/>
</td>
) : null}
<td>
{allPermissions && allPermissions.length
? <MultiSelectDropdown
items={allPermissions.map(p => ({name: p}))}
selectedItems={perms.map(p => ({name: p}))}
label={
permissions && permissions.length ? '' : 'Select Permissions'
}
onApply={handleUpdatePermissions}
buttonSize="btn-xs"
buttonColor="btn-primary"
customClass={classnames(
`dropdown-${USERS_TABLE.colPermissions}`,
{
'admin-table--multi-select-empty': !permissions.length,
}
)}
/>
: null}
{allPermissions && allPermissions.length ? (
<MultiSelectDropdown
items={allPermissions.map(p => ({name: p}))}
selectedItems={perms.map(p => ({name: p}))}
label={
permissions && permissions.length ? '' : 'Select Permissions'
}
onApply={handleUpdatePermissions}
buttonSize="btn-xs"
buttonColor="btn-primary"
customClass={classnames(`dropdown-${USERS_TABLE.colPermissions}`, {
'admin-table--multi-select-empty': !permissions.length,
})}
resetStateOnReceiveProps={false}
/>
) : null}
</td>
<DeleteConfirmTableCell
onDelete={onDelete}

View File

@ -1,4 +1,5 @@
import React, {PropTypes} from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import UserRow from 'src/admin/components/UserRow'
import EmptyRow from 'src/admin/components/EmptyRow'
@ -19,8 +20,8 @@ const UsersTable = ({
onUpdatePermissions,
onUpdateRoles,
onUpdatePassword,
}) =>
<div className="panel panel-default">
}) => (
<div className="panel panel-solid">
<FilterBar
type="users"
onFilter={onFilter}
@ -39,32 +40,35 @@ const UsersTable = ({
</tr>
</thead>
<tbody>
{users.length
? users
.filter(u => !u.hidden)
.map(user =>
<UserRow
key={user.links.self}
user={user}
onEdit={onEdit}
onSave={onSave}
onCancel={onCancel}
onDelete={onDelete}
isEditing={user.isEditing}
isNew={user.isNew}
allRoles={allRoles}
hasRoles={hasRoles}
allPermissions={permissions}
onUpdatePermissions={onUpdatePermissions}
onUpdateRoles={onUpdateRoles}
onUpdatePassword={onUpdatePassword}
/>
)
: <EmptyRow tableName={'Users'} />}
{users.length ? (
users
.filter(u => !u.hidden)
.map(user => (
<UserRow
key={user.links.self}
user={user}
onEdit={onEdit}
onSave={onSave}
onCancel={onCancel}
onDelete={onDelete}
isEditing={user.isEditing}
isNew={user.isNew}
allRoles={allRoles}
hasRoles={hasRoles}
allPermissions={permissions}
onUpdatePermissions={onUpdatePermissions}
onUpdateRoles={onUpdateRoles}
onUpdatePassword={onUpdatePassword}
/>
))
) : (
<EmptyRow tableName={'Users'} />
)}
</tbody>
</table>
</div>
</div>
)
const {arrayOf, bool, func, shape, string} = PropTypes

View File

@ -1,4 +1,5 @@
import React, {PropTypes} from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import {
isUserAuthorized,
@ -50,18 +51,12 @@ const AdminTabs = ({
return (
<Tabs className="row">
<TabList customClass="col-md-2 admin-tabs">
{tabs.map((t, i) =>
<Tab key={tabs[i].type}>
{tabs[i].type}
</Tab>
)}
{tabs.map((t, i) => <Tab key={tabs[i].type}>{tabs[i].type}</Tab>)}
</TabList>
<TabPanels customClass="col-md-10 admin-tabs--content">
{tabs.map((t, i) =>
<TabPanel key={tabs[i].type}>
{t.component}
</TabPanel>
)}
{tabs.map((t, i) => (
<TabPanel key={tabs[i].type}>{t.component}</TabPanel>
))}
</TabPanels>
</Tabs>
)

View File

@ -1,6 +1,7 @@
import React, {Component, PropTypes} from 'react'
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import uuid from 'node-uuid'
import uuid from 'uuid'
import AllUsersTableHeader from 'src/admin/components/chronograf/AllUsersTableHeader'
import AllUsersTableRowNew from 'src/admin/components/chronograf/AllUsersTableRowNew'
@ -15,6 +16,11 @@ const {
colActions,
} = ALL_USERS_TABLE
import {
notifyChronografUserAddedToOrg,
notifyChronografUserRemovedFromOrg,
} from 'shared/copy/notifications'
class AllUsersTable extends Component {
constructor(props) {
super(props)
@ -46,7 +52,7 @@ class AllUsersTable extends Component {
this.props.onUpdateUserRoles(
user,
newRoles,
`${user.name} has been added to ${organization.name}`
notifyChronografUserAddedToOrg(user.name, organization.name)
)
}
@ -60,7 +66,7 @@ class AllUsersTable extends Component {
this.props.onUpdateUserRoles(
user,
newRoles,
`${user.name} has been removed from ${name}`
notifyChronografUserRemovedFromOrg(user.name, name)
)
}
@ -83,7 +89,6 @@ class AllUsersTable extends Component {
onCreateUser,
authConfig,
meID,
notify,
onDeleteUser,
isLoading,
} = this.props
@ -91,7 +96,7 @@ class AllUsersTable extends Component {
const {isCreatingUser} = this.state
if (isLoading) {
return (
<div className="panel panel-default">
<div className="panel panel-solid">
<div className="panel-body">
<div className="page-spinner" />
</div>
@ -99,7 +104,7 @@ class AllUsersTable extends Component {
)
}
return (
<div className="panel panel-default">
<div className="panel panel-solid">
<AllUsersTableHeader
numUsers={users.length}
numOrganizations={organizations.length}
@ -128,34 +133,33 @@ class AllUsersTable extends Component {
</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
{users.length ? (
users.map(user => (
<AllUsersTableRow
user={user}
key={uuid.v4()}
organizations={organizations}
onBlur={this.handleBlurCreateUserRow}
onCreateUser={onCreateUser}
notify={notify}
onAddToOrganization={this.handleAddToOrganization}
onRemoveFromOrganization={this.handleRemoveFromOrganization}
onChangeSuperAdmin={this.handleChangeSuperAdmin}
onDelete={onDeleteUser}
meID={meID}
/>
: null}
))
) : (
<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}
/>
) : null}
</tbody>
</table>
</div>
@ -208,7 +212,6 @@ AllUsersTable.propTypes = {
superAdminNewUsers: bool,
}),
meID: string.isRequired,
notify: func.isRequired,
isLoading: bool.isRequired,
}

View File

@ -1,4 +1,5 @@
import React, {PropTypes} from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import SlideToggle from 'shared/components/SlideToggle'
@ -11,13 +12,12 @@ const AllUsersTableHeader = ({
onChangeAuthConfig,
}) => {
const numUsersString = `${numUsers} User${numUsers === 1 ? '' : 's'}`
const numOrganizationsString = `${numOrganizations} Org${numOrganizations ===
1
? ''
: 's'}`
const numOrganizationsString = `${numOrganizations} Org${
numOrganizations === 1 ? '' : 's'
}`
return (
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
<div className="panel-heading">
<h2 className="panel-title">
{numUsersString} across {numOrganizationsString}
</h2>

View File

@ -1,4 +1,6 @@
import React, {PropTypes} from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import _ from 'lodash'
import Tags from 'shared/components/Tags'
import SlideToggle from 'shared/components/SlideToggle'
@ -36,7 +38,7 @@ const AllUsersTableRow = ({
name: organizations.find(o => r.organization === o.id).name,
}))
const wrappedDelete = () => onDelete(user)
const wrappedDelete = _.curry(onDelete, user)
const removeWarning = userIsMe
? 'Delete your user record\nand log yourself out?'
@ -45,14 +47,14 @@ const AllUsersTableRow = ({
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>}
{userIsMe ? (
<strong className="chronograf-user--me">
<span className="icon user" />
{user.name}
</strong>
) : (
<strong>{user.name}</strong>
)}
</td>
<td style={{width: colOrganizations}}>
<Tags
@ -63,12 +65,8 @@ const AllUsersTableRow = ({
addMenuChoose={onAddToOrganization(user)}
/>
</td>
<td style={{width: colProvider}}>
{user.provider}
</td>
<td style={{width: colScheme}}>
{user.scheme}
</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}
@ -82,7 +80,8 @@ const AllUsersTableRow = ({
confirmText={removeWarning}
confirmAction={wrappedDelete}
size="btn-xs"
text="Remove"
type="btn-danger"
text="Delete"
customClass="table--show-on-row-hover"
/>
</td>

View File

@ -1,7 +1,12 @@
import React, {Component, PropTypes} from 'react'
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import {notify as notifyAction} from 'shared/actions/notifications'
import Dropdown from 'shared/components/Dropdown'
import {notifyChronografUserMissingNameAndProvider} from 'shared/copy/notifications'
import {ALL_USERS_TABLE} from 'src/admin/constants/chronografTableSizing'
const {
colOrganizations,
@ -78,10 +83,7 @@ class AllUsersTableRowNew extends Component {
if (e.key === 'Enter') {
if (preventCreate) {
return this.props.notify(
'warning',
'User must have a name and provider'
)
return this.props.notify(notifyChronografUserMissingNameAndProvider())
}
this.handleConfirmCreateUser()
}
@ -180,4 +182,8 @@ AllUsersTableRowNew.propTypes = {
notify: func.isRequired,
}
export default AllUsersTableRowNew
const mapDispatchToProps = dispatch => ({
notify: bindActionCreators(notifyAction, dispatch),
})
export default connect(null, mapDispatchToProps)(AllUsersTableRowNew)

View File

@ -1,6 +1,7 @@
import React, {Component, PropTypes} from 'react'
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import uuid from 'node-uuid'
import uuid from 'uuid'
import OrganizationsTableRow from 'src/admin/components/chronograf/OrganizationsTableRow'
import OrganizationsTableRowNew from 'src/admin/components/chronograf/OrganizationsTableRowNew'
@ -38,14 +39,13 @@ class OrganizationsTable extends Component {
} = this.props
const {isCreatingOrganization} = this.state
const tableTitle = `${organizations.length} Organization${organizations.length ===
1
? ''
: 's'}`
const tableTitle = `${organizations.length} Organization${
organizations.length === 1 ? '' : 's'
}`
if (!organizations.length) {
return (
<div className="panel panel-default">
<div className="panel panel-solid">
<div className="panel-body">
<div className="page-spinner" />
</div>
@ -53,11 +53,9 @@ class OrganizationsTable extends Component {
)
}
return (
<div className="panel panel-default">
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
<h2 className="panel-title">
{tableTitle}
</h2>
<div className="panel panel-solid">
<div className="panel-heading">
<h2 className="panel-title">{tableTitle}</h2>
<button
className="btn btn-sm btn-primary"
onClick={this.handleClickCreateOrganization}
@ -75,13 +73,13 @@ class OrganizationsTable extends Component {
</div>
<div className="fancytable--th orgs-table--delete" />
</div>
{isCreatingOrganization
? <OrganizationsTableRowNew
onCreateOrganization={this.handleCreateOrganization}
onCancelCreateOrganization={this.handleCancelCreateOrganization}
/>
: null}
{organizations.map(org =>
{isCreatingOrganization ? (
<OrganizationsTableRowNew
onCreateOrganization={this.handleCreateOrganization}
onCancelCreateOrganization={this.handleCancelCreateOrganization}
/>
) : null}
{organizations.map(org => (
<OrganizationsTableRow
key={uuid.v4()}
organization={org}
@ -90,7 +88,7 @@ class OrganizationsTable extends Component {
onChooseDefaultRole={onChooseDefaultRole}
currentOrganization={currentOrganization}
/>
)}
))}
</div>
</div>
)

View File

@ -1,4 +1,5 @@
import React, {Component, PropTypes} from 'react'
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import {withRouter} from 'react-router'
@ -13,19 +14,21 @@ import {DEFAULT_ORG_ID} from 'src/admin/constants/chronografAdmin'
import {USER_ROLES} from 'src/admin/constants/chronografAdmin'
const OrganizationsTableRowDeleteButton = ({organization, onClickDelete}) =>
organization.id === DEFAULT_ORG_ID
? <button
className="btn btn-sm btn-default btn-square orgs-table--delete"
disabled={true}
>
<span className="icon trash" />
</button>
: <button
className="btn btn-sm btn-default btn-square"
onClick={onClickDelete}
>
<span className="icon trash" />
</button>
organization.id === DEFAULT_ORG_ID ? (
<button
className="btn btn-sm btn-default btn-square orgs-table--delete"
disabled={true}
>
<span className="icon trash" />
</button>
) : (
<button
className="btn btn-sm btn-default btn-square"
onClick={onClickDelete}
>
<span className="icon trash" />
</button>
)
class OrganizationsTableRow extends Component {
constructor(props) {
@ -80,21 +83,23 @@ class OrganizationsTableRow extends Component {
return (
<div className="fancytable--row">
<div className="fancytable--td orgs-table--active">
{organization.id === currentOrganization.id
? <button className="btn btn-sm btn-success">
<span className="icon checkmark" /> Current
</button>
: <button
className="btn btn-sm btn-default"
onClick={this.handleChangeCurrentOrganization}
>
<span className="icon shuffle" /> Switch to
</button>}
{organization.id === currentOrganization.id ? (
<button className="btn btn-sm btn-success">
<span className="icon checkmark" /> Current
</button>
) : (
<button
className="btn btn-sm btn-default"
onClick={this.handleChangeCurrentOrganization}
>
<span className="icon shuffle" /> Switch to
</button>
)}
</div>
<InputClickToEdit
value={organization.name}
wrapperClass="fancytable--td orgs-table--name"
onUpdate={this.handleUpdateOrgName}
onBlur={this.handleUpdateOrgName}
/>
<div className={defaultRoleClassName}>
<Dropdown
@ -104,18 +109,21 @@ class OrganizationsTableRow extends Component {
className="dropdown-stretch"
/>
</div>
{isDeleting
? <ConfirmButtons
item={organization}
onCancel={this.handleDismissDeleteConfirmation}
onConfirm={this.handleDeleteOrg}
onClickOutside={this.handleDismissDeleteConfirmation}
confirmLeft={true}
/>
: <OrganizationsTableRowDeleteButton
organization={organization}
onClickDelete={this.handleDeleteClick}
/>}
{isDeleting ? (
<ConfirmButtons
item={organization}
onCancel={this.handleDismissDeleteConfirmation}
onConfirm={this.handleDeleteOrg}
onClickOutside={this.handleDismissDeleteConfirmation}
confirmLeft={true}
confirmTitle="Delete"
/>
) : (
<OrganizationsTableRowDeleteButton
organization={organization}
onClickDelete={this.handleDeleteClick}
/>
)}
</div>
)
}

Some files were not shown because too many files have changed in this diff Show More