Merge branch 'master' of https://github.com/influxdata/chronograf
commit
0a98d1c7b7
|
@ -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}
|
||||
|
|
|
@ -22,3 +22,4 @@ chronograf*.db
|
|||
*_gen.go
|
||||
canned/apps_gen.go
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
|
1208
CHANGELOG.md
1208
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
------------------------
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
5
Makefile
5
Makefile
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -56,6 +56,7 @@
|
|||
]
|
||||
}
|
||||
],
|
||||
"colors": [],
|
||||
"type": "single-stat"
|
||||
},
|
||||
{
|
||||
|
@ -73,6 +74,7 @@
|
|||
]
|
||||
}
|
||||
],
|
||||
"colors": [],
|
||||
"type": "single-stat"
|
||||
},
|
||||
{
|
||||
|
|
|
@ -117,6 +117,7 @@
|
|||
"h": 4,
|
||||
"i": "0fa47984-825b-46f1-9ca5-0366e3220008",
|
||||
"name": "Mesos Master Uptime",
|
||||
"colors": [],
|
||||
"type": "single-stat",
|
||||
"queries": [
|
||||
{
|
||||
|
|
118
chronograf.go
118
chronograf.go
|
@ -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
|
||||
|
|
|
@ -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/).
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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{
|
||||
|
|
108
influx/influx.go
108
influx/influx.go
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package influx
|
||||
|
||||
import "time"
|
||||
|
||||
// Now returns the current time
|
||||
type Now func() time.Time
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"id": 5000,
|
||||
"srcID": 5000,
|
||||
"id": "5000",
|
||||
"srcID": "5000",
|
||||
"name": "Kapa 1",
|
||||
"url": "http://localhost:9092",
|
||||
"active": true,
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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{},
|
||||
|
|
|
@ -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{},
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
|
|
34
server/me.go
34
server/me.go
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"transform-runtime",
|
||||
"lodash"
|
||||
],
|
||||
"presets": ["es2015", "react", "stage-0"],
|
||||
"presets": ["env", "react", "stage-0"],
|
||||
"env": {
|
||||
"production": {
|
||||
"plugins": [
|
||||
|
|
538
ui/.eslintrc
538
ui/.eslintrc
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,3 +6,4 @@ dist/
|
|||
bower_components/
|
||||
log/
|
||||
.tern-project
|
||||
yarn-error.log
|
|
@ -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',
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
}
|
|
@ -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())
|
|
@ -0,0 +1 @@
|
|||
export default jest.fn(() => Promise.resolve())
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
117
ui/package.json
117
ui/package.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
const context = require.context('./', true, /Spec\.js$/)
|
||||
context.keys().forEach(context)
|
||||
module.exports = context
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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)(
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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})
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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})
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue