Merge branch 'master' into feature/annotationz-pre-pl-with-master
commit
59b6979812
|
@ -1,5 +1,5 @@
|
|||
[bumpversion]
|
||||
current_version = 1.4.0.1
|
||||
current_version = 1.4.1.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}
|
||||
|
|
|
@ -6,6 +6,7 @@ backup/
|
|||
|
||||
# Binaries
|
||||
/chronograf
|
||||
/chronoctl
|
||||
|
||||
# Dotfiles
|
||||
.pull-request
|
||||
|
|
51
CHANGELOG.md
51
CHANGELOG.md
|
@ -1,29 +1,46 @@
|
|||
## v1.4.1.0 [unreleased]
|
||||
## v1.4.2.0 [unreleased]
|
||||
### Features
|
||||
1. [#2409](https://github.com/influxdata/chronograf/pull/2409): Allow adding multiple event handlers to a rule
|
||||
1. [#2709](https://github.com/influxdata/chronograf/pull/2709): Add "send test alert" button to test kapacitor alert configurations
|
||||
1. [#2708](https://github.com/influxdata/chronograf/pull/2708): Link to specified kapacitor config panel from rule builder alert handlers
|
||||
1. [#2722](https://github.com/influxdata/chronograf/pull/2722): Add auto refresh widget to hosts list page
|
||||
1. [#2765](https://github.com/influxdata/chronograf/pull/2765): Update to go 1.9.3 and node 6.12.3 for releases
|
||||
1. [#2784](https://github.com/influxdata/chronograf/pull/2784): Update to go 1.9.4
|
||||
1. [#2703](https://github.com/influxdata/chronograf/pull/2703): Add global users page visible only to super admins
|
||||
1. [#2777](https://github.com/influxdata/chronograf/pull/2777): Allow user to delete themselves
|
||||
|
||||
1. [#2703](https://github.com/influxdata/chronograf/pull/2703): Add global users page visible only to super admins
|
||||
1. [#2781](https://github.com/influxdata/chronograf/pull/2781): Add commands to users & create super admin
|
||||
### UI Improvements
|
||||
1. [#2698](https://github.com/influxdata/chronograf/pull/2698): Improve clarity of terminology surrounding InfluxDB & Kapacitor connections
|
||||
### Bug Fixes
|
||||
|
||||
## v1.4.1.3 [2018-02-14]
|
||||
### Bug Fixes
|
||||
1. [#2818](https://github.com/influxdata/chronograf/pull/2818): Allow self-signed certificates for Enterprise InfluxDB Meta nodes
|
||||
|
||||
## v1.4.1.2 [2018-02-13]
|
||||
### Bug Fixes
|
||||
1. [9321336](https://github.com/influxdata/chronograf/commit/9321336): Respect basepath when fetching server api routes
|
||||
1. [#2812](https://github.com/influxdata/chronograf/pull/2812): Set default tempVar :interval: with data explorer csv download call.
|
||||
1. [#2811](https://github.com/influxdata/chronograf/pull/2811): Display series with value of 0 in a cell legend
|
||||
|
||||
## v1.4.1.1 [2018-02-12]
|
||||
### Features
|
||||
1. [#2409](https://github.com/influxdata/chronograf/pull/2409): Allow multiple event handlers per rule
|
||||
1. [#2709](https://github.com/influxdata/chronograf/pull/2709): Add "send test alert" button to test kapacitor alert configurations
|
||||
1. [#2708](https://github.com/influxdata/chronograf/pull/2708): Link to kapacitor config panel from alert rule builder
|
||||
1. [#2722](https://github.com/influxdata/chronograf/pull/2722): Add auto refresh widget to hosts list page
|
||||
1. [#2784](https://github.com/influxdata/chronograf/pull/2784): Update go from 1.9.3 to 1.9.4
|
||||
1. [#2765](https://github.com/influxdata/chronograf/pull/2765): Update to go 1.9.3 and node 6.12.3 for releases
|
||||
1. [#2777](https://github.com/influxdata/chronograf/pull/2777): Allow user to delete themselves
|
||||
1. [#2703](https://github.com/influxdata/chronograf/pull/2703): Add All Users page, visible only to super admins
|
||||
1. [#2781](https://github.com/influxdata/chronograf/pull/2781): Introduce chronoctl binary for user CRUD operations
|
||||
1. [#2699](https://github.com/influxdata/chronograf/pull/2699): Introduce Mappings to allow control over new user organization assignments
|
||||
### UI Improvements
|
||||
1. [#2698](https://github.com/influxdata/chronograf/pull/2698): Clarify terminology surrounding InfluxDB & Kapacitor connections
|
||||
1. [#2746](https://github.com/influxdata/chronograf/pull/2746): Separate saving TICKscript from exiting editor page
|
||||
1. [#2774](https://github.com/influxdata/chronograf/pull/2774): Enable Save (⌘ + Enter) and Cancel (Escape) hotkeys in Cell Editor Overlay
|
||||
1. [#2788](https://github.com/influxdata/chronograf/pull/2788): Enable customization of Single Stat "Base Color"
|
||||
|
||||
### Bug Fixes
|
||||
1. [#2684](https://github.com/influxdata/chronograf/pull/2684): Fix TICKscript Sensu alerts when no group by tags selected
|
||||
1. [#2735](https://github.com/influxdata/chronograf/pull/2735): Remove cli options from systemd service file
|
||||
1. [#2757](https://github.com/influxdata/chronograf/pull/2757): Added "TO" field to kapacitor SMTP config, and improved error messages for config saving and testing
|
||||
1. [#2756](https://github.com/influxdata/chronograf/pull/2756): Display 200 most-recent TICKscript log messages; prevent overlapping
|
||||
1. [#2757](https://github.com/influxdata/chronograf/pull/2757): Add "TO" to kapacitor SMTP config; improve config update error messages
|
||||
1. [#2761](https://github.com/influxdata/chronograf/pull/2761): Remove cli options from sysvinit service file
|
||||
1. [#2780](https://github.com/influxdata/chronograf/pull/2780): Fix routing on alert save
|
||||
1. [#2735](https://github.com/influxdata/chronograf/pull/2735): Remove cli options from systemd service file
|
||||
1. [#2788](https://github.com/influxdata/chronograf/pull/2788): Fix disappearance of text in Single Stat graphs during editing
|
||||
1. [#2780](https://github.com/influxdata/chronograf/pull/2780): Redirect to Alerts page after saving Alert Rule
|
||||
|
||||
## v1.4.0.1 [2017-1-9]
|
||||
## v1.4.0.1 [2018-1-9]
|
||||
### Features
|
||||
1. [#2690](https://github.com/influxdata/chronograf/pull/2690): Add separate CLI flag for canned sources, kapacitors, dashboards, and organizations
|
||||
1. [#2672](https://github.com/influxdata/chronograf/pull/2672): Add telegraf interval configuration
|
||||
|
|
|
@ -5,6 +5,7 @@ RUN apk add --update ca-certificates && \
|
|||
rm /var/cache/apk/*
|
||||
|
||||
ADD chronograf /usr/bin/chronograf
|
||||
ADD chronoctl /usr/bin/chronoctl
|
||||
ADD canned/*.json /usr/share/chronograf/canned/
|
||||
ADD LICENSE /usr/share/chronograf/LICENSE
|
||||
ADD agpl-3.0.md /usr/share/chronograf/agpl-3.0.md
|
||||
|
|
2
Makefile
2
Makefile
|
@ -11,6 +11,7 @@ UISOURCES := $(shell find ui -type f -not \( -path ui/build/\* -o -path ui/node_
|
|||
unexport LDFLAGS
|
||||
LDFLAGS=-ldflags "-s -X main.version=${VERSION} -X main.commit=${COMMIT}"
|
||||
BINARY=chronograf
|
||||
CTLBINARY=chronoctl
|
||||
|
||||
.DEFAULT_GOAL := all
|
||||
|
||||
|
@ -22,6 +23,7 @@ dev: dep dev-assets ${BINARY}
|
|||
|
||||
${BINARY}: $(SOURCES) .bindata .jsdep .godep
|
||||
go build -o ${BINARY} ${LDFLAGS} ./cmd/chronograf/main.go
|
||||
go build -o ${CTLBINARY} ${LDFLAGS} ./cmd/chronoctl
|
||||
|
||||
define CHRONOGIRAFFE
|
||||
._ o o
|
||||
|
|
|
@ -136,7 +136,7 @@ option.
|
|||
## Versions
|
||||
|
||||
The most recent version of Chronograf is
|
||||
[v1.4.0.1](https://www.influxdata.com/downloads/).
|
||||
[v1.4.1.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.0.1
|
||||
docker pull chronograf:1.4.1.3
|
||||
```
|
||||
|
||||
### From Source
|
||||
|
|
|
@ -41,6 +41,7 @@ type Client struct {
|
|||
UsersStore *UsersStore
|
||||
OrganizationsStore *OrganizationsStore
|
||||
ConfigStore *ConfigStore
|
||||
MappingsStore *MappingsStore
|
||||
}
|
||||
|
||||
// NewClient initializes all stores
|
||||
|
@ -60,6 +61,7 @@ func NewClient() *Client {
|
|||
c.UsersStore = &UsersStore{client: c}
|
||||
c.OrganizationsStore = &OrganizationsStore{client: c}
|
||||
c.ConfigStore = &ConfigStore{client: c}
|
||||
c.MappingsStore = &MappingsStore{client: c}
|
||||
return c
|
||||
}
|
||||
|
||||
|
@ -151,6 +153,10 @@ func (c *Client) initialize(ctx context.Context) error {
|
|||
if _, err := tx.CreateBucketIfNotExists(BuildBucket); err != nil {
|
||||
return err
|
||||
}
|
||||
// Always create Mapping bucket.
|
||||
if _, err := tx.CreateBucketIfNotExists(MappingsBucket); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
|
@ -184,6 +190,9 @@ func (c *Client) migrate(ctx context.Context, build chronograf.BuildInfo) error
|
|||
if err := c.BuildStore.Migrate(ctx, build); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.MappingsStore.Migrate(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -586,11 +586,11 @@ func UnmarshalRolePB(data []byte, r *Role) error {
|
|||
|
||||
// MarshalOrganization encodes a organization to binary protobuf format.
|
||||
func MarshalOrganization(o *chronograf.Organization) ([]byte, error) {
|
||||
|
||||
return MarshalOrganizationPB(&Organization{
|
||||
ID: o.ID,
|
||||
Name: o.Name,
|
||||
DefaultRole: o.DefaultRole,
|
||||
Public: o.Public,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -608,7 +608,6 @@ func UnmarshalOrganization(data []byte, o *chronograf.Organization) error {
|
|||
o.ID = pb.ID
|
||||
o.Name = pb.Name
|
||||
o.DefaultRole = pb.DefaultRole
|
||||
o.Public = pb.Public
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -650,3 +649,41 @@ func UnmarshalConfig(data []byte, c *chronograf.Config) error {
|
|||
func UnmarshalConfigPB(data []byte, c *Config) error {
|
||||
return proto.Unmarshal(data, c)
|
||||
}
|
||||
|
||||
// MarshalMapping encodes a mapping to binary protobuf format.
|
||||
func MarshalMapping(m *chronograf.Mapping) ([]byte, error) {
|
||||
|
||||
return MarshalMappingPB(&Mapping{
|
||||
Provider: m.Provider,
|
||||
Scheme: m.Scheme,
|
||||
ProviderOrganization: m.ProviderOrganization,
|
||||
ID: m.ID,
|
||||
Organization: m.Organization,
|
||||
})
|
||||
}
|
||||
|
||||
// MarshalMappingPB encodes a mapping to binary protobuf format.
|
||||
func MarshalMappingPB(m *Mapping) ([]byte, error) {
|
||||
return proto.Marshal(m)
|
||||
}
|
||||
|
||||
// UnmarshalMapping decodes a mapping from binary protobuf data.
|
||||
func UnmarshalMapping(data []byte, m *chronograf.Mapping) error {
|
||||
var pb Mapping
|
||||
if err := UnmarshalMappingPB(data, &pb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.Provider = pb.Provider
|
||||
m.Scheme = pb.Scheme
|
||||
m.ProviderOrganization = pb.ProviderOrganization
|
||||
m.Organization = pb.Organization
|
||||
m.ID = pb.ID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalMappingPB decodes a mapping from binary protobuf data.
|
||||
func UnmarshalMappingPB(data []byte, m *Mapping) error {
|
||||
return proto.Unmarshal(data, m)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
// Code generated by protoc-gen-gogo.
|
||||
// Code generated by protoc-gen-gogo. DO NOT EDIT.
|
||||
// source: internal.proto
|
||||
// DO NOT EDIT!
|
||||
|
||||
/*
|
||||
Package internal is a generated protocol buffer package.
|
||||
|
@ -27,6 +26,7 @@ It has these top-level messages:
|
|||
AlertRule
|
||||
User
|
||||
Role
|
||||
Mapping
|
||||
Organization
|
||||
Config
|
||||
AuthConfig
|
||||
|
@ -1057,17 +1057,64 @@ func (m *Role) GetName() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
type Mapping struct {
|
||||
Provider string `protobuf:"bytes,1,opt,name=Provider,proto3" json:"Provider,omitempty"`
|
||||
Scheme string `protobuf:"bytes,2,opt,name=Scheme,proto3" json:"Scheme,omitempty"`
|
||||
ProviderOrganization string `protobuf:"bytes,3,opt,name=ProviderOrganization,proto3" json:"ProviderOrganization,omitempty"`
|
||||
ID string `protobuf:"bytes,4,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
Organization string `protobuf:"bytes,5,opt,name=Organization,proto3" json:"Organization,omitempty"`
|
||||
}
|
||||
|
||||
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 (m *Mapping) GetProvider() string {
|
||||
if m != nil {
|
||||
return m.Provider
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Mapping) GetScheme() string {
|
||||
if m != nil {
|
||||
return m.Scheme
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Mapping) GetProviderOrganization() string {
|
||||
if m != nil {
|
||||
return m.ProviderOrganization
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Mapping) GetID() string {
|
||||
if m != nil {
|
||||
return m.ID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Mapping) GetOrganization() string {
|
||||
if m != nil {
|
||||
return m.Organization
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type Organization struct {
|
||||
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
|
||||
DefaultRole string `protobuf:"bytes,3,opt,name=DefaultRole,proto3" json:"DefaultRole,omitempty"`
|
||||
Public bool `protobuf:"varint,4,opt,name=Public,proto3" json:"Public,omitempty"`
|
||||
}
|
||||
|
||||
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{18} }
|
||||
func (*Organization) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{19} }
|
||||
|
||||
func (m *Organization) GetID() string {
|
||||
if m != nil {
|
||||
|
@ -1090,13 +1137,6 @@ func (m *Organization) GetDefaultRole() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (m *Organization) GetPublic() bool {
|
||||
if m != nil {
|
||||
return m.Public
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Auth *AuthConfig `protobuf:"bytes,1,opt,name=Auth" json:"Auth,omitempty"`
|
||||
}
|
||||
|
@ -1104,7 +1144,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{19} }
|
||||
func (*Config) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{20} }
|
||||
|
||||
func (m *Config) GetAuth() *AuthConfig {
|
||||
if m != nil {
|
||||
|
@ -1120,7 +1160,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{20} }
|
||||
func (*AuthConfig) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{21} }
|
||||
|
||||
func (m *AuthConfig) GetSuperAdminNewUsers() bool {
|
||||
if m != nil {
|
||||
|
@ -1137,7 +1177,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{21} }
|
||||
func (*BuildInfo) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{22} }
|
||||
|
||||
func (m *BuildInfo) GetVersion() string {
|
||||
if m != nil {
|
||||
|
@ -1172,6 +1212,7 @@ func init() {
|
|||
proto.RegisterType((*AlertRule)(nil), "internal.AlertRule")
|
||||
proto.RegisterType((*User)(nil), "internal.User")
|
||||
proto.RegisterType((*Role)(nil), "internal.Role")
|
||||
proto.RegisterType((*Mapping)(nil), "internal.Mapping")
|
||||
proto.RegisterType((*Organization)(nil), "internal.Organization")
|
||||
proto.RegisterType((*Config)(nil), "internal.Config")
|
||||
proto.RegisterType((*AuthConfig)(nil), "internal.AuthConfig")
|
||||
|
@ -1181,92 +1222,93 @@ func init() {
|
|||
func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) }
|
||||
|
||||
var fileDescriptorInternal = []byte{
|
||||
// 1379 bytes of a gzipped FileDescriptorProto
|
||||
// 1406 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x57, 0x5f, 0x8f, 0xdb, 0x44,
|
||||
0x10, 0x97, 0xe3, 0x38, 0x89, 0x27, 0xd7, 0x52, 0x99, 0x8a, 0x9a, 0x22, 0xa1, 0x60, 0x81, 0x08,
|
||||
0x82, 0x1e, 0xe8, 0x2a, 0x24, 0x84, 0xa0, 0x52, 0xee, 0x82, 0xca, 0xd1, 0x6b, 0x7b, 0xdd, 0xdc,
|
||||
0x1d, 0x4f, 0xa8, 0xda, 0x38, 0x93, 0xc4, 0xaa, 0x63, 0x9b, 0xb5, 0x7d, 0x17, 0xf3, 0x61, 0x90,
|
||||
0x90, 0xf8, 0x04, 0x88, 0x77, 0x5e, 0x11, 0x1f, 0x84, 0xaf, 0xc0, 0x13, 0x12, 0x9a, 0xdd, 0xf5,
|
||||
0x9f, 0x5c, 0x42, 0xd5, 0x07, 0xc4, 0xdb, 0xfe, 0x66, 0x36, 0xb3, 0xf3, 0xe7, 0x37, 0x33, 0x0e,
|
||||
0xdc, 0x0c, 0xa2, 0x0c, 0x45, 0xc4, 0xc3, 0xfd, 0x44, 0xc4, 0x59, 0xec, 0xf4, 0x4a, 0xec, 0xfd,
|
||||
0xd9, 0x82, 0xce, 0x24, 0xce, 0x85, 0x8f, 0xce, 0x4d, 0x68, 0x1d, 0x8f, 0x5d, 0x63, 0x60, 0x0c,
|
||||
0x4d, 0xd6, 0x3a, 0x1e, 0x3b, 0x0e, 0xb4, 0x9f, 0xf0, 0x15, 0xba, 0xad, 0x81, 0x31, 0xb4, 0x99,
|
||||
0x3c, 0x93, 0xec, 0xac, 0x48, 0xd0, 0x35, 0x95, 0x8c, 0xce, 0xce, 0x5d, 0xe8, 0x9d, 0xa7, 0x64,
|
||||
0x6d, 0x85, 0x6e, 0x5b, 0xca, 0x2b, 0x4c, 0xba, 0x53, 0x9e, 0xa6, 0x57, 0xb1, 0x98, 0xb9, 0x96,
|
||||
0xd2, 0x95, 0xd8, 0xb9, 0x05, 0xe6, 0x39, 0x3b, 0x71, 0x3b, 0x52, 0x4c, 0x47, 0xc7, 0x85, 0xee,
|
||||
0x18, 0xe7, 0x3c, 0x0f, 0x33, 0xb7, 0x3b, 0x30, 0x86, 0x3d, 0x56, 0x42, 0xb2, 0x73, 0x86, 0x21,
|
||||
0x2e, 0x04, 0x9f, 0xbb, 0x3d, 0x65, 0xa7, 0xc4, 0xce, 0x3e, 0x38, 0xc7, 0x51, 0x8a, 0x7e, 0x2e,
|
||||
0x70, 0xf2, 0x22, 0x48, 0x2e, 0x50, 0x04, 0xf3, 0xc2, 0xb5, 0xa5, 0x81, 0x1d, 0x1a, 0x7a, 0xe5,
|
||||
0x31, 0x66, 0x9c, 0xde, 0x06, 0x69, 0xaa, 0x84, 0x8e, 0x07, 0x7b, 0x93, 0x25, 0x17, 0x38, 0x9b,
|
||||
0xa0, 0x2f, 0x30, 0x73, 0xfb, 0x52, 0xbd, 0x21, 0xa3, 0x3b, 0x4f, 0xc5, 0x82, 0x47, 0xc1, 0x0f,
|
||||
0x3c, 0x0b, 0xe2, 0xc8, 0xdd, 0x53, 0x77, 0x9a, 0x32, 0xca, 0x12, 0x8b, 0x43, 0x74, 0x6f, 0xa8,
|
||||
0x2c, 0xd1, 0xd9, 0xfb, 0xd5, 0x00, 0x7b, 0xcc, 0xd3, 0xe5, 0x34, 0xe6, 0x62, 0xf6, 0x4a, 0xb9,
|
||||
0xbe, 0x07, 0x96, 0x8f, 0x61, 0x98, 0xba, 0xe6, 0xc0, 0x1c, 0xf6, 0x0f, 0xee, 0xec, 0x57, 0x45,
|
||||
0xac, 0xec, 0x1c, 0x61, 0x18, 0x32, 0x75, 0xcb, 0xf9, 0x04, 0xec, 0x0c, 0x57, 0x49, 0xc8, 0x33,
|
||||
0x4c, 0xdd, 0xb6, 0xfc, 0x89, 0x53, 0xff, 0xe4, 0x4c, 0xab, 0x58, 0x7d, 0x69, 0x2b, 0x14, 0x6b,
|
||||
0x3b, 0x14, 0xef, 0xef, 0x16, 0xdc, 0xd8, 0x78, 0xce, 0xd9, 0x03, 0x63, 0x2d, 0x3d, 0xb7, 0x98,
|
||||
0xb1, 0x26, 0x54, 0x48, 0xaf, 0x2d, 0x66, 0x14, 0x84, 0xae, 0x24, 0x37, 0x2c, 0x66, 0x5c, 0x11,
|
||||
0x5a, 0x4a, 0x46, 0x58, 0xcc, 0x58, 0x3a, 0x1f, 0x40, 0xf7, 0xfb, 0x1c, 0x45, 0x80, 0xa9, 0x6b,
|
||||
0x49, 0xef, 0x5e, 0xab, 0xbd, 0x7b, 0x96, 0xa3, 0x28, 0x58, 0xa9, 0xa7, 0x6c, 0x48, 0x36, 0x29,
|
||||
0x6a, 0xc8, 0x33, 0xc9, 0x32, 0x62, 0x5e, 0x57, 0xc9, 0xe8, 0xac, 0xb3, 0xa8, 0xf8, 0x40, 0x59,
|
||||
0xfc, 0x14, 0xda, 0x7c, 0x8d, 0xa9, 0x6b, 0x4b, 0xfb, 0xef, 0xfc, 0x4b, 0xc2, 0xf6, 0x47, 0x6b,
|
||||
0x4c, 0xbf, 0x8a, 0x32, 0x51, 0x30, 0x79, 0xdd, 0x79, 0x1f, 0x3a, 0x7e, 0x1c, 0xc6, 0x22, 0x75,
|
||||
0xe1, 0xba, 0x63, 0x47, 0x24, 0x67, 0x5a, 0xed, 0x0c, 0xa1, 0x13, 0xe2, 0x02, 0xa3, 0x99, 0x64,
|
||||
0x46, 0xff, 0xe0, 0x56, 0x7d, 0xf1, 0x44, 0xca, 0x99, 0xd6, 0xdf, 0x7d, 0x08, 0x76, 0xf5, 0x0a,
|
||||
0x11, 0xfd, 0x05, 0x16, 0x32, 0x67, 0x36, 0xa3, 0xa3, 0xf3, 0x2e, 0x58, 0x97, 0x3c, 0xcc, 0x55,
|
||||
0xbd, 0xfb, 0x07, 0x37, 0x6b, 0x3b, 0xa3, 0x75, 0x90, 0x32, 0xa5, 0xfc, 0xbc, 0xf5, 0x99, 0xe1,
|
||||
0x2d, 0xc0, 0x92, 0x3e, 0x34, 0x18, 0x63, 0x97, 0x8c, 0x91, 0x9d, 0xd8, 0x6a, 0x74, 0xe2, 0x2d,
|
||||
0x30, 0xbf, 0xc6, 0xb5, 0x6e, 0x4e, 0x3a, 0x56, 0xbc, 0x6a, 0x37, 0x78, 0x75, 0x1b, 0xac, 0x0b,
|
||||
0xf9, 0xb8, 0xaa, 0xb7, 0x02, 0xde, 0x03, 0xe8, 0xa8, 0x18, 0x2a, 0xcb, 0x46, 0xc3, 0xf2, 0x00,
|
||||
0xfa, 0x4f, 0x45, 0x80, 0x51, 0xa6, 0x98, 0xa2, 0x1e, 0x6d, 0x8a, 0xbc, 0x5f, 0x0c, 0x68, 0x93,
|
||||
0xf3, 0xc4, 0xaa, 0x10, 0x17, 0xdc, 0x2f, 0x0e, 0xe3, 0x3c, 0x9a, 0xa5, 0xae, 0x31, 0x30, 0x87,
|
||||
0x26, 0xdb, 0x90, 0x39, 0x6f, 0x40, 0x67, 0xaa, 0xb4, 0xad, 0x81, 0x39, 0xb4, 0x99, 0x46, 0xe4,
|
||||
0x5a, 0xc8, 0xa7, 0x18, 0xea, 0x10, 0x14, 0xa0, 0xdb, 0x89, 0xc0, 0x79, 0xb0, 0xd6, 0x61, 0x68,
|
||||
0x44, 0xf2, 0x34, 0x9f, 0x93, 0x5c, 0x45, 0xa2, 0x11, 0x05, 0x30, 0xe5, 0x69, 0x45, 0x1f, 0x3a,
|
||||
0x93, 0xe5, 0xd4, 0xe7, 0x61, 0xc9, 0x1f, 0x05, 0xbc, 0xdf, 0x0c, 0x9a, 0x2b, 0xaa, 0x1f, 0xb6,
|
||||
0x32, 0xfc, 0x26, 0xf4, 0xa8, 0x57, 0x9e, 0x5f, 0x72, 0xa1, 0x03, 0xee, 0x12, 0xbe, 0xe0, 0xc2,
|
||||
0xf9, 0x18, 0x3a, 0xb2, 0x44, 0x3b, 0x7a, 0xb3, 0x34, 0x27, 0xb3, 0xca, 0xf4, 0xb5, 0x8a, 0xbd,
|
||||
0xed, 0x06, 0x7b, 0xab, 0x60, 0xad, 0x66, 0xb0, 0xf7, 0xc0, 0xa2, 0x36, 0x28, 0xa4, 0xf7, 0x3b,
|
||||
0x2d, 0xab, 0x66, 0x51, 0xb7, 0xbc, 0x73, 0xb8, 0xb1, 0xf1, 0x62, 0xf5, 0x92, 0xb1, 0xf9, 0x52,
|
||||
0x4d, 0x37, 0x5b, 0xd3, 0x8b, 0x66, 0x6a, 0x8a, 0x21, 0xfa, 0x19, 0xce, 0x64, 0xbe, 0x7b, 0xac,
|
||||
0xc2, 0xde, 0x4f, 0x46, 0x6d, 0x57, 0xbe, 0x47, 0x53, 0xd3, 0x8f, 0x57, 0x2b, 0x1e, 0xcd, 0xb4,
|
||||
0xe9, 0x12, 0x52, 0xde, 0x66, 0x53, 0x6d, 0xba, 0x35, 0x9b, 0x12, 0x16, 0x89, 0xae, 0x60, 0x4b,
|
||||
0x24, 0xc4, 0x9d, 0x15, 0xf2, 0x34, 0x17, 0xb8, 0xc2, 0x28, 0xd3, 0x29, 0x68, 0x8a, 0x9c, 0x3b,
|
||||
0xd0, 0xcd, 0xf8, 0xe2, 0x39, 0x35, 0x89, 0xae, 0x64, 0xc6, 0x17, 0x8f, 0xb0, 0x70, 0xde, 0x02,
|
||||
0x7b, 0x1e, 0x60, 0x38, 0x93, 0x2a, 0x55, 0xce, 0x9e, 0x14, 0x3c, 0xc2, 0xc2, 0xfb, 0xdd, 0x80,
|
||||
0xce, 0x04, 0xc5, 0x25, 0x8a, 0x57, 0x1a, 0xa7, 0xcd, 0x35, 0x65, 0xbe, 0x64, 0x4d, 0xb5, 0x77,
|
||||
0xaf, 0x29, 0xab, 0x5e, 0x53, 0xb7, 0xc1, 0x9a, 0x08, 0xff, 0x78, 0x2c, 0x3d, 0x32, 0x99, 0x02,
|
||||
0xc4, 0xc6, 0x91, 0x9f, 0x05, 0x97, 0xa8, 0x77, 0x97, 0x46, 0x5b, 0x53, 0xb6, 0xb7, 0x63, 0xca,
|
||||
0xfe, 0x68, 0x40, 0xe7, 0x84, 0x17, 0x71, 0x9e, 0x6d, 0xb1, 0x70, 0x00, 0xfd, 0x51, 0x92, 0x84,
|
||||
0x81, 0xbf, 0xd1, 0x79, 0x0d, 0x11, 0xdd, 0x78, 0xdc, 0xc8, 0xaf, 0x8a, 0xad, 0x29, 0xa2, 0x71,
|
||||
0x73, 0x24, 0x37, 0x89, 0x5a, 0x0b, 0x8d, 0x71, 0xa3, 0x16, 0x88, 0x54, 0x52, 0x12, 0x46, 0x79,
|
||||
0x16, 0xcf, 0xc3, 0xf8, 0x4a, 0x46, 0xdb, 0x63, 0x15, 0xf6, 0xfe, 0x68, 0x41, 0xfb, 0xff, 0x9a,
|
||||
0xfe, 0x7b, 0x60, 0x04, 0xba, 0xd8, 0x46, 0x50, 0xed, 0x82, 0x6e, 0x63, 0x17, 0xb8, 0xd0, 0x2d,
|
||||
0x04, 0x8f, 0x16, 0x98, 0xba, 0x3d, 0x39, 0x5d, 0x4a, 0x28, 0x35, 0xb2, 0x8f, 0xd4, 0x12, 0xb0,
|
||||
0x59, 0x09, 0xab, 0xbe, 0x80, 0x46, 0x5f, 0x7c, 0xa4, 0xf7, 0x45, 0x5f, 0x7a, 0xe4, 0x6e, 0xa6,
|
||||
0xe5, 0xfa, 0x9a, 0xf8, 0xef, 0x66, 0xfa, 0x5f, 0x06, 0x58, 0x55, 0x53, 0x1d, 0x6d, 0x36, 0xd5,
|
||||
0x51, 0xdd, 0x54, 0xe3, 0xc3, 0xb2, 0xa9, 0xc6, 0x87, 0x84, 0xd9, 0x69, 0xd9, 0x54, 0xec, 0x94,
|
||||
0x8a, 0xf5, 0x50, 0xc4, 0x79, 0x72, 0x58, 0xa8, 0xaa, 0xda, 0xac, 0xc2, 0xc4, 0xc4, 0x6f, 0x97,
|
||||
0x28, 0x74, 0xaa, 0x6d, 0xa6, 0x11, 0xf1, 0xf6, 0x44, 0x0e, 0x1c, 0x95, 0x5c, 0x05, 0x9c, 0xf7,
|
||||
0xc0, 0x62, 0x94, 0x3c, 0x99, 0xe1, 0x8d, 0xba, 0x48, 0x31, 0x53, 0x5a, 0x32, 0xaa, 0xbe, 0x13,
|
||||
0x35, 0x81, 0xcb, 0xaf, 0xc6, 0x0f, 0xa1, 0x33, 0x59, 0x06, 0xf3, 0xac, 0xdc, 0xba, 0xaf, 0x37,
|
||||
0x06, 0x56, 0xb0, 0x42, 0xa9, 0x63, 0xfa, 0x8a, 0xf7, 0x0c, 0xec, 0x4a, 0x58, 0xbb, 0x63, 0x34,
|
||||
0xdd, 0x71, 0xa0, 0x7d, 0x1e, 0x05, 0x59, 0xd9, 0xba, 0x74, 0xa6, 0x60, 0x9f, 0xe5, 0x3c, 0xca,
|
||||
0x82, 0xac, 0x28, 0x5b, 0xb7, 0xc4, 0xde, 0x7d, 0xed, 0x3e, 0x99, 0x3b, 0x4f, 0x12, 0x14, 0x7a,
|
||||
0x0c, 0x28, 0x20, 0x1f, 0x89, 0xaf, 0x50, 0x4d, 0x70, 0x93, 0x29, 0xe0, 0x7d, 0x07, 0xf6, 0x28,
|
||||
0x44, 0x91, 0xb1, 0x3c, 0xc4, 0x5d, 0x9b, 0xf5, 0x9b, 0xc9, 0xd3, 0x27, 0xa5, 0x07, 0x74, 0xae,
|
||||
0x5b, 0xde, 0xbc, 0xd6, 0xf2, 0x8f, 0x78, 0xc2, 0x8f, 0xc7, 0x92, 0xe7, 0x26, 0xd3, 0xc8, 0xfb,
|
||||
0xd9, 0x80, 0x36, 0xcd, 0x96, 0x86, 0xe9, 0xf6, 0xcb, 0xe6, 0xd2, 0xa9, 0x88, 0x2f, 0x83, 0x19,
|
||||
0x8a, 0x32, 0xb8, 0x12, 0xcb, 0xa4, 0xfb, 0x4b, 0xac, 0x16, 0xb8, 0x46, 0xc4, 0x35, 0xfa, 0xa8,
|
||||
0x2c, 0x7b, 0xa9, 0xc1, 0x35, 0x12, 0x33, 0xa5, 0x74, 0xde, 0x06, 0x98, 0xe4, 0x09, 0x8a, 0xd1,
|
||||
0x6c, 0x15, 0x44, 0xb2, 0xe8, 0x3d, 0xd6, 0x90, 0x78, 0x0f, 0xd4, 0x67, 0xea, 0xd6, 0x84, 0x32,
|
||||
0x76, 0x7f, 0xd2, 0x5e, 0xf7, 0xdc, 0x0b, 0x37, 0x7f, 0xb7, 0x2b, 0x91, 0x5b, 0xd1, 0x0e, 0xa0,
|
||||
0xaf, 0xbf, 0xe9, 0xe5, 0x17, 0xb2, 0x1e, 0x56, 0x0d, 0x11, 0xc5, 0x7c, 0x9a, 0x4f, 0xc3, 0xc0,
|
||||
0x97, 0x31, 0xf7, 0x98, 0x46, 0xde, 0x01, 0x74, 0x8e, 0xe2, 0x68, 0x1e, 0x2c, 0x9c, 0x21, 0xb4,
|
||||
0x47, 0x79, 0xb6, 0x94, 0x2f, 0xf5, 0x0f, 0x6e, 0x37, 0x1a, 0x2d, 0xcf, 0x96, 0xea, 0x0e, 0x93,
|
||||
0x37, 0xbc, 0x2f, 0x00, 0x6a, 0x19, 0xfd, 0x51, 0xa8, 0xa3, 0x7f, 0x82, 0x57, 0x54, 0xa2, 0x54,
|
||||
0x5a, 0xe9, 0xb1, 0x1d, 0x1a, 0xef, 0x4b, 0xb0, 0x0f, 0xf3, 0x20, 0x9c, 0x1d, 0x47, 0xf3, 0x98,
|
||||
0x5a, 0xf5, 0x02, 0x45, 0x5a, 0xe7, 0xa7, 0x84, 0xe4, 0x30, 0x75, 0x6d, 0xc5, 0x59, 0x8d, 0xa6,
|
||||
0x1d, 0xf9, 0x5f, 0xeb, 0xfe, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x0f, 0x64, 0xa4, 0xff, 0x7d,
|
||||
0x0d, 0x00, 0x00,
|
||||
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,
|
||||
}
|
||||
|
|
|
@ -159,15 +159,22 @@ message User {
|
|||
}
|
||||
|
||||
message Role {
|
||||
string Organization = 1; // Organization is the ID of the organization that this user has a role in
|
||||
string Name = 2; // Name is the name of the role of this user in the respective organization
|
||||
string Organization = 1; // Organization is the ID of the organization that this user has a role in
|
||||
string Name = 2; // Name is the name of the role of this user in the respective organization
|
||||
}
|
||||
|
||||
message Mapping {
|
||||
string Provider = 1; // Provider is the provider that certifies and issues this user's authentication, e.g. GitHub
|
||||
string Scheme = 2; // Scheme is the scheme used to perform this user's authentication, e.g. OAuth2 or LDAP
|
||||
string ProviderOrganization = 3; // ProviderOrganization is the group or organizations that you are a part of in an auth provider
|
||||
string ID = 4; // ID is the unique ID for the mapping
|
||||
string Organization = 5; // Organization is the organization ID that resource belongs to
|
||||
}
|
||||
|
||||
message Organization {
|
||||
string ID = 1; // ID is the unique ID of the organization
|
||||
string Name = 2; // Name is the organization's name
|
||||
string DefaultRole = 3; // DefaultRole is the name of the role that is the default for any users added to the organization
|
||||
bool Public = 4; // Public specifies that users must be explicitly added to the organization
|
||||
string ID = 1; // ID is the unique ID of the organization
|
||||
string Name = 2; // Name is the organization's name
|
||||
string DefaultRole = 3; // DefaultRole is the name of the role that is the default for any users added to the organization
|
||||
}
|
||||
|
||||
message Config {
|
||||
|
@ -179,8 +186,8 @@ message AuthConfig {
|
|||
}
|
||||
|
||||
message BuildInfo {
|
||||
string Version = 1; // Version is a descriptive git SHA identifier
|
||||
string Commit = 2; // Commit is an abbreviated SHA
|
||||
string Version = 1; // Version is a descriptive git SHA identifier
|
||||
string Commit = 2; // Commit is an abbreviated SHA
|
||||
}
|
||||
|
||||
// The following is a vim modeline, it autoconfigures vim to have the
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
package bolt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/bolt/internal"
|
||||
)
|
||||
|
||||
// Ensure MappingsStore implements chronograf.MappingsStore.
|
||||
var _ chronograf.MappingsStore = &MappingsStore{}
|
||||
|
||||
var (
|
||||
// MappingsBucket is the bucket where organizations are stored.
|
||||
MappingsBucket = []byte("MappingsV1")
|
||||
)
|
||||
|
||||
// MappingsStore uses bolt to store and retrieve Mappings
|
||||
type MappingsStore struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// Migrate sets the default organization at runtime
|
||||
func (s *MappingsStore) Migrate(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add creates a new Mapping in the MappingsStore
|
||||
func (s *MappingsStore) Add(ctx context.Context, o *chronograf.Mapping) (*chronograf.Mapping, error) {
|
||||
err := s.client.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket(MappingsBucket)
|
||||
seq, err := b.NextSequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.ID = fmt.Sprintf("%d", seq)
|
||||
|
||||
v, err := internal.MarshalMapping(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.Put([]byte(o.ID), v)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// All returns all known organizations
|
||||
func (s *MappingsStore) All(ctx context.Context) ([]chronograf.Mapping, error) {
|
||||
var mappings []chronograf.Mapping
|
||||
err := s.each(ctx, func(m *chronograf.Mapping) {
|
||||
mappings = append(mappings, *m)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mappings, nil
|
||||
}
|
||||
|
||||
// Delete the organization from MappingsStore
|
||||
func (s *MappingsStore) Delete(ctx context.Context, o *chronograf.Mapping) error {
|
||||
_, err := s.get(ctx, o.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.client.db.Update(func(tx *bolt.Tx) error {
|
||||
return tx.Bucket(MappingsBucket).Delete([]byte(o.ID))
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MappingsStore) get(ctx context.Context, id string) (*chronograf.Mapping, error) {
|
||||
var o chronograf.Mapping
|
||||
err := s.client.db.View(func(tx *bolt.Tx) error {
|
||||
v := tx.Bucket(MappingsBucket).Get([]byte(id))
|
||||
if v == nil {
|
||||
return chronograf.ErrMappingNotFound
|
||||
}
|
||||
return internal.UnmarshalMapping(v, &o)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &o, nil
|
||||
}
|
||||
|
||||
func (s *MappingsStore) each(ctx context.Context, fn func(*chronograf.Mapping)) error {
|
||||
return s.client.db.View(func(tx *bolt.Tx) error {
|
||||
return tx.Bucket(MappingsBucket).ForEach(func(k, v []byte) error {
|
||||
var m chronograf.Mapping
|
||||
if err := internal.UnmarshalMapping(v, &m); err != nil {
|
||||
return err
|
||||
}
|
||||
fn(&m)
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Get returns a Mapping if the id exists.
|
||||
func (s *MappingsStore) Get(ctx context.Context, id string) (*chronograf.Mapping, error) {
|
||||
return s.get(ctx, id)
|
||||
}
|
||||
|
||||
// Update the organization in MappingsStore
|
||||
func (s *MappingsStore) Update(ctx context.Context, o *chronograf.Mapping) error {
|
||||
return s.client.db.Update(func(tx *bolt.Tx) error {
|
||||
if v, err := internal.MarshalMapping(o); err != nil {
|
||||
return err
|
||||
} else if err := tx.Bucket(MappingsBucket).Put([]byte(o.ID), v); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
|
@ -0,0 +1,483 @@
|
|||
package bolt_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
var mappingCmpOptions = cmp.Options{
|
||||
cmpopts.IgnoreFields(chronograf.Mapping{}, "ID"),
|
||||
cmpopts.EquateEmpty(),
|
||||
}
|
||||
|
||||
func TestMappingStore_Add(t *testing.T) {
|
||||
type fields struct {
|
||||
mappings []*chronograf.Mapping
|
||||
}
|
||||
type args struct {
|
||||
mapping *chronograf.Mapping
|
||||
}
|
||||
type wants struct {
|
||||
mapping *chronograf.Mapping
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "default with wildcards",
|
||||
args: args{
|
||||
mapping: &chronograf.Mapping{
|
||||
Organization: "default",
|
||||
Provider: "*",
|
||||
Scheme: "*",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
mapping: &chronograf.Mapping{
|
||||
Organization: "default",
|
||||
Provider: "*",
|
||||
Scheme: "*",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "simple",
|
||||
args: args{
|
||||
mapping: &chronograf.Mapping{
|
||||
Organization: "default",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
ProviderOrganization: "idk",
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
mapping: &chronograf.Mapping{
|
||||
Organization: "default",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
ProviderOrganization: "idk",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client, err := NewTestClient()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
s := client.MappingsStore
|
||||
ctx := context.Background()
|
||||
|
||||
for _, mapping := range tt.fields.mappings {
|
||||
// YOLO database prepopulation
|
||||
_, _ = s.Add(ctx, mapping)
|
||||
}
|
||||
|
||||
tt.args.mapping, err = s.Add(ctx, tt.args.mapping)
|
||||
|
||||
if (err != nil) != (tt.wants.err != nil) {
|
||||
t.Errorf("MappingsStore.Add() error = %v, want error %v", err, tt.wants.err)
|
||||
return
|
||||
}
|
||||
|
||||
got, err := s.Get(ctx, tt.args.mapping.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get mapping: %v", err)
|
||||
return
|
||||
}
|
||||
if diff := cmp.Diff(got, tt.wants.mapping, mappingCmpOptions...); diff != "" {
|
||||
t.Errorf("MappingStore.Add():\n-got/+want\ndiff %s", diff)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMappingStore_All(t *testing.T) {
|
||||
type fields struct {
|
||||
mappings []*chronograf.Mapping
|
||||
}
|
||||
type args struct {
|
||||
}
|
||||
type wants struct {
|
||||
mappings []chronograf.Mapping
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "simple",
|
||||
fields: fields{
|
||||
mappings: []*chronograf.Mapping{
|
||||
&chronograf.Mapping{
|
||||
Organization: "0",
|
||||
Provider: "google",
|
||||
Scheme: "ldap",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
mappings: []chronograf.Mapping{
|
||||
chronograf.Mapping{
|
||||
Organization: "0",
|
||||
Provider: "google",
|
||||
Scheme: "ldap",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
chronograf.Mapping{
|
||||
Organization: "default",
|
||||
Provider: "*",
|
||||
Scheme: "*",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client, err := NewTestClient()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
s := client.MappingsStore
|
||||
ctx := context.Background()
|
||||
|
||||
for _, mapping := range tt.fields.mappings {
|
||||
// YOLO database prepopulation
|
||||
_, _ = s.Add(ctx, mapping)
|
||||
}
|
||||
|
||||
got, err := s.All(ctx)
|
||||
|
||||
if (err != nil) != (tt.wants.err != nil) {
|
||||
t.Errorf("MappingsStore.All() error = %v, want error %v", err, tt.wants.err)
|
||||
return
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(got, tt.wants.mappings, mappingCmpOptions...); diff != "" {
|
||||
t.Errorf("MappingStore.All():\n-got/+want\ndiff %s", diff)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMappingStore_Delete(t *testing.T) {
|
||||
type fields struct {
|
||||
mappings []*chronograf.Mapping
|
||||
}
|
||||
type args struct {
|
||||
mapping *chronograf.Mapping
|
||||
}
|
||||
type wants struct {
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "simple",
|
||||
fields: fields{
|
||||
mappings: []*chronograf.Mapping{
|
||||
&chronograf.Mapping{
|
||||
Organization: "default",
|
||||
Provider: "*",
|
||||
Scheme: "*",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
&chronograf.Mapping{
|
||||
Organization: "0",
|
||||
Provider: "google",
|
||||
Scheme: "ldap",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
mapping: &chronograf.Mapping{
|
||||
ID: "1",
|
||||
Organization: "default",
|
||||
Provider: "*",
|
||||
Scheme: "*",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mapping not found",
|
||||
fields: fields{
|
||||
mappings: []*chronograf.Mapping{
|
||||
&chronograf.Mapping{
|
||||
Organization: "default",
|
||||
Provider: "*",
|
||||
Scheme: "*",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
&chronograf.Mapping{
|
||||
Organization: "0",
|
||||
Provider: "google",
|
||||
Scheme: "ldap",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
mapping: &chronograf.Mapping{
|
||||
ID: "0",
|
||||
Organization: "default",
|
||||
Provider: "*",
|
||||
Scheme: "*",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
err: chronograf.ErrMappingNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client, err := NewTestClient()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
s := client.MappingsStore
|
||||
ctx := context.Background()
|
||||
|
||||
for _, mapping := range tt.fields.mappings {
|
||||
// YOLO database prepopulation
|
||||
_, _ = s.Add(ctx, mapping)
|
||||
}
|
||||
|
||||
err = s.Delete(ctx, tt.args.mapping)
|
||||
|
||||
if (err != nil) != (tt.wants.err != nil) {
|
||||
t.Errorf("MappingsStore.Delete() error = %v, want error %v", err, tt.wants.err)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMappingStore_Get(t *testing.T) {
|
||||
type fields struct {
|
||||
mappings []*chronograf.Mapping
|
||||
}
|
||||
type args struct {
|
||||
mappingID string
|
||||
}
|
||||
type wants struct {
|
||||
mapping *chronograf.Mapping
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "simple",
|
||||
fields: fields{
|
||||
mappings: []*chronograf.Mapping{
|
||||
&chronograf.Mapping{
|
||||
Organization: "default",
|
||||
Provider: "*",
|
||||
Scheme: "*",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
&chronograf.Mapping{
|
||||
Organization: "0",
|
||||
Provider: "google",
|
||||
Scheme: "ldap",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
mappingID: "1",
|
||||
},
|
||||
wants: wants{
|
||||
mapping: &chronograf.Mapping{
|
||||
ID: "1",
|
||||
Organization: "default",
|
||||
Provider: "*",
|
||||
Scheme: "*",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mapping not found",
|
||||
fields: fields{
|
||||
mappings: []*chronograf.Mapping{
|
||||
&chronograf.Mapping{
|
||||
Organization: "default",
|
||||
Provider: "*",
|
||||
Scheme: "*",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
&chronograf.Mapping{
|
||||
Organization: "0",
|
||||
Provider: "google",
|
||||
Scheme: "ldap",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
mappingID: "0",
|
||||
},
|
||||
wants: wants{
|
||||
err: chronograf.ErrMappingNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client, err := NewTestClient()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
s := client.MappingsStore
|
||||
ctx := context.Background()
|
||||
|
||||
for _, mapping := range tt.fields.mappings {
|
||||
// YOLO database prepopulation
|
||||
_, _ = s.Add(ctx, mapping)
|
||||
}
|
||||
|
||||
got, err := s.Get(ctx, tt.args.mappingID)
|
||||
if (err != nil) != (tt.wants.err != nil) {
|
||||
t.Errorf("MappingsStore.Get() error = %v, want error %v", err, tt.wants.err)
|
||||
return
|
||||
}
|
||||
if diff := cmp.Diff(got, tt.wants.mapping, mappingCmpOptions...); diff != "" {
|
||||
t.Errorf("MappingStore.Get():\n-got/+want\ndiff %s", diff)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMappingStore_Update(t *testing.T) {
|
||||
type fields struct {
|
||||
mappings []*chronograf.Mapping
|
||||
}
|
||||
type args struct {
|
||||
mapping *chronograf.Mapping
|
||||
}
|
||||
type wants struct {
|
||||
mapping *chronograf.Mapping
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "simple",
|
||||
fields: fields{
|
||||
mappings: []*chronograf.Mapping{
|
||||
&chronograf.Mapping{
|
||||
Organization: "default",
|
||||
Provider: "*",
|
||||
Scheme: "*",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
&chronograf.Mapping{
|
||||
Organization: "0",
|
||||
Provider: "google",
|
||||
Scheme: "ldap",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
mapping: &chronograf.Mapping{
|
||||
ID: "1",
|
||||
Organization: "default",
|
||||
Provider: "cool",
|
||||
Scheme: "it",
|
||||
ProviderOrganization: "works",
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
mapping: &chronograf.Mapping{
|
||||
ID: "1",
|
||||
Organization: "default",
|
||||
Provider: "cool",
|
||||
Scheme: "it",
|
||||
ProviderOrganization: "works",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client, err := NewTestClient()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
s := client.MappingsStore
|
||||
ctx := context.Background()
|
||||
|
||||
for _, mapping := range tt.fields.mappings {
|
||||
// YOLO database prepopulation
|
||||
_, _ = s.Add(ctx, mapping)
|
||||
}
|
||||
|
||||
err = s.Update(ctx, tt.args.mapping)
|
||||
if (err != nil) != (tt.wants.err != nil) {
|
||||
t.Errorf("MappingsStore.Update() error = %v, want error %v", err, tt.wants.err)
|
||||
return
|
||||
}
|
||||
if diff := cmp.Diff(tt.args.mapping, tt.wants.mapping, mappingCmpOptions...); diff != "" {
|
||||
t.Errorf("MappingStore.Update():\n-got/+want\ndiff %s", diff)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -25,8 +25,6 @@ const (
|
|||
DefaultOrganizationName string = "Default"
|
||||
// DefaultOrganizationRole is the DefaultRole for the Default organization
|
||||
DefaultOrganizationRole string = "member"
|
||||
// DefaultOrganizationPublic is the Public setting for the Default organization.
|
||||
DefaultOrganizationPublic bool = true
|
||||
)
|
||||
|
||||
// OrganizationsStore uses bolt to store and retrieve Organizations
|
||||
|
@ -45,7 +43,14 @@ func (s *OrganizationsStore) CreateDefault(ctx context.Context) error {
|
|||
ID: string(DefaultOrganizationID),
|
||||
Name: DefaultOrganizationName,
|
||||
DefaultRole: DefaultOrganizationRole,
|
||||
Public: DefaultOrganizationPublic,
|
||||
}
|
||||
|
||||
m := chronograf.Mapping{
|
||||
ID: string(DefaultOrganizationID),
|
||||
Organization: string(DefaultOrganizationID),
|
||||
Provider: chronograf.MappingWildcard,
|
||||
Scheme: chronograf.MappingWildcard,
|
||||
ProviderOrganization: chronograf.MappingWildcard,
|
||||
}
|
||||
return s.client.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket(OrganizationsBucket)
|
||||
|
@ -59,6 +64,17 @@ func (s *OrganizationsStore) CreateDefault(ctx context.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
b = tx.Bucket(MappingsBucket)
|
||||
v = b.Get(DefaultOrganizationID)
|
||||
if v != nil {
|
||||
return nil
|
||||
}
|
||||
if v, err := internal.MarshalMapping(&m); err != nil {
|
||||
return err
|
||||
} else if err := b.Put(DefaultOrganizationID, v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
@ -189,6 +205,18 @@ func (s *OrganizationsStore) Delete(ctx context.Context, o *chronograf.Organizat
|
|||
}
|
||||
}
|
||||
|
||||
mappings, err := s.client.MappingsStore.All(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, mapping := range mappings {
|
||||
if mapping.Organization == o.ID {
|
||||
if err := s.client.MappingsStore.Delete(ctx, &mapping); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -170,12 +170,10 @@ func TestOrganizationsStore_All(t *testing.T) {
|
|||
{
|
||||
Name: "EE - Evil Empire",
|
||||
DefaultRole: roles.MemberRoleName,
|
||||
Public: true,
|
||||
},
|
||||
{
|
||||
Name: "The Good Place",
|
||||
DefaultRole: roles.EditorRoleName,
|
||||
Public: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -183,17 +181,14 @@ func TestOrganizationsStore_All(t *testing.T) {
|
|||
{
|
||||
Name: "EE - Evil Empire",
|
||||
DefaultRole: roles.MemberRoleName,
|
||||
Public: true,
|
||||
},
|
||||
{
|
||||
Name: "The Good Place",
|
||||
DefaultRole: roles.EditorRoleName,
|
||||
Public: true,
|
||||
},
|
||||
{
|
||||
Name: bolt.DefaultOrganizationName,
|
||||
DefaultRole: bolt.DefaultOrganizationRole,
|
||||
Public: bolt.DefaultOrganizationPublic,
|
||||
},
|
||||
},
|
||||
addFirst: true,
|
||||
|
@ -316,52 +311,63 @@ func TestOrganizationsStore_Update(t *testing.T) {
|
|||
addFirst: true,
|
||||
},
|
||||
{
|
||||
name: "Update organization name, role, public",
|
||||
name: "Update organization name, role",
|
||||
fields: fields{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
initial: &chronograf.Organization{
|
||||
Name: "The Good Place",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: false,
|
||||
},
|
||||
updates: &chronograf.Organization{
|
||||
Name: "The Bad Place",
|
||||
Public: true,
|
||||
DefaultRole: roles.AdminRoleName,
|
||||
},
|
||||
},
|
||||
want: &chronograf.Organization{
|
||||
Name: "The Bad Place",
|
||||
Public: true,
|
||||
DefaultRole: roles.AdminRoleName,
|
||||
},
|
||||
addFirst: true,
|
||||
},
|
||||
{
|
||||
name: "Update organization name and public",
|
||||
name: "Update organization name",
|
||||
fields: fields{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
initial: &chronograf.Organization{
|
||||
Name: "The Good Place",
|
||||
DefaultRole: roles.EditorRoleName,
|
||||
Public: false,
|
||||
},
|
||||
updates: &chronograf.Organization{
|
||||
Name: "The Bad Place",
|
||||
Public: true,
|
||||
Name: "The Bad Place",
|
||||
},
|
||||
},
|
||||
want: &chronograf.Organization{
|
||||
Name: "The Bad Place",
|
||||
DefaultRole: roles.EditorRoleName,
|
||||
Public: true,
|
||||
},
|
||||
addFirst: true,
|
||||
},
|
||||
{
|
||||
name: "Update organization name - organization already exists",
|
||||
name: "Update organization name",
|
||||
fields: fields{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
initial: &chronograf.Organization{
|
||||
Name: "The Good Place",
|
||||
},
|
||||
updates: &chronograf.Organization{
|
||||
Name: "The Bad Place",
|
||||
},
|
||||
},
|
||||
want: &chronograf.Organization{
|
||||
Name: "The Bad Place",
|
||||
},
|
||||
addFirst: true,
|
||||
},
|
||||
{
|
||||
name: "Update organization name - name already taken",
|
||||
fields: fields{
|
||||
orgs: []chronograf.Organization{
|
||||
{
|
||||
|
@ -409,10 +415,6 @@ func TestOrganizationsStore_Update(t *testing.T) {
|
|||
tt.args.initial.DefaultRole = tt.args.updates.DefaultRole
|
||||
}
|
||||
|
||||
if tt.args.updates.Public != tt.args.initial.Public {
|
||||
tt.args.initial.Public = tt.args.updates.Public
|
||||
}
|
||||
|
||||
if err := s.Update(tt.args.ctx, tt.args.initial); (err != nil) != tt.wantErr {
|
||||
t.Errorf("%q. OrganizationsStore.Update() error = %v, wantErr %v", tt.name, err, tt.wantErr)
|
||||
}
|
||||
|
@ -618,7 +620,6 @@ func TestOrganizationsStore_DefaultOrganization(t *testing.T) {
|
|||
ID: string(bolt.DefaultOrganizationID),
|
||||
Name: bolt.DefaultOrganizationName,
|
||||
DefaultRole: bolt.DefaultOrganizationRole,
|
||||
Public: bolt.DefaultOrganizationPublic,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
|
|
@ -30,6 +30,7 @@ const (
|
|||
ErrInvalidLegendOrient = Error("Invalid orientation type. Valid orientation types are 'top', 'bottom', 'right', 'left'")
|
||||
ErrUserAlreadyExists = Error("user already exists")
|
||||
ErrOrganizationNotFound = Error("organization not found")
|
||||
ErrMappingNotFound = Error("mapping not found")
|
||||
ErrOrganizationAlreadyExists = Error("organization already exists")
|
||||
ErrCannotDeleteDefaultOrganization = Error("cannot delete default organization")
|
||||
ErrConfigNotFound = Error("cannot find configuration")
|
||||
|
@ -415,7 +416,7 @@ type User struct {
|
|||
Name string `json:"name"`
|
||||
Passwd string `json:"password,omitempty"`
|
||||
Permissions Permissions `json:"permissions,omitempty"`
|
||||
Roles []Role `json:"roles,omitempty"`
|
||||
Roles []Role `json:"roles"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Scheme string `json:"scheme,omitempty"`
|
||||
SuperAdmin bool `json:"superAdmin,omitempty"`
|
||||
|
@ -605,15 +606,52 @@ type LayoutsStore interface {
|
|||
Update(context.Context, Layout) error
|
||||
}
|
||||
|
||||
// MappingWildcard is the wildcard value for mappings
|
||||
const MappingWildcard string = "*"
|
||||
|
||||
// A Mapping is the structure that is used to determine a users
|
||||
// role within an organization. The high level idea is to grant
|
||||
// certain roles to certain users without them having to be given
|
||||
// explicit role within the organization.
|
||||
//
|
||||
// One can think of a mapping like so:
|
||||
// Provider:Scheme:Group -> Organization
|
||||
// github:oauth2:influxdata -> Happy
|
||||
// beyondcorp:ldap:influxdata -> TheBillHilliettas
|
||||
//
|
||||
// Any of Provider, Scheme, or Group may be provided as a wildcard *
|
||||
// github:oauth2:* -> MyOrg
|
||||
// *:*:* -> AllOrg
|
||||
type Mapping struct {
|
||||
ID string `json:"id"`
|
||||
Organization string `json:"organizationId"`
|
||||
Provider string `json:"provider"`
|
||||
Scheme string `json:"scheme"`
|
||||
ProviderOrganization string `json:"providerOrganization"`
|
||||
}
|
||||
|
||||
// MappingsStore is the storage and retrieval of Mappings
|
||||
type MappingsStore interface {
|
||||
// Add creates a new Mapping.
|
||||
// The Created mapping is returned back to the user with the
|
||||
// ID field populated.
|
||||
Add(context.Context, *Mapping) (*Mapping, error)
|
||||
// All lists all Mapping in the MappingsStore
|
||||
All(context.Context) ([]Mapping, error)
|
||||
// Delete removes an Mapping from the MappingsStore
|
||||
Delete(context.Context, *Mapping) error
|
||||
// Get retrieves an Mapping from the MappingsStore
|
||||
Get(context.Context, string) (*Mapping, error)
|
||||
// Update updates an Mapping in the MappingsStore
|
||||
Update(context.Context, *Mapping) error
|
||||
}
|
||||
|
||||
// Organization is a group of resources under a common name
|
||||
type Organization struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
// DefaultRole is the name of the role that is the default for any users added to the organization
|
||||
DefaultRole string `json:"defaultRole,omitempty"`
|
||||
// Public specifies whether users must be explicitly added to the organization.
|
||||
// It is currently only used by the default organization, but that may change in the future.
|
||||
Public bool `json:"public"`
|
||||
}
|
||||
|
||||
// OrganizationQuery represents the attributes that a organization may be retrieved by.
|
||||
|
|
|
@ -31,6 +31,7 @@ deployment:
|
|||
--upload
|
||||
- sudo chown -R ubuntu:ubuntu /home/ubuntu
|
||||
- cp build/linux/static_amd64/chronograf .
|
||||
- cp build/linux/static_amd64/chronoctl .
|
||||
- docker build -t chronograf .
|
||||
- docker login -e $QUAY_EMAIL -u "$QUAY_USER" -p $QUAY_PASS quay.io
|
||||
- docker tag chronograf quay.io/influxdb/chronograf:${CIRCLE_SHA1:0:7}
|
||||
|
@ -52,6 +53,7 @@ deployment:
|
|||
--bucket dl.influxdata.com/chronograf/releases
|
||||
- sudo chown -R ubuntu:ubuntu /home/ubuntu
|
||||
- cp build/linux/static_amd64/chronograf .
|
||||
- cp build/linux/static_amd64/chronoctl .
|
||||
- docker build -t chronograf .
|
||||
- docker login -e $QUAY_EMAIL -u "$QUAY_USER" -p $QUAY_PASS quay.io
|
||||
- docker tag chronograf quay.io/influxdb/chronograf:${CIRCLE_SHA1:0:7}
|
||||
|
@ -75,6 +77,7 @@ deployment:
|
|||
--bucket dl.influxdata.com/chronograf/releases
|
||||
- sudo chown -R ubuntu:ubuntu /home/ubuntu
|
||||
- cp build/linux/static_amd64/chronograf .
|
||||
- cp build/linux/static_amd64/chronoctl .
|
||||
- docker build -t chronograf .
|
||||
- docker login -e $QUAY_EMAIL -u "$QUAY_USER" -p $QUAY_PASS quay.io
|
||||
- docker tag chronograf quay.io/influxdb/chronograf:${CIRCLE_SHA1:0:7}
|
||||
|
|
|
@ -2,17 +2,18 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
type AddCommand struct {
|
||||
BoltPath string `short:"b" long:"bolt-path" description:"Full path to boltDB file (e.g. './chronograf-v1.db')" env:"BOLT_PATH" default:"chronograf-v1.db"`
|
||||
ID *uint64 `short:"i" long:"id" description:"Users ID. Must be id for existing user"`
|
||||
Username string `short:"n" long:"name" description:"Users name. Must be Oauth-able email address or username"`
|
||||
Provider string `short:"p" long:"provider" description:"Name of the Auth provider (e.g. google, github, auth0, or generic)"`
|
||||
Scheme string `short:"s" long:"scheme" description:"Authentication scheme that matches auth provider (e.g. oauth or ldap)"`
|
||||
//Organizations string `short:"o" long:"orgs" description:"A comma separated list of organizations that the user should be added to"`
|
||||
BoltPath string `short:"b" long:"bolt-path" description:"Full path to boltDB file (e.g. './chronograf-v1.db')" env:"BOLT_PATH" default:"chronograf-v1.db"`
|
||||
ID *uint64 `short:"i" long:"id" description:"Users ID. Must be id for existing user"`
|
||||
Username string `short:"n" long:"name" description:"Users name. Must be Oauth-able email address or username"`
|
||||
Provider string `short:"p" long:"provider" description:"Name of the Auth provider (e.g. google, github, auth0, or generic)"`
|
||||
Scheme string `short:"s" long:"scheme" description:"Authentication scheme that matches auth provider (e.g. oauth2)" default:"oauth2"`
|
||||
Organizations string `short:"o" long:"orgs" description:"A comma separated list of organizations that the user should be added to" default:"default"`
|
||||
}
|
||||
|
||||
var addCommand AddCommand
|
||||
|
@ -41,9 +42,15 @@ func (l *AddCommand) Execute(args []string) error {
|
|||
return err
|
||||
} else if err == chronograf.ErrUserNotFound {
|
||||
user = &chronograf.User{
|
||||
Name: l.Username,
|
||||
Provider: l.Provider,
|
||||
Scheme: l.Scheme,
|
||||
Name: l.Username,
|
||||
Provider: l.Provider,
|
||||
Scheme: l.Scheme,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: "member",
|
||||
Organization: "default",
|
||||
},
|
||||
},
|
||||
SuperAdmin: true,
|
||||
}
|
||||
|
||||
|
@ -53,13 +60,49 @@ func (l *AddCommand) Execute(args []string) error {
|
|||
}
|
||||
} else {
|
||||
user.SuperAdmin = true
|
||||
if len(user.Roles) == 0 {
|
||||
user.Roles = []chronograf.Role{
|
||||
{
|
||||
Name: "member",
|
||||
Organization: "default",
|
||||
},
|
||||
}
|
||||
}
|
||||
if err = c.UsersStore.Update(ctx, user); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(desa): Apply mapping to user and update their roles
|
||||
// TODO(desa): Add a flag that allows the user to specify an organization to join
|
||||
roles := []chronograf.Role{}
|
||||
OrgLoop:
|
||||
for _, org := range strings.Split(l.Organizations, ",") {
|
||||
// Check to see is user is already a part of the organization
|
||||
for _, r := range user.Roles {
|
||||
if r.Organization == org {
|
||||
continue OrgLoop
|
||||
}
|
||||
}
|
||||
|
||||
orgQuery := chronograf.OrganizationQuery{
|
||||
ID: &org,
|
||||
}
|
||||
o, err := c.OrganizationsStore.Get(ctx, orgQuery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
role := chronograf.Role{
|
||||
Organization: org,
|
||||
Name: o.DefaultRole,
|
||||
}
|
||||
roles = append(roles, role)
|
||||
}
|
||||
|
||||
user.Roles = append(user.Roles, roles...)
|
||||
if err = c.UsersStore.Update(ctx, user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w := NewTabWriter()
|
||||
WriteHeaders(w)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/jessevdk/go-flags"
|
||||
|
@ -18,8 +19,9 @@ func main() {
|
|||
if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp {
|
||||
os.Exit(0)
|
||||
} else {
|
||||
fmt.Fprintln(os.Stdout)
|
||||
parser.WriteHelp(os.Stdout)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -51,13 +51,13 @@ type Client struct {
|
|||
}
|
||||
|
||||
// NewClientWithTimeSeries initializes a Client with a known set of TimeSeries.
|
||||
func NewClientWithTimeSeries(lg chronograf.Logger, mu string, authorizer influx.Authorizer, tls bool, series ...chronograf.TimeSeries) (*Client, error) {
|
||||
func NewClientWithTimeSeries(lg chronograf.Logger, mu string, authorizer influx.Authorizer, tls, insecure bool, series ...chronograf.TimeSeries) (*Client, error) {
|
||||
metaURL, err := parseMetaURL(mu, tls)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctrl := NewMetaClient(metaURL, authorizer)
|
||||
ctrl := NewMetaClient(metaURL, insecure, authorizer)
|
||||
c := &Client{
|
||||
Ctrl: ctrl,
|
||||
UsersStore: &UserStore{
|
||||
|
@ -85,13 +85,13 @@ func NewClientWithTimeSeries(lg chronograf.Logger, mu string, authorizer influx.
|
|||
// varieties. TLS is used when the URL contains "https" or when the TLS
|
||||
// parameter is set. authorizer will add the correct `Authorization` headers
|
||||
// on the out-bound request.
|
||||
func NewClientWithURL(mu string, authorizer influx.Authorizer, tls bool, lg chronograf.Logger) (*Client, error) {
|
||||
func NewClientWithURL(mu string, authorizer influx.Authorizer, tls bool, insecure bool, lg chronograf.Logger) (*Client, error) {
|
||||
metaURL, err := parseMetaURL(mu, tls)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctrl := NewMetaClient(metaURL, authorizer)
|
||||
ctrl := NewMetaClient(metaURL, insecure, authorizer)
|
||||
return &Client{
|
||||
Ctrl: ctrl,
|
||||
UsersStore: &UserStore{
|
||||
|
|
|
@ -84,6 +84,7 @@ func Test_Enterprise_AdvancesDataNodes(t *testing.T) {
|
|||
Password: "thelake",
|
||||
},
|
||||
false,
|
||||
false,
|
||||
chronograf.TimeSeries(m1),
|
||||
chronograf.TimeSeries(m2))
|
||||
if err != nil {
|
||||
|
@ -114,23 +115,53 @@ func Test_Enterprise_NewClientWithURL(t *testing.T) {
|
|||
t.Parallel()
|
||||
|
||||
urls := []struct {
|
||||
url string
|
||||
username string
|
||||
password string
|
||||
tls bool
|
||||
shouldErr bool
|
||||
name string
|
||||
url string
|
||||
username string
|
||||
password string
|
||||
tls bool
|
||||
insecureSkipVerify bool
|
||||
wantErr bool
|
||||
}{
|
||||
{"http://localhost:8086", "", "", false, false},
|
||||
{"https://localhost:8086", "", "", false, false},
|
||||
{"http://localhost:8086", "username", "password", false, false},
|
||||
|
||||
{"http://localhost:8086", "", "", true, false},
|
||||
{"https://localhost:8086", "", "", true, false},
|
||||
|
||||
{"localhost:8086", "", "", false, false},
|
||||
{"localhost:8086", "", "", true, false},
|
||||
|
||||
{":http", "", "", false, true},
|
||||
{
|
||||
name: "no tls should have no error",
|
||||
url: "http://localhost:8086",
|
||||
},
|
||||
{
|
||||
name: "tls sholuld have no error",
|
||||
url: "https://localhost:8086",
|
||||
},
|
||||
{
|
||||
name: "no tls but with basic auth",
|
||||
url: "http://localhost:8086",
|
||||
username: "username",
|
||||
password: "password",
|
||||
},
|
||||
{
|
||||
name: "tls request but url is not tls should not error",
|
||||
url: "http://localhost:8086",
|
||||
tls: true,
|
||||
},
|
||||
{
|
||||
name: "https with tls and with insecureSkipVerify should not error",
|
||||
url: "https://localhost:8086",
|
||||
tls: true,
|
||||
insecureSkipVerify: true,
|
||||
},
|
||||
{
|
||||
name: "URL does not require http or https",
|
||||
url: "localhost:8086",
|
||||
},
|
||||
{
|
||||
name: "URL with TLS request should not error",
|
||||
url: "localhost:8086",
|
||||
tls: true,
|
||||
},
|
||||
{
|
||||
name: "invalid URL causes error",
|
||||
url: ":http",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testURL := range urls {
|
||||
|
@ -141,10 +172,11 @@ func Test_Enterprise_NewClientWithURL(t *testing.T) {
|
|||
Password: testURL.password,
|
||||
},
|
||||
testURL.tls,
|
||||
testURL.insecureSkipVerify,
|
||||
log.New(log.DebugLevel))
|
||||
if err != nil && !testURL.shouldErr {
|
||||
if err != nil && !testURL.wantErr {
|
||||
t.Errorf("Unexpected error creating Client with URL %s and TLS preference %t. err: %s", testURL.url, testURL.tls, err.Error())
|
||||
} else if err == nil && testURL.shouldErr {
|
||||
} else if err == nil && testURL.wantErr {
|
||||
t.Errorf("Expected error creating Client with URL %s and TLS preference %t", testURL.url, testURL.tls)
|
||||
}
|
||||
}
|
||||
|
@ -159,7 +191,7 @@ func Test_Enterprise_ComplainsIfNotOpened(t *testing.T) {
|
|||
Username: "docbrown",
|
||||
Password: "1.21 gigawatts",
|
||||
},
|
||||
false, chronograf.TimeSeries(m1))
|
||||
false, false, chronograf.TimeSeries(m1))
|
||||
if err != nil {
|
||||
t.Error("Expected ErrUnitialized, but was this err:", err)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package enterprise
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
@ -14,6 +15,14 @@ import (
|
|||
"github.com/influxdata/chronograf/influx"
|
||||
)
|
||||
|
||||
// Shared transports for all clients to prevent leaking connections
|
||||
var (
|
||||
skipVerifyTransport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
defaultTransport = &http.Transport{}
|
||||
)
|
||||
|
||||
type client interface {
|
||||
Do(URL *url.URL, path, method string, authorizer influx.Authorizer, params map[string]string, body io.Reader) (*http.Response, error)
|
||||
}
|
||||
|
@ -26,10 +35,12 @@ type MetaClient struct {
|
|||
}
|
||||
|
||||
// NewMetaClient represents a meta node in an Influx Enterprise cluster
|
||||
func NewMetaClient(url *url.URL, authorizer influx.Authorizer) *MetaClient {
|
||||
func NewMetaClient(url *url.URL, InsecureSkipVerify bool, authorizer influx.Authorizer) *MetaClient {
|
||||
return &MetaClient{
|
||||
URL: url,
|
||||
client: &defaultClient{},
|
||||
URL: url,
|
||||
client: &defaultClient{
|
||||
InsecureSkipVerify: InsecureSkipVerify,
|
||||
},
|
||||
authorizer: authorizer,
|
||||
}
|
||||
}
|
||||
|
@ -399,7 +410,8 @@ func (m *MetaClient) Post(ctx context.Context, path string, action interface{},
|
|||
}
|
||||
|
||||
type defaultClient struct {
|
||||
Leader string
|
||||
Leader string
|
||||
InsecureSkipVerify bool
|
||||
}
|
||||
|
||||
// Do is a helper function to interface with Influx Enterprise's Meta API
|
||||
|
@ -438,6 +450,12 @@ func (d *defaultClient) Do(URL *url.URL, path, method string, authorizer influx.
|
|||
CheckRedirect: d.AuthedCheckRedirect,
|
||||
}
|
||||
|
||||
if d.InsecureSkipVerify {
|
||||
client.Transport = skipVerifyTransport
|
||||
} else {
|
||||
client.Transport = defaultTransport
|
||||
}
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -37,7 +37,7 @@ func (c *UserStore) Delete(ctx context.Context, u *chronograf.User) error {
|
|||
return c.Ctrl.DeleteUser(ctx, u.Name)
|
||||
}
|
||||
|
||||
// Number of users in Influx
|
||||
// Num of users in Influx
|
||||
func (c *UserStore) Num(ctx context.Context) (int, error) {
|
||||
all, err := c.All(ctx)
|
||||
if err != nil {
|
||||
|
|
|
@ -75,6 +75,7 @@ for f in CONFIGURATION_FILES:
|
|||
|
||||
targets = {
|
||||
'chronograf' : './cmd/chronograf',
|
||||
'chronoctl' : './cmd/chronoctl',
|
||||
}
|
||||
|
||||
supported_builds = {
|
||||
|
|
|
@ -26,6 +26,7 @@ import (
|
|||
func TestServer(t *testing.T) {
|
||||
type fields struct {
|
||||
Organizations []chronograf.Organization
|
||||
Mappings []chronograf.Mapping
|
||||
Users []chronograf.User
|
||||
Sources []chronograf.Source
|
||||
Servers []chronograf.Server
|
||||
|
@ -352,8 +353,7 @@ func TestServer(t *testing.T) {
|
|||
},
|
||||
"id": "default",
|
||||
"name": "Default",
|
||||
"defaultRole": "member",
|
||||
"public": true
|
||||
"defaultRole": "member"
|
||||
},
|
||||
{
|
||||
"links": {
|
||||
|
@ -361,8 +361,7 @@ func TestServer(t *testing.T) {
|
|||
},
|
||||
"id": "howdy",
|
||||
"name": "An Organization",
|
||||
"defaultRole": "viewer",
|
||||
"public": false
|
||||
"defaultRole": "viewer"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
|
@ -410,8 +409,7 @@ func TestServer(t *testing.T) {
|
|||
},
|
||||
"id": "howdy",
|
||||
"name": "An Organization",
|
||||
"defaultRole": "viewer",
|
||||
"public": false
|
||||
"defaultRole": "viewer"
|
||||
}`,
|
||||
},
|
||||
},
|
||||
|
@ -2045,24 +2043,21 @@ func TestServer(t *testing.T) {
|
|||
"self": "/chronograf/v1/organizations/1/users/1"
|
||||
},
|
||||
"organizations": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Sweet",
|
||||
"defaultRole": "viewer",
|
||||
"public": false
|
||||
},
|
||||
{
|
||||
"id": "default",
|
||||
"name": "Default",
|
||||
"defaultRole": "member",
|
||||
"public": true
|
||||
}
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Sweet",
|
||||
"defaultRole": "viewer"
|
||||
},
|
||||
{
|
||||
"id": "default",
|
||||
"name": "Default",
|
||||
"defaultRole": "member"
|
||||
}
|
||||
],
|
||||
"currentOrganization": {
|
||||
"id": "1",
|
||||
"name": "Sweet",
|
||||
"defaultRole": "viewer",
|
||||
"public": false
|
||||
"defaultRole": "viewer"
|
||||
}
|
||||
}`,
|
||||
},
|
||||
|
@ -2124,6 +2119,502 @@ func TestServer(t *testing.T) {
|
|||
}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GET /me",
|
||||
subName: "New user hits me for the first time",
|
||||
fields: fields{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
Mappings: []chronograf.Mapping{
|
||||
{
|
||||
ID: "1",
|
||||
Organization: "1",
|
||||
Provider: "*",
|
||||
Scheme: "*",
|
||||
ProviderOrganization: "influxdata",
|
||||
},
|
||||
{
|
||||
ID: "1",
|
||||
Organization: "1",
|
||||
Provider: "*",
|
||||
Scheme: "*",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Organization: "2",
|
||||
Provider: "github",
|
||||
Scheme: "*",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
{
|
||||
ID: "3",
|
||||
Organization: "3",
|
||||
Provider: "auth0",
|
||||
Scheme: "ldap",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
},
|
||||
Organizations: []chronograf.Organization{
|
||||
{
|
||||
ID: "1",
|
||||
Name: "Sweet",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Name: "What",
|
||||
DefaultRole: roles.EditorRoleName,
|
||||
},
|
||||
{
|
||||
ID: "3",
|
||||
Name: "Okay",
|
||||
DefaultRole: roles.AdminRoleName,
|
||||
},
|
||||
},
|
||||
Users: []chronograf.User{
|
||||
{
|
||||
ID: 1, // This is artificial, but should be reflective of the users actual ID
|
||||
Name: "billibob",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
Roles: []chronograf.Role{},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
server: &server.Server{
|
||||
GithubClientID: "not empty",
|
||||
GithubClientSecret: "not empty",
|
||||
},
|
||||
method: "GET",
|
||||
path: "/chronograf/v1/me",
|
||||
principal: oauth2.Principal{
|
||||
Subject: "billietta",
|
||||
Issuer: "github",
|
||||
Group: "influxdata,idk,mimi",
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: 200,
|
||||
body: `
|
||||
{
|
||||
"id": "2",
|
||||
"name": "billietta",
|
||||
"roles": [
|
||||
{
|
||||
"name": "viewer",
|
||||
"organization": "1"
|
||||
},
|
||||
{
|
||||
"name": "editor",
|
||||
"organization": "2"
|
||||
},
|
||||
{
|
||||
"name": "member",
|
||||
"organization": "default"
|
||||
}
|
||||
],
|
||||
"provider": "github",
|
||||
"scheme": "oauth2",
|
||||
"links": {
|
||||
"self": "/chronograf/v1/organizations/default/users/2"
|
||||
},
|
||||
"organizations": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Sweet",
|
||||
"defaultRole": "viewer"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "What",
|
||||
"defaultRole": "editor"
|
||||
},
|
||||
{
|
||||
"id": "default",
|
||||
"name": "Default",
|
||||
"defaultRole": "member"
|
||||
}
|
||||
],
|
||||
"currentOrganization": {
|
||||
"id": "default",
|
||||
"name": "Default",
|
||||
"defaultRole": "member"
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GET /mappings",
|
||||
subName: "get all mappings",
|
||||
fields: fields{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
Mappings: []chronograf.Mapping{
|
||||
{
|
||||
ID: "1",
|
||||
Organization: "1",
|
||||
Provider: "*",
|
||||
Scheme: "*",
|
||||
ProviderOrganization: "influxdata",
|
||||
},
|
||||
{
|
||||
ID: "1",
|
||||
Organization: "1",
|
||||
Provider: "*",
|
||||
Scheme: "*",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Organization: "2",
|
||||
Provider: "github",
|
||||
Scheme: "*",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
{
|
||||
ID: "3",
|
||||
Organization: "3",
|
||||
Provider: "auth0",
|
||||
Scheme: "ldap",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
},
|
||||
Organizations: []chronograf.Organization{
|
||||
{
|
||||
ID: "1",
|
||||
Name: "Sweet",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Name: "What",
|
||||
DefaultRole: roles.EditorRoleName,
|
||||
},
|
||||
{
|
||||
ID: "3",
|
||||
Name: "Okay",
|
||||
DefaultRole: roles.AdminRoleName,
|
||||
},
|
||||
},
|
||||
Users: []chronograf.User{
|
||||
{
|
||||
ID: 1, // This is artificial, but should be reflective of the users actual ID
|
||||
Name: "billibob",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
server: &server.Server{
|
||||
GithubClientID: "not empty",
|
||||
GithubClientSecret: "not empty",
|
||||
},
|
||||
method: "GET",
|
||||
path: "/chronograf/v1/mappings",
|
||||
principal: oauth2.Principal{
|
||||
Subject: "billibob",
|
||||
Issuer: "github",
|
||||
Group: "influxdata,idk,mimi",
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: 200,
|
||||
body: `
|
||||
{
|
||||
"links": {
|
||||
"self": "/chronograf/v1/mappings"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"links": {
|
||||
"self": "/chronograf/v1/mappings/1"
|
||||
},
|
||||
"id": "1",
|
||||
"organizationId": "1",
|
||||
"provider": "*",
|
||||
"scheme": "*",
|
||||
"providerOrganization": "influxdata"
|
||||
},
|
||||
{
|
||||
"links": {
|
||||
"self": "/chronograf/v1/mappings/2"
|
||||
},
|
||||
"id": "2",
|
||||
"organizationId": "1",
|
||||
"provider": "*",
|
||||
"scheme": "*",
|
||||
"providerOrganization": "*"
|
||||
},
|
||||
{
|
||||
"links": {
|
||||
"self": "/chronograf/v1/mappings/3"
|
||||
},
|
||||
"id": "3",
|
||||
"organizationId": "2",
|
||||
"provider": "github",
|
||||
"scheme": "*",
|
||||
"providerOrganization": "*"
|
||||
},
|
||||
{
|
||||
"links": {
|
||||
"self": "/chronograf/v1/mappings/4"
|
||||
},
|
||||
"id": "4",
|
||||
"organizationId": "3",
|
||||
"provider": "auth0",
|
||||
"scheme": "ldap",
|
||||
"providerOrganization": "*"
|
||||
},
|
||||
{
|
||||
"links": {
|
||||
"self": "/chronograf/v1/mappings/default"
|
||||
},
|
||||
"id": "default",
|
||||
"organizationId": "default",
|
||||
"provider": "*",
|
||||
"scheme": "*",
|
||||
"providerOrganization": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GET /mappings",
|
||||
subName: "get all mappings - user is not super admin",
|
||||
fields: fields{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
Mappings: []chronograf.Mapping{
|
||||
{
|
||||
ID: "1",
|
||||
Organization: "1",
|
||||
Provider: "*",
|
||||
Scheme: "*",
|
||||
ProviderOrganization: "influxdata",
|
||||
},
|
||||
{
|
||||
ID: "1",
|
||||
Organization: "1",
|
||||
Provider: "*",
|
||||
Scheme: "*",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Organization: "2",
|
||||
Provider: "github",
|
||||
Scheme: "*",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
{
|
||||
ID: "3",
|
||||
Organization: "3",
|
||||
Provider: "auth0",
|
||||
Scheme: "ldap",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
},
|
||||
Organizations: []chronograf.Organization{
|
||||
{
|
||||
ID: "1",
|
||||
Name: "Sweet",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Name: "What",
|
||||
DefaultRole: roles.EditorRoleName,
|
||||
},
|
||||
{
|
||||
ID: "3",
|
||||
Name: "Okay",
|
||||
DefaultRole: roles.AdminRoleName,
|
||||
},
|
||||
},
|
||||
Users: []chronograf.User{
|
||||
{
|
||||
ID: 1, // This is artificial, but should be reflective of the users actual ID
|
||||
Name: "billibob",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
server: &server.Server{
|
||||
GithubClientID: "not empty",
|
||||
GithubClientSecret: "not empty",
|
||||
},
|
||||
method: "GET",
|
||||
path: "/chronograf/v1/mappings",
|
||||
principal: oauth2.Principal{
|
||||
Subject: "billibob",
|
||||
Issuer: "github",
|
||||
Group: "influxdata,idk,mimi",
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: 403,
|
||||
body: `
|
||||
{
|
||||
"code": 403,
|
||||
"message": "User is not authorized"
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "POST /mappings",
|
||||
subName: "create new mapping",
|
||||
fields: fields{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
Mappings: []chronograf.Mapping{},
|
||||
Organizations: []chronograf.Organization{
|
||||
{
|
||||
ID: "1",
|
||||
Name: "Sweet",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
},
|
||||
},
|
||||
Users: []chronograf.User{
|
||||
{
|
||||
ID: 1, // This is artificial, but should be reflective of the users actual ID
|
||||
Name: "billibob",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
server: &server.Server{
|
||||
GithubClientID: "not empty",
|
||||
GithubClientSecret: "not empty",
|
||||
},
|
||||
method: "POST",
|
||||
path: "/chronograf/v1/mappings",
|
||||
payload: &chronograf.Mapping{
|
||||
ID: "1",
|
||||
Organization: "1",
|
||||
Provider: "*",
|
||||
Scheme: "*",
|
||||
ProviderOrganization: "influxdata",
|
||||
},
|
||||
principal: oauth2.Principal{
|
||||
Subject: "billibob",
|
||||
Issuer: "github",
|
||||
Group: "influxdata,idk,mimi",
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: 201,
|
||||
body: `
|
||||
{
|
||||
"links": {
|
||||
"self": "/chronograf/v1/mappings/1"
|
||||
},
|
||||
"id": "1",
|
||||
"organizationId": "1",
|
||||
"provider": "*",
|
||||
"scheme": "*",
|
||||
"providerOrganization": "influxdata"
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "PUT /mappings",
|
||||
subName: "update new mapping",
|
||||
fields: fields{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
Mappings: []chronograf.Mapping{
|
||||
chronograf.Mapping{
|
||||
ID: "1",
|
||||
Organization: "1",
|
||||
Provider: "*",
|
||||
Scheme: "*",
|
||||
ProviderOrganization: "influxdata",
|
||||
},
|
||||
},
|
||||
Organizations: []chronograf.Organization{
|
||||
{
|
||||
ID: "1",
|
||||
Name: "Sweet",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
},
|
||||
},
|
||||
Users: []chronograf.User{
|
||||
{
|
||||
ID: 1, // This is artificial, but should be reflective of the users actual ID
|
||||
Name: "billibob",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
server: &server.Server{
|
||||
GithubClientID: "not empty",
|
||||
GithubClientSecret: "not empty",
|
||||
},
|
||||
method: "PUT",
|
||||
path: "/chronograf/v1/mappings/1",
|
||||
payload: &chronograf.Mapping{
|
||||
ID: "1",
|
||||
Organization: "1",
|
||||
Provider: "*",
|
||||
Scheme: "*",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
principal: oauth2.Principal{
|
||||
Subject: "billibob",
|
||||
Issuer: "github",
|
||||
Group: "influxdata,idk,mimi",
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: 200,
|
||||
body: `
|
||||
{
|
||||
"links": {
|
||||
"self": "/chronograf/v1/mappings/1"
|
||||
},
|
||||
"id": "1",
|
||||
"organizationId": "1",
|
||||
"provider": "*",
|
||||
"scheme": "*",
|
||||
"providerOrganization": "*"
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GET /",
|
||||
subName: "signed into default org",
|
||||
|
@ -2325,6 +2816,16 @@ func TestServer(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Populate Organizations
|
||||
for i, mapping := range tt.fields.Mappings {
|
||||
o, err := boltdb.MappingsStore.Add(ctx, &mapping)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to add mapping: %v", err)
|
||||
return
|
||||
}
|
||||
tt.fields.Mappings[i] = *o
|
||||
}
|
||||
|
||||
// Populate Organizations
|
||||
for i, organization := range tt.fields.Organizations {
|
||||
o, err := boltdb.OrganizationsStore.Add(ctx, &organization)
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
type MappingsStore struct {
|
||||
AddF func(context.Context, *chronograf.Mapping) (*chronograf.Mapping, error)
|
||||
AllF func(context.Context) ([]chronograf.Mapping, error)
|
||||
DeleteF func(context.Context, *chronograf.Mapping) error
|
||||
UpdateF func(context.Context, *chronograf.Mapping) error
|
||||
GetF func(context.Context, string) (*chronograf.Mapping, error)
|
||||
}
|
||||
|
||||
func (s *MappingsStore) Add(ctx context.Context, m *chronograf.Mapping) (*chronograf.Mapping, error) {
|
||||
return s.AddF(ctx, m)
|
||||
}
|
||||
|
||||
func (s *MappingsStore) All(ctx context.Context) ([]chronograf.Mapping, error) {
|
||||
return s.AllF(ctx)
|
||||
}
|
||||
|
||||
func (s *MappingsStore) Delete(ctx context.Context, m *chronograf.Mapping) error {
|
||||
return s.DeleteF(ctx, m)
|
||||
}
|
||||
|
||||
func (s *MappingsStore) Get(ctx context.Context, id string) (*chronograf.Mapping, error) {
|
||||
return s.GetF(ctx, id)
|
||||
}
|
||||
|
||||
func (s *MappingsStore) Update(ctx context.Context, m *chronograf.Mapping) error {
|
||||
return s.UpdateF(ctx, m)
|
||||
}
|
|
@ -9,6 +9,7 @@ import (
|
|||
// Store is a server.DataStore
|
||||
type Store struct {
|
||||
SourcesStore chronograf.SourcesStore
|
||||
MappingsStore chronograf.MappingsStore
|
||||
ServersStore chronograf.ServersStore
|
||||
LayoutsStore chronograf.LayoutsStore
|
||||
UsersStore chronograf.UsersStore
|
||||
|
@ -36,6 +37,9 @@ func (s *Store) Users(ctx context.Context) chronograf.UsersStore {
|
|||
func (s *Store) Organizations(ctx context.Context) chronograf.OrganizationsStore {
|
||||
return s.OrganizationsStore
|
||||
}
|
||||
func (s *Store) Mappings(ctx context.Context) chronograf.MappingsStore {
|
||||
return s.MappingsStore
|
||||
}
|
||||
|
||||
func (s *Store) Dashboards(ctx context.Context) chronograf.DashboardsStore {
|
||||
return s.DashboardsStore
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
package noop
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
// ensure MappingsStore implements chronograf.MappingsStore
|
||||
var _ chronograf.MappingsStore = &MappingsStore{}
|
||||
|
||||
type MappingsStore struct{}
|
||||
|
||||
func (s *MappingsStore) All(context.Context) ([]chronograf.Mapping, error) {
|
||||
return nil, fmt.Errorf("no mappings found")
|
||||
}
|
||||
|
||||
func (s *MappingsStore) Add(context.Context, *chronograf.Mapping) (*chronograf.Mapping, error) {
|
||||
return nil, fmt.Errorf("failed to add mapping")
|
||||
}
|
||||
|
||||
func (s *MappingsStore) Delete(context.Context, *chronograf.Mapping) error {
|
||||
return fmt.Errorf("failed to delete mapping")
|
||||
}
|
||||
|
||||
func (s *MappingsStore) Get(ctx context.Context, ID string) (*chronograf.Mapping, error) {
|
||||
return nil, chronograf.ErrMappingNotFound
|
||||
}
|
||||
|
||||
func (s *MappingsStore) Update(context.Context, *chronograf.Mapping) error {
|
||||
return fmt.Errorf("failed to update mapping")
|
||||
}
|
|
@ -8,6 +8,8 @@ import (
|
|||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
var _ Provider = &Auth0{}
|
||||
|
||||
type Auth0 struct {
|
||||
Generic
|
||||
Organizations map[string]bool // the set of allowed organizations users may belong to
|
||||
|
@ -41,6 +43,26 @@ func (a *Auth0) PrincipalID(provider *http.Client) (string, error) {
|
|||
return act.Email, nil
|
||||
}
|
||||
|
||||
func (a *Auth0) Group(provider *http.Client) (string, error) {
|
||||
type Account struct {
|
||||
Email string `json:"email"`
|
||||
Organization string `json:"organization"`
|
||||
}
|
||||
|
||||
resp, err := provider.Get(a.Generic.APIURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
act := Account{}
|
||||
if err = json.NewDecoder(resp.Body).Decode(&act); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return act.Organization, nil
|
||||
}
|
||||
|
||||
func NewAuth0(auth0Domain, clientID, clientSecret, redirectURL string, organizations []string, logger chronograf.Logger) (Auth0, error) {
|
||||
domain, err := url.Parse(auth0Domain)
|
||||
if err != nil {
|
||||
|
|
|
@ -111,6 +111,31 @@ func (g *Generic) PrincipalID(provider *http.Client) (string, error) {
|
|||
return email, nil
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}{}
|
||||
|
||||
r, err := provider.Get(g.APIURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer r.Body.Close()
|
||||
if err = json.NewDecoder(r.Body).Decode(&res); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
email := strings.Split(res.Email, "@")
|
||||
if len(email) != 2 {
|
||||
return "", fmt.Errorf("malformed email address, expected %q to contain @ symbol", res.Email)
|
||||
}
|
||||
|
||||
return email[1], nil
|
||||
}
|
||||
|
||||
// UserEmail represents user's email address
|
||||
type UserEmail struct {
|
||||
Email *string `json:"email,omitempty"`
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-github/github"
|
||||
"github.com/influxdata/chronograf"
|
||||
|
@ -44,9 +45,9 @@ func (g *Github) Secret() string {
|
|||
// we are filtering by organizations.
|
||||
func (g *Github) Scopes() []string {
|
||||
scopes := []string{"user:email"}
|
||||
if len(g.Orgs) > 0 {
|
||||
scopes = append(scopes, "read:org")
|
||||
}
|
||||
// In order to access a users orgs, we need the "read:org" scope
|
||||
// even if g.Orgs == 0
|
||||
scopes = append(scopes, "read:org")
|
||||
return scopes
|
||||
}
|
||||
|
||||
|
@ -84,6 +85,26 @@ func (g *Github) PrincipalID(provider *http.Client) (string, error) {
|
|||
return email, nil
|
||||
}
|
||||
|
||||
// Group returns a comma delimited string of Github organizations
|
||||
// that a user belongs to in Github
|
||||
func (g *Github) Group(provider *http.Client) (string, error) {
|
||||
client := github.NewClient(provider)
|
||||
orgs, err := getOrganizations(client, g.Logger)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
groups := []string{}
|
||||
for _, org := range orgs {
|
||||
if org.Login != nil {
|
||||
groups = append(groups, *org.Login)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(groups, ","), nil
|
||||
}
|
||||
|
||||
func randomString(length int) string {
|
||||
k := make([]byte, length)
|
||||
if _, err := io.ReadFull(rand.Reader, k); err != nil {
|
||||
|
|
|
@ -88,3 +88,19 @@ func (g *Google) PrincipalID(provider *http.Client) (string, error) {
|
|||
g.Logger.Error("Domain '", info.Hd, "' is not a member of required Google domain(s): ", g.Domains)
|
||||
return "", fmt.Errorf("Not in required domain")
|
||||
}
|
||||
|
||||
// Group returns the string of domain a user belongs to in Google
|
||||
func (g *Google) Group(provider *http.Client) (string, error) {
|
||||
srv, err := goauth2.New(provider)
|
||||
if err != nil {
|
||||
g.Logger.Error("Unable to communicate with Google ", err.Error())
|
||||
return "", err
|
||||
}
|
||||
info, err := srv.Userinfo.Get().Do()
|
||||
if err != nil {
|
||||
g.Logger.Error("Unable to retrieve Google email ", err.Error())
|
||||
return "", err
|
||||
}
|
||||
|
||||
return info.Hd, nil
|
||||
}
|
||||
|
|
|
@ -88,6 +88,34 @@ func (h *Heroku) PrincipalID(provider *http.Client) (string, error) {
|
|||
return account.Email, nil
|
||||
}
|
||||
|
||||
// Group returns the Heroku organization that user belongs to.
|
||||
func (h *Heroku) Group(provider *http.Client) (string, error) {
|
||||
type DefaultOrg struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
type Account struct {
|
||||
Email string `json:"email"`
|
||||
DefaultOrganization DefaultOrg `json:"default_organization"`
|
||||
}
|
||||
|
||||
resp, err := provider.Get(HerokuAccountRoute)
|
||||
if err != nil {
|
||||
h.Logger.Error("Unable to communicate with Heroku. err:", err)
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
d := json.NewDecoder(resp.Body)
|
||||
|
||||
var account Account
|
||||
if err := d.Decode(&account); err != nil {
|
||||
h.Logger.Error("Unable to decode response from Heroku. err:", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return account.DefaultOrganization.Name, nil
|
||||
}
|
||||
|
||||
// Scopes for heroku is "identity" which grants access to user account
|
||||
// information. This will grant us access to the user's email address which is
|
||||
// used as the Principal's identifier.
|
||||
|
|
|
@ -31,9 +31,18 @@ var _ gojwt.Claims = &Claims{}
|
|||
// Claims extends jwt.StandardClaims' Valid to make sure claims has a subject.
|
||||
type Claims struct {
|
||||
gojwt.StandardClaims
|
||||
// We were unable to find a standard claim at https://www.iana.org/assignments/jwt/jwt.xhtmldd
|
||||
// We were unable to find a standard claim at https://www.iana.org/assignments/jwt/jwt.xhtml
|
||||
// that felt appropriate for Organization. As a result, we added a custom `org` field.
|
||||
Organization string `json:"org,omitempty"`
|
||||
// We were unable to find a standard claim at https://www.iana.org/assignments/jwt/jwt.xhtml
|
||||
// that felt appropriate for a users Group(s). As a result we added a custom `grp` field.
|
||||
// Multiple groups may be specified by comma delimiting the various group.
|
||||
//
|
||||
// The singlular `grp` was chosen over the `grps` to keep consistent with the JWT naming
|
||||
// convention (it is common for singlularly named values to actually be arrays, see `given_name`,
|
||||
// `family_name`, and `middle_name` in the iana link provided above). I should add the discalimer
|
||||
// I'm currently sick, so this thought process might be off.
|
||||
Group string `json:"grp,omitempty"`
|
||||
}
|
||||
|
||||
// Valid adds an empty subject test to the StandardClaims checks.
|
||||
|
@ -99,6 +108,7 @@ func (j *JWT) ValidClaims(jwtToken Token, lifespan time.Duration, alg gojwt.Keyf
|
|||
Subject: claims.Subject,
|
||||
Issuer: claims.Issuer,
|
||||
Organization: claims.Organization,
|
||||
Group: claims.Group,
|
||||
ExpiresAt: exp,
|
||||
IssuedAt: iat,
|
||||
}, nil
|
||||
|
@ -117,6 +127,7 @@ func (j *JWT) Create(ctx context.Context, user Principal) (Token, error) {
|
|||
NotBefore: user.IssuedAt.Unix(),
|
||||
},
|
||||
Organization: user.Organization,
|
||||
Group: user.Group,
|
||||
}
|
||||
token := gojwt.NewWithClaims(gojwt.SigningMethodHS256, claims)
|
||||
// Sign and get the complete encoded token as a string using the secret
|
||||
|
|
|
@ -23,8 +23,8 @@ func NewAuthMux(p Provider, a Authenticator, t Tokenizer, basepath string, l chr
|
|||
Tokens: t,
|
||||
SuccessURL: path.Join(basepath, "/"),
|
||||
FailureURL: path.Join(basepath, "/login"),
|
||||
Now: DefaultNowTime,
|
||||
Logger: l,
|
||||
Now: DefaultNowTime,
|
||||
Logger: l,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -125,9 +125,17 @@ func (j *AuthMux) Callback() http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
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(),
|
||||
Group: group,
|
||||
}
|
||||
ctx := r.Context()
|
||||
err = j.Auth.Authorize(ctx, w, p)
|
||||
|
|
|
@ -27,7 +27,11 @@ func setupMuxTest(selector func(*AuthMux) http.Handler) (*http.Client, *httptest
|
|||
now := func() time.Time {
|
||||
return testTime
|
||||
}
|
||||
mp := &MockProvider{"biff@example.com", provider.URL}
|
||||
mp := &MockProvider{
|
||||
Email: "biff@example.com",
|
||||
ProviderURL: provider.URL,
|
||||
Orgs: "",
|
||||
}
|
||||
mt := &YesManTokenizer{}
|
||||
auth := &cookie{
|
||||
Name: DefaultCookieName,
|
||||
|
|
|
@ -33,6 +33,7 @@ type Principal struct {
|
|||
Subject string
|
||||
Issuer string
|
||||
Organization string
|
||||
Group string
|
||||
ExpiresAt time.Time
|
||||
IssuedAt time.Time
|
||||
}
|
||||
|
@ -53,6 +54,10 @@ type Provider interface {
|
|||
PrincipalID(provider *http.Client) (string, error)
|
||||
// Name is the name of the Provider
|
||||
Name() string
|
||||
// Group is a comma delimited list of groups and organizations for a provider
|
||||
// TODO: This will break if there are any group names that contain commas.
|
||||
// I think this is okay, but I'm not 100% certain.
|
||||
Group(provider *http.Client) (string, error)
|
||||
}
|
||||
|
||||
// Mux is a collection of handlers responsible for servicing an Oauth2 interaction between a browser and a provider
|
||||
|
|
|
@ -16,6 +16,7 @@ var _ Provider = &MockProvider{}
|
|||
|
||||
type MockProvider struct {
|
||||
Email string
|
||||
Orgs string
|
||||
|
||||
ProviderURL string
|
||||
}
|
||||
|
@ -44,6 +45,10 @@ func (mp *MockProvider) PrincipalID(provider *http.Client) (string, error) {
|
|||
return mp.Email, nil
|
||||
}
|
||||
|
||||
func (mp *MockProvider) Group(provider *http.Client) (string, error) {
|
||||
return mp.Orgs, nil
|
||||
}
|
||||
|
||||
func (mp *MockProvider) Scopes() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,250 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/bouk/httprouter"
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/oauth2"
|
||||
)
|
||||
|
||||
func (s *Service) mapPrincipalToRoles(ctx context.Context, p oauth2.Principal) ([]chronograf.Role, error) {
|
||||
mappings, err := s.Store.Mappings(ctx).All(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
roles := []chronograf.Role{}
|
||||
MappingsLoop:
|
||||
for _, mapping := range mappings {
|
||||
if applyMapping(mapping, p) {
|
||||
org, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &mapping.Organization})
|
||||
if err != nil {
|
||||
continue MappingsLoop
|
||||
}
|
||||
|
||||
for _, role := range roles {
|
||||
if role.Organization == org.ID {
|
||||
continue MappingsLoop
|
||||
}
|
||||
}
|
||||
roles = append(roles, chronograf.Role{Organization: org.ID, Name: org.DefaultRole})
|
||||
}
|
||||
}
|
||||
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
func applyMapping(m chronograf.Mapping, p oauth2.Principal) bool {
|
||||
switch m.Provider {
|
||||
case chronograf.MappingWildcard, p.Issuer:
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
switch m.Scheme {
|
||||
case chronograf.MappingWildcard, "oauth2":
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
if m.ProviderOrganization == chronograf.MappingWildcard {
|
||||
return true
|
||||
}
|
||||
|
||||
groups := strings.Split(p.Group, ",")
|
||||
|
||||
return matchGroup(m.ProviderOrganization, groups)
|
||||
}
|
||||
|
||||
func matchGroup(match string, groups []string) bool {
|
||||
for _, group := range groups {
|
||||
if match == group {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type mappingsRequest chronograf.Mapping
|
||||
|
||||
// Valid determines if a mapping request is valid
|
||||
func (m *mappingsRequest) Valid() error {
|
||||
if m.Provider == "" {
|
||||
return fmt.Errorf("mapping must specify provider")
|
||||
}
|
||||
if m.Scheme == "" {
|
||||
return fmt.Errorf("mapping must specify scheme")
|
||||
}
|
||||
if m.ProviderOrganization == "" {
|
||||
return fmt.Errorf("mapping must specify group")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type mappingResponse struct {
|
||||
Links selfLinks `json:"links"`
|
||||
chronograf.Mapping
|
||||
}
|
||||
|
||||
func newMappingResponse(m chronograf.Mapping) *mappingResponse {
|
||||
|
||||
return &mappingResponse{
|
||||
Links: selfLinks{
|
||||
Self: fmt.Sprintf("/chronograf/v1/mappings/%s", m.ID),
|
||||
},
|
||||
Mapping: m,
|
||||
}
|
||||
}
|
||||
|
||||
type mappingsResponse struct {
|
||||
Links selfLinks `json:"links"`
|
||||
Mappings []*mappingResponse `json:"mappings"`
|
||||
}
|
||||
|
||||
func newMappingsResponse(ms []chronograf.Mapping) *mappingsResponse {
|
||||
mappings := []*mappingResponse{}
|
||||
for _, m := range ms {
|
||||
mappings = append(mappings, newMappingResponse(m))
|
||||
}
|
||||
return &mappingsResponse{
|
||||
Links: selfLinks{
|
||||
Self: "/chronograf/v1/mappings",
|
||||
},
|
||||
Mappings: mappings,
|
||||
}
|
||||
}
|
||||
|
||||
// Mappings retrives all mappings
|
||||
func (s *Service) Mappings(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
mappings, err := s.Store.Mappings(ctx).All(ctx)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "failed to retrieve mappings from database", s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("mappings: %#v\n", mappings)
|
||||
|
||||
res := newMappingsResponse(mappings)
|
||||
|
||||
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||
}
|
||||
|
||||
// NewMapping adds a new mapping
|
||||
func (s *Service) NewMapping(w http.ResponseWriter, r *http.Request) {
|
||||
var req mappingsRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
invalidJSON(w, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if err := req.Valid(); err != nil {
|
||||
invalidData(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// validate that the organization exists
|
||||
if !s.organizationExists(ctx, req.Organization) {
|
||||
invalidData(w, fmt.Errorf("organization does not exist"), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
mapping := &chronograf.Mapping{
|
||||
Organization: req.Organization,
|
||||
Scheme: req.Scheme,
|
||||
Provider: req.Provider,
|
||||
ProviderOrganization: req.ProviderOrganization,
|
||||
}
|
||||
|
||||
m, err := s.Store.Mappings(ctx).Add(ctx, mapping)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "failed to add mapping to database", s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
cu := newMappingResponse(*m)
|
||||
location(w, cu.Links.Self)
|
||||
encodeJSON(w, http.StatusCreated, cu, s.Logger)
|
||||
}
|
||||
|
||||
// UpdateMapping updates a mapping
|
||||
func (s *Service) UpdateMapping(w http.ResponseWriter, r *http.Request) {
|
||||
var req mappingsRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
invalidJSON(w, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if err := req.Valid(); err != nil {
|
||||
invalidData(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// validate that the organization exists
|
||||
if !s.organizationExists(ctx, req.Organization) {
|
||||
invalidData(w, fmt.Errorf("organization does not exist"), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
mapping := &chronograf.Mapping{
|
||||
ID: req.ID,
|
||||
Organization: req.Organization,
|
||||
Scheme: req.Scheme,
|
||||
Provider: req.Provider,
|
||||
ProviderOrganization: req.ProviderOrganization,
|
||||
}
|
||||
|
||||
err := s.Store.Mappings(ctx).Update(ctx, mapping)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "failed to update mapping in database", s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
cu := newMappingResponse(*mapping)
|
||||
location(w, cu.Links.Self)
|
||||
encodeJSON(w, http.StatusOK, cu, s.Logger)
|
||||
}
|
||||
|
||||
// RemoveMapping removes a mapping
|
||||
func (s *Service) RemoveMapping(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
id := httprouter.GetParamFromContext(ctx, "id")
|
||||
|
||||
m, err := s.Store.Mappings(ctx).Get(ctx, id)
|
||||
if err == chronograf.ErrMappingNotFound {
|
||||
Error(w, http.StatusNotFound, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "failed to retrieve mapping from database", s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.Store.Mappings(ctx).Delete(ctx, m); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "failed to remove mapping from database", s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s *Service) organizationExists(ctx context.Context, orgID string) bool {
|
||||
if _, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &orgID}); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
|
@ -0,0 +1,360 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/bouk/httprouter"
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/log"
|
||||
"github.com/influxdata/chronograf/mocks"
|
||||
"github.com/influxdata/chronograf/roles"
|
||||
)
|
||||
|
||||
func TestMappings_All(t *testing.T) {
|
||||
type fields struct {
|
||||
MappingsStore chronograf.MappingsStore
|
||||
}
|
||||
type args struct {
|
||||
}
|
||||
type wants struct {
|
||||
statusCode int
|
||||
contentType string
|
||||
body string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "get all mappings",
|
||||
fields: fields{
|
||||
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
|
||||
},
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: 200,
|
||||
contentType: "application/json",
|
||||
body: `{"links":{"self":"/chronograf/v1/mappings"},"mappings":[{"links":{"self":"/chronograf/v1/mappings/"},"id":"","organizationId":"0","provider":"*","scheme":"*","providerOrganization":"*"}]}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &Service{
|
||||
Store: &mocks.Store{
|
||||
MappingsStore: tt.fields.MappingsStore,
|
||||
},
|
||||
Logger: log.New(log.DebugLevel),
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "http://any.url", nil)
|
||||
s.Mappings(w, r)
|
||||
|
||||
resp := w.Result()
|
||||
content := resp.Header.Get("Content-Type")
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode != tt.wants.statusCode {
|
||||
t.Errorf("%q. Mappings() = %v, want %v", tt.name, resp.StatusCode, tt.wants.statusCode)
|
||||
}
|
||||
if tt.wants.contentType != "" && content != tt.wants.contentType {
|
||||
t.Errorf("%q. Mappings() = %v, want %v", tt.name, content, tt.wants.contentType)
|
||||
}
|
||||
if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
|
||||
t.Errorf("%q. Mappings() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMappings_Add(t *testing.T) {
|
||||
type fields struct {
|
||||
MappingsStore chronograf.MappingsStore
|
||||
OrganizationsStore chronograf.OrganizationsStore
|
||||
}
|
||||
type args struct {
|
||||
mapping *chronograf.Mapping
|
||||
}
|
||||
type wants struct {
|
||||
statusCode int
|
||||
contentType string
|
||||
body string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "create new mapping",
|
||||
fields: fields{
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: "0",
|
||||
Name: "The Gnarly Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
MappingsStore: &mocks.MappingsStore{
|
||||
AddF: func(ctx context.Context, m *chronograf.Mapping) (*chronograf.Mapping, error) {
|
||||
m.ID = "0"
|
||||
return m, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
mapping: &chronograf.Mapping{
|
||||
Organization: "0",
|
||||
Provider: "*",
|
||||
Scheme: "*",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: 201,
|
||||
contentType: "application/json",
|
||||
body: `{"links":{"self":"/chronograf/v1/mappings/0"},"id":"0","organizationId":"0","provider":"*","scheme":"*","providerOrganization":"*"}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &Service{
|
||||
Store: &mocks.Store{
|
||||
MappingsStore: tt.fields.MappingsStore,
|
||||
OrganizationsStore: tt.fields.OrganizationsStore,
|
||||
},
|
||||
Logger: log.New(log.DebugLevel),
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "http://any.url", nil)
|
||||
|
||||
buf, _ := json.Marshal(tt.args.mapping)
|
||||
r.Body = ioutil.NopCloser(bytes.NewReader(buf))
|
||||
|
||||
s.NewMapping(w, r)
|
||||
|
||||
resp := w.Result()
|
||||
content := resp.Header.Get("Content-Type")
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode != tt.wants.statusCode {
|
||||
t.Errorf("%q. Add() = %v, want %v", tt.name, resp.StatusCode, tt.wants.statusCode)
|
||||
}
|
||||
if tt.wants.contentType != "" && content != tt.wants.contentType {
|
||||
t.Errorf("%q. Add() = %v, want %v", tt.name, content, tt.wants.contentType)
|
||||
}
|
||||
if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
|
||||
t.Errorf("%q. Add() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMappings_Update(t *testing.T) {
|
||||
type fields struct {
|
||||
MappingsStore chronograf.MappingsStore
|
||||
OrganizationsStore chronograf.OrganizationsStore
|
||||
}
|
||||
type args struct {
|
||||
mapping *chronograf.Mapping
|
||||
}
|
||||
type wants struct {
|
||||
statusCode int
|
||||
contentType string
|
||||
body string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "update new mapping",
|
||||
fields: fields{
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: "0",
|
||||
Name: "The Gnarly Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
MappingsStore: &mocks.MappingsStore{
|
||||
UpdateF: func(ctx context.Context, m *chronograf.Mapping) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
mapping: &chronograf.Mapping{
|
||||
ID: "1",
|
||||
Organization: "0",
|
||||
Provider: "*",
|
||||
Scheme: "*",
|
||||
ProviderOrganization: "*",
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: 200,
|
||||
contentType: "application/json",
|
||||
body: `{"links":{"self":"/chronograf/v1/mappings/1"},"id":"1","organizationId":"0","provider":"*","scheme":"*","providerOrganization":"*"}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &Service{
|
||||
Store: &mocks.Store{
|
||||
MappingsStore: tt.fields.MappingsStore,
|
||||
OrganizationsStore: tt.fields.OrganizationsStore,
|
||||
},
|
||||
Logger: log.New(log.DebugLevel),
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "http://any.url", nil)
|
||||
|
||||
buf, _ := json.Marshal(tt.args.mapping)
|
||||
r.Body = ioutil.NopCloser(bytes.NewReader(buf))
|
||||
r = r.WithContext(httprouter.WithParams(
|
||||
context.Background(),
|
||||
httprouter.Params{
|
||||
{
|
||||
Key: "id",
|
||||
Value: tt.args.mapping.ID,
|
||||
},
|
||||
}))
|
||||
|
||||
s.UpdateMapping(w, r)
|
||||
|
||||
resp := w.Result()
|
||||
content := resp.Header.Get("Content-Type")
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode != tt.wants.statusCode {
|
||||
t.Errorf("%q. Add() = %v, want %v", tt.name, resp.StatusCode, tt.wants.statusCode)
|
||||
}
|
||||
if tt.wants.contentType != "" && content != tt.wants.contentType {
|
||||
t.Errorf("%q. Add() = %v, want %v", tt.name, content, tt.wants.contentType)
|
||||
}
|
||||
if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
|
||||
t.Errorf("%q. Add() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMappings_Remove(t *testing.T) {
|
||||
type fields struct {
|
||||
MappingsStore chronograf.MappingsStore
|
||||
}
|
||||
type args struct {
|
||||
id string
|
||||
}
|
||||
type wants struct {
|
||||
statusCode int
|
||||
contentType string
|
||||
body string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "remove mapping",
|
||||
fields: fields{
|
||||
MappingsStore: &mocks.MappingsStore{
|
||||
GetF: func(ctx context.Context, id string) (*chronograf.Mapping, error) {
|
||||
return &chronograf.Mapping{
|
||||
ID: "1",
|
||||
Organization: "0",
|
||||
Provider: "*",
|
||||
Scheme: "*",
|
||||
ProviderOrganization: "*",
|
||||
}, nil
|
||||
},
|
||||
DeleteF: func(ctx context.Context, m *chronograf.Mapping) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{},
|
||||
wants: wants{
|
||||
statusCode: 204,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &Service{
|
||||
Store: &mocks.Store{
|
||||
MappingsStore: tt.fields.MappingsStore,
|
||||
},
|
||||
Logger: log.New(log.DebugLevel),
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "http://any.url", nil)
|
||||
|
||||
r = r.WithContext(httprouter.WithParams(
|
||||
context.Background(),
|
||||
httprouter.Params{
|
||||
{
|
||||
Key: "id",
|
||||
Value: tt.args.id,
|
||||
},
|
||||
}))
|
||||
|
||||
s.RemoveMapping(w, r)
|
||||
|
||||
resp := w.Result()
|
||||
content := resp.Header.Get("Content-Type")
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode != tt.wants.statusCode {
|
||||
t.Errorf("%q. Remove() = %v, want %v", tt.name, resp.StatusCode, tt.wants.statusCode)
|
||||
}
|
||||
if tt.wants.contentType != "" && content != tt.wants.contentType {
|
||||
t.Errorf("%q. Remove() = %v, want %v", tt.name, content, tt.wants.contentType)
|
||||
}
|
||||
if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
|
||||
t.Errorf("%q. Remove() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
84
server/me.go
84
server/me.go
|
@ -20,10 +20,22 @@ type meLinks struct {
|
|||
type meResponse struct {
|
||||
*chronograf.User
|
||||
Links meLinks `json:"links"`
|
||||
Organizations []chronograf.Organization `json:"organizations,omitempty"`
|
||||
Organizations []chronograf.Organization `json:"organizations"`
|
||||
CurrentOrganization *chronograf.Organization `json:"currentOrganization,omitempty"`
|
||||
}
|
||||
|
||||
type noAuthMeResponse struct {
|
||||
Links meLinks `json:"links"`
|
||||
}
|
||||
|
||||
func newNoAuthMeResponse() noAuthMeResponse {
|
||||
return noAuthMeResponse{
|
||||
Links: meLinks{
|
||||
Self: "/chronograf/v1/me",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// If new user response is nil, return an empty meResponse because it
|
||||
// indicates authentication is not needed
|
||||
func newMeResponse(usr *chronograf.User, org string) meResponse {
|
||||
|
@ -182,7 +194,7 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
|
|||
ctx := r.Context()
|
||||
if !s.UseAuth {
|
||||
// If there's no authentication, return an empty user
|
||||
res := newMeResponse(nil, "")
|
||||
res := newNoAuthMeResponse()
|
||||
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||
return
|
||||
}
|
||||
|
@ -201,12 +213,13 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
|
|||
ctx = context.WithValue(ctx, organizations.ContextKey, p.Organization)
|
||||
serverCtx := serverContext(ctx)
|
||||
|
||||
defaultOrg, err := s.Store.Organizations(serverCtx).DefaultOrganization(serverCtx)
|
||||
if err != nil {
|
||||
unknownErrorWithMessage(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if p.Organization == "" {
|
||||
defaultOrg, err := s.Store.Organizations(serverCtx).DefaultOrganization(serverCtx)
|
||||
if err != nil {
|
||||
unknownErrorWithMessage(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
p.Organization = defaultOrg.ID
|
||||
}
|
||||
|
||||
|
@ -220,35 +233,8 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
defaultOrg, err := s.Store.Organizations(serverCtx).DefaultOrganization(serverCtx)
|
||||
if err != nil {
|
||||
unknownErrorWithMessage(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
// user exists
|
||||
if usr != nil {
|
||||
|
||||
if defaultOrg.Public || usr.SuperAdmin == true {
|
||||
// If the default organization is public, or the user is a super admin
|
||||
// they will always have a role in the default organization
|
||||
defaultOrgID := defaultOrg.ID
|
||||
if !hasRoleInDefaultOrganization(usr, defaultOrgID) {
|
||||
usr.Roles = append(usr.Roles, chronograf.Role{
|
||||
Organization: defaultOrgID,
|
||||
Name: defaultOrg.DefaultRole,
|
||||
})
|
||||
if err := s.Store.Users(serverCtx).Update(serverCtx, usr); err != nil {
|
||||
unknownErrorWithMessage(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the default org is private and the user has no roles, they should not have access
|
||||
if !defaultOrg.Public && len(usr.Roles) == 0 {
|
||||
Error(w, http.StatusForbidden, "This organization is private. To gain access, you must be explicitly added by an administrator.", 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
|
||||
|
@ -265,6 +251,7 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
|
|||
unknownErrorWithMessage(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
res := newMeResponse(usr, currentOrg.ID)
|
||||
res.Organizations = orgs
|
||||
res.CurrentOrganization = currentOrg
|
||||
|
@ -272,13 +259,6 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// If users must be explicitly added to the default organization, respond with 403
|
||||
// forbidden
|
||||
if !defaultOrg.Public {
|
||||
Error(w, http.StatusForbidden, "This organization is private. To gain access, you must be explicitly added by an administrator.", s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
// Because we didnt find a user, making a new one
|
||||
user := &chronograf.User{
|
||||
Name: p.Subject,
|
||||
|
@ -287,17 +267,23 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
|
|||
// support OAuth2. This hard-coding should be removed whenever we add
|
||||
// support for other authentication schemes.
|
||||
Scheme: scheme,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: defaultOrg.DefaultRole,
|
||||
// This is the ID of the default organization
|
||||
Organization: defaultOrg.ID,
|
||||
},
|
||||
},
|
||||
// TODO(desa): this needs a better name
|
||||
SuperAdmin: s.newUsersAreSuperAdmin(),
|
||||
}
|
||||
|
||||
roles, err := s.mapPrincipalToRoles(serverCtx, p)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if 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
|
||||
}
|
||||
|
||||
user.Roles = roles
|
||||
|
||||
newUser, err := s.Store.Users(serverCtx).Add(serverCtx, user)
|
||||
if err != nil {
|
||||
msg := fmt.Errorf("error storing user %s: %v", user.Name, err)
|
||||
|
|
|
@ -23,6 +23,7 @@ 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
|
||||
|
@ -56,13 +57,24 @@ func TestService_Me(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
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{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: false,
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -72,13 +84,11 @@ func TestService_Me(t *testing.T) {
|
|||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: false,
|
||||
}, nil
|
||||
case "1":
|
||||
return &chronograf.Organization{
|
||||
ID: "1",
|
||||
Name: "The Bad Place",
|
||||
Public: false,
|
||||
ID: "1",
|
||||
Name: "The Bad Place",
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
|
@ -108,12 +118,12 @@ func TestService_Me(t *testing.T) {
|
|||
Subject: "me",
|
||||
Issuer: "github",
|
||||
},
|
||||
wantStatus: http.StatusForbidden,
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"code":403,"message":"This organization is private. To gain access, you must be explicitly added by an administrator."}`,
|
||||
wantBody: `{"name":"me","roles":null,"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[],"currentOrganization":{"id":"0","name":"Default","defaultRole":"viewer"}}`,
|
||||
},
|
||||
{
|
||||
name: "Existing user - private default org and user is a super admin",
|
||||
name: "Existing superadmin - not member of any organization",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
|
@ -121,13 +131,17 @@ func TestService_Me(t *testing.T) {
|
|||
fields: fields{
|
||||
UseAuth: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
MappingsStore: &mocks.MappingsStore{
|
||||
AllF: func(ctx context.Context) ([]chronograf.Mapping, error) {
|
||||
return []chronograf.Mapping{}, nil
|
||||
},
|
||||
},
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: false,
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -137,13 +151,11 @@ func TestService_Me(t *testing.T) {
|
|||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
}, nil
|
||||
case "1":
|
||||
return &chronograf.Organization{
|
||||
ID: "1",
|
||||
Name: "The Bad Place",
|
||||
Public: true,
|
||||
ID: "1",
|
||||
Name: "The Bad Place",
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
|
@ -176,137 +188,7 @@ func TestService_Me(t *testing.T) {
|
|||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"name":"me","roles":[{"name":"viewer","organization":"0"}],"provider":"github","scheme":"oauth2","superAdmin":true,"links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"viewer","public":true}],"currentOrganization":{"id":"0","name":"Default","defaultRole":"viewer","public":true}}`,
|
||||
},
|
||||
{
|
||||
name: "Existing user - private default org",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
},
|
||||
fields: fields{
|
||||
UseAuth: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: false,
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
switch *q.ID {
|
||||
case "0":
|
||||
return &chronograf.Organization{
|
||||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
}, nil
|
||||
case "1":
|
||||
return &chronograf.Organization{
|
||||
ID: "1",
|
||||
Name: "The Bad Place",
|
||||
Public: true,
|
||||
}, nil
|
||||
}
|
||||
return nil, 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: "me",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
}, nil
|
||||
},
|
||||
UpdateF: func(ctx context.Context, u *chronograf.User) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
principal: oauth2.Principal{
|
||||
Subject: "me",
|
||||
Issuer: "github",
|
||||
},
|
||||
wantStatus: http.StatusForbidden,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"code":403,"message":"This organization is private. To gain access, you must be explicitly added by an administrator."}`,
|
||||
},
|
||||
{
|
||||
name: "Existing user - default org public",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
},
|
||||
fields: fields{
|
||||
UseAuth: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
switch *q.ID {
|
||||
case "0":
|
||||
return &chronograf.Organization{
|
||||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
}, nil
|
||||
case "1":
|
||||
return &chronograf.Organization{
|
||||
ID: "1",
|
||||
Name: "The Bad Place",
|
||||
Public: true,
|
||||
}, nil
|
||||
}
|
||||
return nil, 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: "me",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
}, nil
|
||||
},
|
||||
UpdateF: func(ctx context.Context, u *chronograf.User) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
principal: oauth2.Principal{
|
||||
Subject: "me",
|
||||
Issuer: "github",
|
||||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"name":"me","roles":[{"name":"viewer","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"viewer","public":true}],"currentOrganization":{"id":"0","name":"Default","defaultRole":"viewer","public":true}}`,
|
||||
wantBody: `{"name":"me","roles":null,"provider":"github","scheme":"oauth2","superAdmin":true,"links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[],"currentOrganization":{"id":"0","name":"Default","defaultRole":"viewer"}}`,
|
||||
},
|
||||
{
|
||||
name: "Existing user - organization doesn't exist",
|
||||
|
@ -317,13 +199,17 @@ func TestService_Me(t *testing.T) {
|
|||
fields: fields{
|
||||
UseAuth: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
MappingsStore: &mocks.MappingsStore{
|
||||
AllF: func(ctx context.Context) ([]chronograf.Mapping, error) {
|
||||
return []chronograf.Mapping{}, nil
|
||||
},
|
||||
},
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -333,7 +219,6 @@ func TestService_Me(t *testing.T) {
|
|||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
}, nil
|
||||
}
|
||||
return nil, chronograf.ErrOrganizationNotFound
|
||||
|
@ -365,7 +250,7 @@ func TestService_Me(t *testing.T) {
|
|||
wantBody: `{"code":403,"message":"user's current organization was not found"}`,
|
||||
},
|
||||
{
|
||||
name: "new user - default org is public",
|
||||
name: "default mapping applies to new user",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
|
@ -380,13 +265,24 @@ func TestService_Me(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
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{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: "0",
|
||||
Name: "The Gnarly Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -394,7 +290,15 @@ func TestService_Me(t *testing.T) {
|
|||
ID: "0",
|
||||
Name: "The Gnarly Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
}, nil
|
||||
},
|
||||
AllF: func(ctx context.Context) ([]chronograf.Organization, error) {
|
||||
return []chronograf.Organization{
|
||||
chronograf.Organization{
|
||||
ID: "0",
|
||||
Name: "The Gnarly Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
|
@ -423,8 +327,7 @@ func TestService_Me(t *testing.T) {
|
|||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"name":"secret","superAdmin":true,"roles":[{"name":"viewer","organization":"0"}],"provider":"auth0","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"The Gnarly Default","defaultRole":"viewer","public":true}],"currentOrganization":{"id":"0","name":"The Gnarly Default","defaultRole":"viewer","public":true}}
|
||||
`,
|
||||
wantBody: `{"name":"secret","superAdmin":true,"roles":[{"name":"viewer","organization":"0"}],"provider":"auth0","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"The Gnarly Default","defaultRole":"viewer"}],"currentOrganization":{"id":"0","name":"The Gnarly Default","defaultRole":"viewer"}}`,
|
||||
},
|
||||
{
|
||||
name: "New user - New users not super admin, not first user",
|
||||
|
@ -442,13 +345,24 @@ func TestService_Me(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
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{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: "0",
|
||||
Name: "The Gnarly Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -456,7 +370,15 @@ func TestService_Me(t *testing.T) {
|
|||
ID: "0",
|
||||
Name: "The Gnarly Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
}, nil
|
||||
},
|
||||
AllF: func(ctx context.Context) ([]chronograf.Organization, error) {
|
||||
return []chronograf.Organization{
|
||||
chronograf.Organization{
|
||||
ID: "0",
|
||||
Name: "The Gnarly Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
|
@ -485,8 +407,7 @@ func TestService_Me(t *testing.T) {
|
|||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"name":"secret","roles":[{"name":"viewer","organization":"0"}],"provider":"auth0","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"The Gnarly Default","public":true,"defaultRole":"viewer"}],"currentOrganization":{"id":"0","name":"The Gnarly Default","public":true,"defaultRole":"viewer"}}
|
||||
`,
|
||||
wantBody: `{"name":"secret","roles":[{"name":"viewer","organization":"0"}],"provider":"auth0","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"The Gnarly Default","defaultRole":"viewer"}],"currentOrganization":{"id":"0","name":"The Gnarly Default","defaultRole":"viewer"}}`,
|
||||
},
|
||||
{
|
||||
name: "New user - New users not super admin, first user",
|
||||
|
@ -504,13 +425,24 @@ func TestService_Me(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
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{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: "0",
|
||||
Name: "The Gnarly Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -518,7 +450,15 @@ func TestService_Me(t *testing.T) {
|
|||
ID: "0",
|
||||
Name: "The Gnarly Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
}, nil
|
||||
},
|
||||
AllF: func(ctx context.Context) ([]chronograf.Organization, error) {
|
||||
return []chronograf.Organization{
|
||||
chronograf.Organization{
|
||||
ID: "0",
|
||||
Name: "The Gnarly Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
|
@ -547,8 +487,7 @@ func TestService_Me(t *testing.T) {
|
|||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"name":"secret","superAdmin":true,"roles":[{"name":"viewer","organization":"0"}],"provider":"auth0","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"The Gnarly Default","public":true,"defaultRole":"viewer"}],"currentOrganization":{"id":"0","name":"The Gnarly Default","public":true,"defaultRole":"viewer"}}
|
||||
`,
|
||||
wantBody: `{"name":"secret","superAdmin":true,"roles":[{"name":"viewer","organization":"0"}],"provider":"auth0","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"The Gnarly Default","defaultRole":"viewer"}],"currentOrganization":{"id":"0","name":"The Gnarly Default","defaultRole":"viewer"}}`,
|
||||
},
|
||||
{
|
||||
name: "Error adding user",
|
||||
|
@ -565,18 +504,31 @@ func TestService_Me(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
MappingsStore: &mocks.MappingsStore{
|
||||
AllF: func(ctx context.Context) ([]chronograf.Mapping, error) {
|
||||
return []chronograf.Mapping{}, nil
|
||||
},
|
||||
},
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: "0",
|
||||
Public: true,
|
||||
ID: "0",
|
||||
Name: "The Bad Place",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: "0",
|
||||
Name: "The Bad Place",
|
||||
Public: true,
|
||||
ID: "0",
|
||||
Name: "The Bad Place",
|
||||
}, nil
|
||||
},
|
||||
AllF: func(ctx context.Context) ([]chronograf.Organization, error) {
|
||||
return []chronograf.Organization{
|
||||
chronograf.Organization{
|
||||
ID: "0",
|
||||
Name: "The Bad Place",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
|
@ -601,9 +553,9 @@ func TestService_Me(t *testing.T) {
|
|||
Subject: "secret",
|
||||
Issuer: "heroku",
|
||||
},
|
||||
wantStatus: http.StatusInternalServerError,
|
||||
wantStatus: http.StatusForbidden,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"code":500,"message":"Unknown error: error storing user secret: Why Heavy?"}`,
|
||||
wantBody: `{"code":403,"message":"This Chronograf is private. To gain access, you must be explicitly added by an administrator."}`,
|
||||
},
|
||||
{
|
||||
name: "No Auth",
|
||||
|
@ -624,8 +576,7 @@ func TestService_Me(t *testing.T) {
|
|||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"links":{"self":"/chronograf/v1/me"}}
|
||||
`,
|
||||
wantBody: `{"links":{"self":"/chronograf/v1/me"}}`,
|
||||
},
|
||||
{
|
||||
name: "Empty Principal",
|
||||
|
@ -659,13 +610,24 @@ func TestService_Me(t *testing.T) {
|
|||
fields: fields{
|
||||
UseAuth: true,
|
||||
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{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: "0",
|
||||
Name: "The Bad Place",
|
||||
DefaultRole: roles.MemberRoleName,
|
||||
Public: false,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
|
@ -694,7 +656,7 @@ func TestService_Me(t *testing.T) {
|
|||
},
|
||||
wantStatus: http.StatusForbidden,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"code":403,"message":"This organization is private. To gain access, you must be explicitly added by an administrator."}`,
|
||||
wantBody: `{"code":403,"message":"This Chronograf is private. To gain access, you must be explicitly added by an administrator."}`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
@ -703,12 +665,14 @@ func TestService_Me(t *testing.T) {
|
|||
Store: &mocks.Store{
|
||||
UsersStore: tt.fields.UsersStore,
|
||||
OrganizationsStore: tt.fields.OrganizationsStore,
|
||||
MappingsStore: tt.fields.MappingsStore,
|
||||
ConfigStore: tt.fields.ConfigStore,
|
||||
},
|
||||
Logger: tt.fields.Logger,
|
||||
UseAuth: tt.fields.UseAuth,
|
||||
}
|
||||
|
||||
fmt.Println(tt.name)
|
||||
s.Me(tt.args.w, tt.args.r)
|
||||
|
||||
resp := tt.args.w.Result()
|
||||
|
@ -792,7 +756,6 @@ func TestService_UpdateMe(t *testing.T) {
|
|||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.AdminRoleName,
|
||||
Public: true,
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -805,13 +768,11 @@ func TestService_UpdateMe(t *testing.T) {
|
|||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.AdminRoleName,
|
||||
Public: true,
|
||||
}, nil
|
||||
case "1337":
|
||||
return &chronograf.Organization{
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
Public: true,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
|
@ -824,7 +785,7 @@ func TestService_UpdateMe(t *testing.T) {
|
|||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"name":"me","roles":[{"name":"admin","organization":"1337"},{"name":"admin","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/1337/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"admin","public":true},{"id":"1337","name":"The ShillBillThrilliettas","public":true}],"currentOrganization":{"id":"1337","name":"The ShillBillThrilliettas","public":true}}`,
|
||||
wantBody: `{"name":"me","roles":[{"name":"admin","organization":"1337"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/1337/users/0"},"organizations":[{"id":"1337","name":"The ShillBillThrilliettas"}],"currentOrganization":{"id":"1337","name":"The ShillBillThrilliettas"}}`,
|
||||
},
|
||||
{
|
||||
name: "Change the current User's organization",
|
||||
|
@ -866,7 +827,6 @@ func TestService_UpdateMe(t *testing.T) {
|
|||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.EditorRoleName,
|
||||
Public: true,
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -876,16 +836,14 @@ func TestService_UpdateMe(t *testing.T) {
|
|||
switch *q.ID {
|
||||
case "1337":
|
||||
return &chronograf.Organization{
|
||||
ID: "1337",
|
||||
Name: "The ThrillShilliettos",
|
||||
Public: false,
|
||||
ID: "1337",
|
||||
Name: "The ThrillShilliettos",
|
||||
}, nil
|
||||
case "0":
|
||||
return &chronograf.Organization{
|
||||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.EditorRoleName,
|
||||
Public: true,
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
|
@ -899,7 +857,7 @@ func TestService_UpdateMe(t *testing.T) {
|
|||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"name":"me","roles":[{"name":"admin","organization":"1337"},{"name":"editor","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/1337/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"editor","public":true},{"id":"1337","name":"The ThrillShilliettos","public":false}],"currentOrganization":{"id":"1337","name":"The ThrillShilliettos","public":false}}`,
|
||||
wantBody: `{"name":"me","roles":[{"name":"admin","organization":"1337"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/1337/users/0"},"organizations":[{"id":"1337","name":"The ThrillShilliettos"}],"currentOrganization":{"id":"1337","name":"The ThrillShilliettos"}}`,
|
||||
},
|
||||
{
|
||||
name: "Unable to find requested user in valid organization",
|
||||
|
@ -946,9 +904,8 @@ func TestService_UpdateMe(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
Public: true,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
|
|
|
@ -141,6 +141,13 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
|
|||
router.PATCH("/chronograf/v1/organizations/:oid", EnsureSuperAdmin(service.UpdateOrganization))
|
||||
router.DELETE("/chronograf/v1/organizations/:oid", EnsureSuperAdmin(service.RemoveOrganization))
|
||||
|
||||
// Mappings
|
||||
router.GET("/chronograf/v1/mappings", EnsureSuperAdmin(service.Mappings))
|
||||
router.POST("/chronograf/v1/mappings", EnsureSuperAdmin(service.NewMapping))
|
||||
|
||||
router.PUT("/chronograf/v1/mappings/:id", EnsureSuperAdmin(service.UpdateMapping))
|
||||
router.DELETE("/chronograf/v1/mappings/:id", EnsureSuperAdmin(service.RemoveMapping))
|
||||
|
||||
// Sources
|
||||
router.GET("/chronograf/v1/sources", EnsureViewer(service.Sources))
|
||||
router.POST("/chronograf/v1/sources", EnsureEditor(service.NewSource))
|
||||
|
|
|
@ -15,7 +15,6 @@ import (
|
|||
type organizationRequest struct {
|
||||
Name string `json:"name"`
|
||||
DefaultRole string `json:"defaultRole"`
|
||||
Public *bool `json:"public"`
|
||||
}
|
||||
|
||||
func (r *organizationRequest) ValidCreate() error {
|
||||
|
@ -27,7 +26,7 @@ func (r *organizationRequest) ValidCreate() error {
|
|||
}
|
||||
|
||||
func (r *organizationRequest) ValidUpdate() error {
|
||||
if r.Name == "" && r.DefaultRole == "" && r.Public == nil {
|
||||
if r.Name == "" && r.DefaultRole == "" {
|
||||
return fmt.Errorf("No fields to update")
|
||||
}
|
||||
|
||||
|
@ -119,10 +118,6 @@ func (s *Service) NewOrganization(w http.ResponseWriter, r *http.Request) {
|
|||
DefaultRole: req.DefaultRole,
|
||||
}
|
||||
|
||||
if req.Public != nil {
|
||||
org.Public = *req.Public
|
||||
}
|
||||
|
||||
res, err := s.Store.Organizations(ctx).Add(ctx, org)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
|
||||
|
@ -207,10 +202,6 @@ func (s *Service) UpdateOrganization(w http.ResponseWriter, r *http.Request) {
|
|||
org.DefaultRole = req.DefaultRole
|
||||
}
|
||||
|
||||
if req.Public != nil {
|
||||
org.Public = *req.Public
|
||||
}
|
||||
|
||||
err = s.Store.Organizations(ctx).Update(ctx, org)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
|
||||
|
|
|
@ -52,9 +52,8 @@ func TestService_OrganizationID(t *testing.T) {
|
|||
switch *q.ID {
|
||||
case "1337":
|
||||
return &chronograf.Organization{
|
||||
ID: "1337",
|
||||
Name: "The Good Place",
|
||||
Public: false,
|
||||
ID: "1337",
|
||||
Name: "The Good Place",
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("Organization with ID %s not found", *q.ID)
|
||||
|
@ -65,7 +64,38 @@ func TestService_OrganizationID(t *testing.T) {
|
|||
id: "1337",
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"links":{"self":"/chronograf/v1/organizations/1337"},"id":"1337","name":"The Good Place","public":false}`,
|
||||
wantBody: `{"links":{"self":"/chronograf/v1/organizations/1337"},"id":"1337","name":"The Good Place"}`,
|
||||
},
|
||||
{
|
||||
name: "Get Single Organization",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest(
|
||||
"GET",
|
||||
"http://any.url", // can be any valid URL as we are bypassing mux
|
||||
nil,
|
||||
),
|
||||
},
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
switch *q.ID {
|
||||
case "1337":
|
||||
return &chronograf.Organization{
|
||||
ID: "1337",
|
||||
Name: "The Good Place",
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("Organization with ID %s not found", *q.ID)
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
id: "1337",
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"id":"1337","name":"The Good Place","links":{"self":"/chronograf/v1/organizations/1337"}}`,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -124,7 +154,7 @@ func TestService_Organizations(t *testing.T) {
|
|||
wantBody string
|
||||
}{
|
||||
{
|
||||
name: "Get Single Organization",
|
||||
name: "Get Organizations",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest(
|
||||
|
@ -139,14 +169,12 @@ func TestService_Organizations(t *testing.T) {
|
|||
AllF: func(ctx context.Context) ([]chronograf.Organization, error) {
|
||||
return []chronograf.Organization{
|
||||
chronograf.Organization{
|
||||
ID: "1337",
|
||||
Name: "The Good Place",
|
||||
Public: false,
|
||||
ID: "1337",
|
||||
Name: "The Good Place",
|
||||
},
|
||||
chronograf.Organization{
|
||||
ID: "100",
|
||||
Name: "The Bad Place",
|
||||
Public: false,
|
||||
ID: "100",
|
||||
Name: "The Bad Place",
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
|
@ -154,7 +182,7 @@ func TestService_Organizations(t *testing.T) {
|
|||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"links":{"self":"/chronograf/v1/organizations"},"organizations":[{"links":{"self":"/chronograf/v1/organizations/1337"},"id":"1337","name":"The Good Place","public":false},{"links":{"self":"/chronograf/v1/organizations/100"},"id":"100","name":"The Bad Place","public":false}]}`,
|
||||
wantBody: `{"links":{"self":"/chronograf/v1/organizations"},"organizations":[{"links":{"self":"/chronograf/v1/organizations/1337"},"id":"1337","name":"The Good Place"},{"links":{"self":"/chronograf/v1/organizations/100"},"id":"100","name":"The Bad Place"}]}`,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -195,7 +223,6 @@ func TestService_UpdateOrganization(t *testing.T) {
|
|||
w *httptest.ResponseRecorder
|
||||
r *http.Request
|
||||
org *organizationRequest
|
||||
public bool
|
||||
setPtr bool
|
||||
}
|
||||
tests := []struct {
|
||||
|
@ -231,7 +258,6 @@ func TestService_UpdateOrganization(t *testing.T) {
|
|||
ID: "1337",
|
||||
Name: "The Good Place",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: false,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
|
@ -239,41 +265,7 @@ func TestService_UpdateOrganization(t *testing.T) {
|
|||
id: "1337",
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"id":"1337","name":"The Bad Place","defaultRole":"viewer","links":{"self":"/chronograf/v1/organizations/1337"},"public":false}`,
|
||||
},
|
||||
{
|
||||
name: "Update Organization public",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest(
|
||||
"GET",
|
||||
"http://any.url", // can be any valid URL as we are bypassing mux
|
||||
nil,
|
||||
),
|
||||
org: &organizationRequest{},
|
||||
public: false,
|
||||
setPtr: true,
|
||||
},
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
UpdateF: func(ctx context.Context, o *chronograf.Organization) error {
|
||||
return nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: "0",
|
||||
Name: "The Good Place",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
id: "0",
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"id":"0","name":"The Good Place","defaultRole":"viewer","public":false,"links":{"self":"/chronograf/v1/organizations/0"}}`,
|
||||
wantBody: `{"id":"1337","name":"The Bad Place","defaultRole":"viewer","links":{"self":"/chronograf/v1/organizations/1337"}}`,
|
||||
},
|
||||
{
|
||||
name: "Update Organization - nothing to update",
|
||||
|
@ -297,7 +289,6 @@ func TestService_UpdateOrganization(t *testing.T) {
|
|||
ID: "1337",
|
||||
Name: "The Good Place",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
|
@ -331,7 +322,6 @@ func TestService_UpdateOrganization(t *testing.T) {
|
|||
ID: "1337",
|
||||
Name: "The Good Place",
|
||||
DefaultRole: roles.MemberRoleName,
|
||||
Public: false,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
|
@ -339,7 +329,7 @@ func TestService_UpdateOrganization(t *testing.T) {
|
|||
id: "1337",
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"links":{"self":"/chronograf/v1/organizations/1337"},"id":"1337","name":"The Good Place","defaultRole":"viewer","public":false}`,
|
||||
wantBody: `{"links":{"self":"/chronograf/v1/organizations/1337"},"id":"1337","name":"The Good Place","defaultRole":"viewer"}`,
|
||||
},
|
||||
{
|
||||
name: "Update Organization - invalid update",
|
||||
|
@ -416,10 +406,6 @@ func TestService_UpdateOrganization(t *testing.T) {
|
|||
},
|
||||
}))
|
||||
|
||||
if tt.args.setPtr {
|
||||
tt.args.org.Public = &tt.args.public
|
||||
}
|
||||
|
||||
buf, _ := json.Marshal(tt.args.org)
|
||||
tt.args.r.Body = ioutil.NopCloser(bytes.NewReader(buf))
|
||||
s.UpdateOrganization(tt.args.w, tt.args.r)
|
||||
|
@ -573,16 +559,54 @@ func TestService_NewOrganization(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
AddF: func(ctx context.Context, o *chronograf.Organization) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: "1337",
|
||||
Name: "The Good Place",
|
||||
Public: false,
|
||||
ID: "1337",
|
||||
Name: "The Good Place",
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wantStatus: http.StatusCreated,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"id":"1337","public":false,"name":"The Good Place","links":{"self":"/chronograf/v1/organizations/1337"}}`,
|
||||
wantBody: `{"id":"1337","name":"The Good Place","links":{"self":"/chronograf/v1/organizations/1337"}}`,
|
||||
},
|
||||
{
|
||||
name: "Fail to create Organization - no org name",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest(
|
||||
"GET",
|
||||
"http://any.url", // can be any valid URL as we are bypassing mux
|
||||
nil,
|
||||
),
|
||||
user: &chronograf.User{
|
||||
ID: 1,
|
||||
Name: "bobetta",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
},
|
||||
org: &organizationRequest{},
|
||||
},
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
UsersStore: &mocks.UsersStore{
|
||||
AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
|
||||
return &chronograf.User{
|
||||
ID: 1,
|
||||
Name: "bobetta",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
AddF: func(ctx context.Context, o *chronograf.Organization) (*chronograf.Organization, error) {
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wantStatus: http.StatusUnprocessableEntity,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"code":422,"message":"Name required on Chronograf Organization request body"}`,
|
||||
},
|
||||
{
|
||||
name: "Create Organization - no user on context",
|
||||
|
|
|
@ -484,6 +484,7 @@ func openService(ctx context.Context, buildInfo chronograf.BuildInfo, boltPath s
|
|||
OrganizationsStore: organizations,
|
||||
UsersStore: db.UsersStore,
|
||||
ConfigStore: db.ConfigStore,
|
||||
MappingsStore: db.MappingsStore,
|
||||
},
|
||||
Logger: logger,
|
||||
UseAuth: useAuth,
|
||||
|
|
|
@ -48,7 +48,8 @@ func (c *InfluxClient) New(src chronograf.Source, logger chronograf.Logger) (chr
|
|||
}
|
||||
if src.Type == chronograf.InfluxEnterprise && src.MetaURL != "" {
|
||||
tls := strings.Contains(src.MetaURL, "https")
|
||||
return enterprise.NewClientWithTimeSeries(logger, src.MetaURL, influx.DefaultAuthorization(&src), tls, client)
|
||||
insecure := src.InsecureSkipVerify
|
||||
return enterprise.NewClientWithTimeSeries(logger, src.MetaURL, influx.DefaultAuthorization(&src), tls, insecure, client)
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
|
|
@ -88,6 +88,7 @@ type DataStore interface {
|
|||
Layouts(ctx context.Context) chronograf.LayoutsStore
|
||||
Users(ctx context.Context) chronograf.UsersStore
|
||||
Organizations(ctx context.Context) chronograf.OrganizationsStore
|
||||
Mappings(ctx context.Context) chronograf.MappingsStore
|
||||
Dashboards(ctx context.Context) chronograf.DashboardsStore
|
||||
Config(ctx context.Context) chronograf.ConfigStore
|
||||
}
|
||||
|
@ -102,6 +103,7 @@ type Store struct {
|
|||
LayoutsStore chronograf.LayoutsStore
|
||||
UsersStore chronograf.UsersStore
|
||||
DashboardsStore chronograf.DashboardsStore
|
||||
MappingsStore chronograf.MappingsStore
|
||||
OrganizationsStore chronograf.OrganizationsStore
|
||||
ConfigStore chronograf.ConfigStore
|
||||
}
|
||||
|
@ -191,3 +193,14 @@ func (s *Store) Config(ctx context.Context) chronograf.ConfigStore {
|
|||
}
|
||||
return &noop.ConfigStore{}
|
||||
}
|
||||
|
||||
// Mappings returns the underlying MappingsStore.
|
||||
func (s *Store) Mappings(ctx context.Context) chronograf.MappingsStore {
|
||||
if isServer := hasServerContext(ctx); isServer {
|
||||
return s.MappingsStore
|
||||
}
|
||||
if isSuperAdmin := hasSuperAdminContext(ctx); isSuperAdmin {
|
||||
return s.MappingsStore
|
||||
}
|
||||
return &noop.MappingsStore{}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"info": {
|
||||
"title": "Chronograf",
|
||||
"description": "API endpoints for Chronograf",
|
||||
"version": "1.4.0.1"
|
||||
"version": "1.4.1.3"
|
||||
},
|
||||
"schemes": ["http"],
|
||||
"basePath": "/chronograf/v1",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "chronograf-ui",
|
||||
"version": "1.4.0-1",
|
||||
"version": "1.4.1-3",
|
||||
"private": false,
|
||||
"license": "AGPL-3.0",
|
||||
"description": "",
|
||||
|
|
|
@ -36,13 +36,15 @@ class CheckSources extends Component {
|
|||
}
|
||||
|
||||
async componentWillMount() {
|
||||
const {auth: {isUsingAuth, me}} = this.props
|
||||
const {router, auth: {isUsingAuth, me}} = this.props
|
||||
|
||||
if (!isUsingAuth || isUserAuthorized(me.role, VIEWER_ROLE)) {
|
||||
await this.props.getSources()
|
||||
this.setState({isFetching: false})
|
||||
} else {
|
||||
router.push('/purgatory')
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({isFetching: false})
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
|
@ -66,7 +68,7 @@ class CheckSources extends Component {
|
|||
params,
|
||||
errorThrown,
|
||||
sources,
|
||||
auth: {isUsingAuth, me, me: {organizations, currentOrganization}},
|
||||
auth: {isUsingAuth, me, me: {organizations = [], currentOrganization}},
|
||||
notify,
|
||||
getSources,
|
||||
} = nextProps
|
||||
|
@ -81,6 +83,14 @@ class CheckSources extends Component {
|
|||
return router.push('/')
|
||||
}
|
||||
|
||||
if (!isFetching && isUsingAuth && !organizations.length) {
|
||||
notify(
|
||||
'error',
|
||||
'You have been removed from all organizations. Please contact your administrator.'
|
||||
)
|
||||
return router.push('/purgatory')
|
||||
}
|
||||
|
||||
if (
|
||||
me.superAdmin &&
|
||||
!organizations.find(o => o.id === currentOrganization.id)
|
||||
|
|
|
@ -10,6 +10,10 @@ import {
|
|||
createOrganization as createOrganizationAJAX,
|
||||
updateOrganization as updateOrganizationAJAX,
|
||||
deleteOrganization as deleteOrganizationAJAX,
|
||||
getMappings as getMappingsAJAX,
|
||||
createMapping as createMappingAJAX,
|
||||
updateMapping as updateMappingAJAX,
|
||||
deleteMapping as deleteMappingAJAX,
|
||||
} from 'src/admin/apis/chronograf'
|
||||
|
||||
import {publishAutoDismissingNotification} from 'shared/dispatchers'
|
||||
|
@ -94,6 +98,35 @@ export const removeOrganization = organization => ({
|
|||
},
|
||||
})
|
||||
|
||||
export const loadMappings = ({mappings}) => ({
|
||||
type: 'CHRONOGRAF_LOAD_MAPPINGS',
|
||||
payload: {
|
||||
mappings,
|
||||
},
|
||||
})
|
||||
|
||||
export const updateMapping = (staleMapping, updatedMapping) => ({
|
||||
type: 'CHRONOGRAF_UPDATE_MAPPING',
|
||||
payload: {
|
||||
staleMapping,
|
||||
updatedMapping,
|
||||
},
|
||||
})
|
||||
|
||||
export const addMapping = mapping => ({
|
||||
type: 'CHRONOGRAF_ADD_MAPPING',
|
||||
payload: {
|
||||
mapping,
|
||||
},
|
||||
})
|
||||
|
||||
export const removeMapping = mapping => ({
|
||||
type: 'CHRONOGRAF_REMOVE_MAPPING',
|
||||
payload: {
|
||||
mapping,
|
||||
},
|
||||
})
|
||||
|
||||
// async actions (thunks)
|
||||
export const loadUsersAsync = url => async dispatch => {
|
||||
try {
|
||||
|
@ -113,6 +146,62 @@ export const loadOrganizationsAsync = url => async dispatch => {
|
|||
}
|
||||
}
|
||||
|
||||
export const loadMappingsAsync = () => async dispatch => {
|
||||
try {
|
||||
const {data} = await getMappingsAJAX()
|
||||
dispatch(loadMappings(data))
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
}
|
||||
}
|
||||
|
||||
export const createMappingAsync = (url, mapping) => async dispatch => {
|
||||
const mappingWithTempId = {...mapping, _tempID: uuid.v4()}
|
||||
dispatch(addMapping(mappingWithTempId))
|
||||
try {
|
||||
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}`
|
||||
dispatch(errorThrown(error, message))
|
||||
setTimeout(
|
||||
() => dispatch(removeMapping(mappingWithTempId)),
|
||||
REVERT_STATE_DELAY
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteMappingAsync = mapping => async dispatch => {
|
||||
dispatch(removeMapping(mapping))
|
||||
try {
|
||||
await deleteMappingAJAX(mapping)
|
||||
dispatch(
|
||||
publishAutoDismissingNotification(
|
||||
'success',
|
||||
`Mapping deleted: ${mapping.id} ${mapping.scheme}`
|
||||
)
|
||||
)
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
dispatch(addMapping(mapping))
|
||||
}
|
||||
}
|
||||
|
||||
export const updateMappingAsync = (
|
||||
staleMapping,
|
||||
updatedMapping
|
||||
) => async dispatch => {
|
||||
dispatch(updateMapping(staleMapping, updatedMapping))
|
||||
try {
|
||||
await updateMappingAJAX(updatedMapping)
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
dispatch(updateMapping(updatedMapping, staleMapping))
|
||||
}
|
||||
}
|
||||
|
||||
export const createUserAsync = (url, user) => async dispatch => {
|
||||
// temp uuid is added to be able to disambiguate a created user that has the
|
||||
// same scheme, provider, and name as an existing user
|
||||
|
|
|
@ -102,3 +102,54 @@ export const deleteOrganization = async organization => {
|
|||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Mappings
|
||||
export const createMapping = async (url, mapping) => {
|
||||
try {
|
||||
return await AJAX({
|
||||
method: 'POST',
|
||||
resource: 'mappings',
|
||||
data: mapping,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const getMappings = async () => {
|
||||
try {
|
||||
return await AJAX({
|
||||
method: 'GET',
|
||||
resource: 'mappings',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const updateMapping = async mapping => {
|
||||
try {
|
||||
return await AJAX({
|
||||
method: 'PUT',
|
||||
url: mapping.links.self,
|
||||
data: mapping,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteMapping = async mapping => {
|
||||
try {
|
||||
return await AJAX({
|
||||
method: 'DELETE',
|
||||
url: mapping.links.self,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,23 +9,18 @@ import {
|
|||
import {Tab, Tabs, TabPanel, TabPanels, TabList} from 'shared/components/Tabs'
|
||||
import OrganizationsPage from 'src/admin/containers/chronograf/OrganizationsPage'
|
||||
import UsersPage from 'src/admin/containers/chronograf/UsersPage'
|
||||
import ProvidersPage from 'src/admin/containers/ProvidersPage'
|
||||
import AllUsersPage from 'src/admin/containers/chronograf/AllUsersPage'
|
||||
|
||||
const ORGANIZATIONS_TAB_NAME = 'Organizations'
|
||||
const CURRENT_ORG_USERS_TAB_NAME = 'Current Org Users'
|
||||
const ORGANIZATIONS_TAB_NAME = 'All Orgs'
|
||||
const PROVIDERS_TAB_NAME = 'Org Mappings'
|
||||
const CURRENT_ORG_USERS_TAB_NAME = 'Current Org'
|
||||
const ALL_USERS_TAB_NAME = 'All Users'
|
||||
|
||||
const AdminTabs = ({
|
||||
me: {currentOrganization: meCurrentOrganization, role: meRole, id: meID},
|
||||
}) => {
|
||||
const tabs = [
|
||||
{
|
||||
requiredRole: SUPERADMIN_ROLE,
|
||||
type: ORGANIZATIONS_TAB_NAME,
|
||||
component: (
|
||||
<OrganizationsPage meCurrentOrganization={meCurrentOrganization} />
|
||||
),
|
||||
},
|
||||
{
|
||||
requiredRole: ADMIN_ROLE,
|
||||
type: CURRENT_ORG_USERS_TAB_NAME,
|
||||
|
@ -38,6 +33,18 @@ const AdminTabs = ({
|
|||
type: ALL_USERS_TAB_NAME,
|
||||
component: <AllUsersPage meID={meID} />,
|
||||
},
|
||||
{
|
||||
requiredRole: SUPERADMIN_ROLE,
|
||||
type: ORGANIZATIONS_TAB_NAME,
|
||||
component: (
|
||||
<OrganizationsPage meCurrentOrganization={meCurrentOrganization} />
|
||||
),
|
||||
},
|
||||
{
|
||||
requiredRole: SUPERADMIN_ROLE,
|
||||
type: PROVIDERS_TAB_NAME,
|
||||
component: <ProvidersPage />,
|
||||
},
|
||||
].filter(t => isUserAuthorized(meRole, t.requiredRole))
|
||||
|
||||
return (
|
||||
|
|
|
@ -19,7 +19,7 @@ const AllUsersTableHeader = ({
|
|||
return (
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<h2 className="panel-title">
|
||||
{numUsersString} in {numOrganizationsString}
|
||||
{numUsersString} across {numOrganizationsString}
|
||||
</h2>
|
||||
<div style={{display: 'flex', alignItems: 'center'}}>
|
||||
<div className="all-users-admin-toggle">
|
||||
|
|
|
@ -4,9 +4,6 @@ import uuid from 'node-uuid'
|
|||
|
||||
import OrganizationsTableRow from 'src/admin/components/chronograf/OrganizationsTableRow'
|
||||
import OrganizationsTableRowNew from 'src/admin/components/chronograf/OrganizationsTableRowNew'
|
||||
import QuestionMarkTooltip from 'shared/components/QuestionMarkTooltip'
|
||||
|
||||
import {PUBLIC_TOOLTIP} from 'src/admin/constants/index'
|
||||
|
||||
class OrganizationsTable extends Component {
|
||||
constructor(props) {
|
||||
|
@ -37,7 +34,6 @@ class OrganizationsTable extends Component {
|
|||
onDeleteOrg,
|
||||
onRenameOrg,
|
||||
onChooseDefaultRole,
|
||||
onTogglePublic,
|
||||
currentOrganization,
|
||||
} = this.props
|
||||
const {isCreatingOrganization} = this.state
|
||||
|
@ -71,15 +67,13 @@ class OrganizationsTable extends Component {
|
|||
</button>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
<div className="orgs-table--org-labels">
|
||||
<div className="orgs-table--active" />
|
||||
<div className="orgs-table--name">Name</div>
|
||||
<div className="orgs-table--public">
|
||||
Public{' '}
|
||||
<QuestionMarkTooltip tipID="public" tipContent={PUBLIC_TOOLTIP} />
|
||||
<div className="fancytable--labels">
|
||||
<div className="fancytable--th orgs-table--active" />
|
||||
<div className="fancytable--th orgs-table--name">Name</div>
|
||||
<div className="fancytable--th orgs-table--default-role">
|
||||
Default Role
|
||||
</div>
|
||||
<div className="orgs-table--default-role">Default Role</div>
|
||||
<div className="orgs-table--delete" />
|
||||
<div className="fancytable--th orgs-table--delete" />
|
||||
</div>
|
||||
{isCreatingOrganization
|
||||
? <OrganizationsTableRowNew
|
||||
|
@ -91,7 +85,6 @@ class OrganizationsTable extends Component {
|
|||
<OrganizationsTableRow
|
||||
key={uuid.v4()}
|
||||
organization={org}
|
||||
onTogglePublic={onTogglePublic}
|
||||
onDelete={onDeleteOrg}
|
||||
onRename={onRenameOrg}
|
||||
onChooseDefaultRole={onChooseDefaultRole}
|
||||
|
@ -120,7 +113,6 @@ OrganizationsTable.propTypes = {
|
|||
onCreateOrg: func.isRequired,
|
||||
onDeleteOrg: func.isRequired,
|
||||
onRenameOrg: func.isRequired,
|
||||
onTogglePublic: func.isRequired,
|
||||
onChooseDefaultRole: func.isRequired,
|
||||
}
|
||||
export default OrganizationsTable
|
||||
|
|
|
@ -3,9 +3,9 @@ import {connect} from 'react-redux'
|
|||
import {bindActionCreators} from 'redux'
|
||||
import {withRouter} from 'react-router'
|
||||
|
||||
import SlideToggle from 'shared/components/SlideToggle'
|
||||
import ConfirmButtons from 'shared/components/ConfirmButtons'
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
import InputClickToEdit from 'shared/components/InputClickToEdit'
|
||||
|
||||
import {meChangeOrganizationAsync} from 'shared/actions/auth'
|
||||
|
||||
|
@ -32,9 +32,7 @@ class OrganizationsTableRow extends Component {
|
|||
super(props)
|
||||
|
||||
this.state = {
|
||||
isEditing: false,
|
||||
isDeleting: false,
|
||||
workingName: this.props.organization.name,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,55 +42,10 @@ class OrganizationsTableRow extends Component {
|
|||
await meChangeOrganization(links.me, {organization: organization.id})
|
||||
router.push('')
|
||||
}
|
||||
|
||||
handleNameClick = () => {
|
||||
this.setState({isEditing: true})
|
||||
handleUpdateOrgName = newName => {
|
||||
const {organization, onRename} = this.props
|
||||
onRename(organization, newName)
|
||||
}
|
||||
|
||||
handleConfirmRename = () => {
|
||||
const {onRename, organization} = this.props
|
||||
const {workingName} = this.state
|
||||
|
||||
onRename(organization, workingName)
|
||||
this.setState({workingName, isEditing: false})
|
||||
}
|
||||
|
||||
handleCancelRename = () => {
|
||||
const {organization} = this.props
|
||||
|
||||
this.setState({
|
||||
workingName: organization.name,
|
||||
isEditing: false,
|
||||
})
|
||||
}
|
||||
|
||||
handleInputChange = e => {
|
||||
this.setState({workingName: e.target.value})
|
||||
}
|
||||
|
||||
handleInputBlur = () => {
|
||||
const {organization} = this.props
|
||||
const {workingName} = this.state
|
||||
|
||||
if (organization.name === workingName) {
|
||||
this.handleCancelRename()
|
||||
} else {
|
||||
this.handleConfirmRename()
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown = e => {
|
||||
if (e.key === 'Enter') {
|
||||
this.handleInputBlur()
|
||||
} else if (e.key === 'Escape') {
|
||||
this.handleCancelRename()
|
||||
}
|
||||
}
|
||||
|
||||
handleFocus = e => {
|
||||
e.target.select()
|
||||
}
|
||||
|
||||
handleDeleteClick = () => {
|
||||
this.setState({isDeleting: true})
|
||||
}
|
||||
|
@ -106,18 +59,13 @@ class OrganizationsTableRow extends Component {
|
|||
onDelete(organization)
|
||||
}
|
||||
|
||||
handleTogglePublic = () => {
|
||||
const {organization, onTogglePublic} = this.props
|
||||
onTogglePublic(organization)
|
||||
}
|
||||
|
||||
handleChooseDefaultRole = role => {
|
||||
const {organization, onChooseDefaultRole} = this.props
|
||||
onChooseDefaultRole(organization, role.name)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {workingName, isEditing, isDeleting} = this.state
|
||||
const {isDeleting} = this.state
|
||||
const {organization, currentOrganization} = this.props
|
||||
|
||||
const dropdownRolesItems = USER_ROLES.map(role => ({
|
||||
|
@ -126,12 +74,12 @@ class OrganizationsTableRow extends Component {
|
|||
}))
|
||||
|
||||
const defaultRoleClassName = isDeleting
|
||||
? 'orgs-table--default-role editing'
|
||||
: 'orgs-table--default-role'
|
||||
? 'fancytable--td orgs-table--default-role deleting'
|
||||
: 'fancytable--td orgs-table--default-role'
|
||||
|
||||
return (
|
||||
<div className="orgs-table--org">
|
||||
<div className="orgs-table--active">
|
||||
<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
|
||||
|
@ -143,32 +91,11 @@ class OrganizationsTableRow extends Component {
|
|||
<span className="icon shuffle" /> Switch to
|
||||
</button>}
|
||||
</div>
|
||||
{isEditing
|
||||
? <input
|
||||
type="text"
|
||||
className="form-control input-sm orgs-table--input"
|
||||
defaultValue={workingName}
|
||||
onChange={this.handleInputChange}
|
||||
onBlur={this.handleInputBlur}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
placeholder="Name this Organization..."
|
||||
autoFocus={true}
|
||||
onFocus={this.handleFocus}
|
||||
ref={r => (this.inputRef = r)}
|
||||
/>
|
||||
: <div className="orgs-table--name" onClick={this.handleNameClick}>
|
||||
{workingName}
|
||||
<span className="icon pencil" />
|
||||
</div>}
|
||||
{organization.id === DEFAULT_ORG_ID
|
||||
? <div className="orgs-table--public">
|
||||
<SlideToggle
|
||||
size="xs"
|
||||
active={organization.public}
|
||||
onToggle={this.handleTogglePublic}
|
||||
/>
|
||||
</div>
|
||||
: <div className="orgs-table--public disabled">—</div>}
|
||||
<InputClickToEdit
|
||||
value={organization.name}
|
||||
wrapperClass="fancytable--td orgs-table--name"
|
||||
onUpdate={this.handleUpdateOrgName}
|
||||
/>
|
||||
<div className={defaultRoleClassName}>
|
||||
<Dropdown
|
||||
items={dropdownRolesItems}
|
||||
|
@ -204,7 +131,6 @@ OrganizationsTableRow.propTypes = {
|
|||
}).isRequired,
|
||||
onDelete: func.isRequired,
|
||||
onRename: func.isRequired,
|
||||
onTogglePublic: func.isRequired,
|
||||
onChooseDefaultRole: func.isRequired,
|
||||
currentOrganization: shape({
|
||||
name: string.isRequired,
|
||||
|
|
|
@ -58,20 +58,22 @@ class OrganizationsTableRowNew extends Component {
|
|||
}))
|
||||
|
||||
return (
|
||||
<div className="orgs-table--org orgs-table--new-org">
|
||||
<div className="orgs-table--active">—</div>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control input-sm orgs-table--input"
|
||||
value={name}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onChange={this.handleInputChange}
|
||||
onFocus={this.handleInputFocus}
|
||||
placeholder="Name this Organization..."
|
||||
autoFocus={true}
|
||||
ref={r => (this.inputRef = r)}
|
||||
/>
|
||||
<div className="orgs-table--default-role editing">
|
||||
<div className="fancytable--row">
|
||||
<div className="fancytable--td orgs-table--active">—</div>
|
||||
<div className="fancytable--td orgs-table--name">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control input-sm"
|
||||
value={name}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onChange={this.handleInputChange}
|
||||
onFocus={this.handleInputFocus}
|
||||
placeholder="Name this Organization..."
|
||||
autoFocus={true}
|
||||
ref={r => (this.inputRef = r)}
|
||||
/>
|
||||
</div>
|
||||
<div className="fancytable--td orgs-table--default-role deleting">
|
||||
<Dropdown
|
||||
items={dropdownRolesItems}
|
||||
onChoose={this.handleChooseDefaultRole}
|
||||
|
|
|
@ -0,0 +1,149 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
|
||||
import uuid from 'node-uuid'
|
||||
import ProvidersTableRow from 'src/admin/components/chronograf/ProvidersTableRow'
|
||||
import ProvidersTableRowNew from 'src/admin/components/chronograf/ProvidersTableRowNew'
|
||||
|
||||
class ProvidersTable extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isCreatingMap: false,
|
||||
}
|
||||
}
|
||||
handleClickCreateMap = () => {
|
||||
this.setState({isCreatingMap: true})
|
||||
}
|
||||
|
||||
handleCancelCreateMap = () => {
|
||||
this.setState({isCreatingMap: false})
|
||||
}
|
||||
|
||||
handleCreateMap = newMap => {
|
||||
this.props.onCreateMap(newMap)
|
||||
this.setState({isCreatingMap: false})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
mappings = [],
|
||||
organizations,
|
||||
onUpdateMap,
|
||||
onDeleteMap,
|
||||
isLoading,
|
||||
} = this.props
|
||||
const {isCreatingMap} = this.state
|
||||
|
||||
const tableTitle =
|
||||
mappings.length === 1 ? '1 Map' : `${mappings.length} Maps`
|
||||
|
||||
// define scheme options
|
||||
const SCHEMES = [{text: '*'}, {text: 'oauth2'}]
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="panel panel-default">
|
||||
<div className="panel-body">
|
||||
<div className="page-spinner" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="panel panel-default">
|
||||
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
|
||||
<h2 className="panel-title">
|
||||
{tableTitle}
|
||||
</h2>
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={this.handleClickCreateMap}
|
||||
disabled={isCreatingMap}
|
||||
>
|
||||
<span className="icon plus" /> Create Mapping
|
||||
</button>
|
||||
</div>
|
||||
{mappings.length || isCreatingMap
|
||||
? <div className="panel-body">
|
||||
<div className="fancytable--labels">
|
||||
<div className="fancytable--th provider--scheme">Scheme</div>
|
||||
<div className="fancytable--th provider--provider">
|
||||
Provider
|
||||
</div>
|
||||
<div className="fancytable--th provider--providerorg">
|
||||
Provider Org
|
||||
</div>
|
||||
<div className="fancytable--th provider--arrow" />
|
||||
<div className="fancytable--th provider--redirect">
|
||||
Organization
|
||||
</div>
|
||||
<div className="fancytable--th" />
|
||||
<div className="fancytable--th provider--delete" />
|
||||
</div>
|
||||
{mappings.map((mapping, i) =>
|
||||
<ProvidersTableRow
|
||||
key={uuid.v4()}
|
||||
mapping={mapping}
|
||||
organizations={organizations}
|
||||
schemes={SCHEMES}
|
||||
onDelete={onDeleteMap}
|
||||
onUpdate={onUpdateMap}
|
||||
rowIndex={i + 1}
|
||||
/>
|
||||
)}
|
||||
{isCreatingMap
|
||||
? <ProvidersTableRowNew
|
||||
organizations={organizations}
|
||||
schemes={SCHEMES}
|
||||
onCreate={this.handleCreateMap}
|
||||
onCancel={this.handleCancelCreateMap}
|
||||
rowIndex={mappings.length + 1}
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
: <div className="panel-body">
|
||||
<div className="generic-empty-state">
|
||||
<h4 style={{margin: '50px 0'}}>
|
||||
Looks like you have no mappings<br />
|
||||
New users will not be able to sign up automatically
|
||||
</h4>
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={this.handleClickCreateMap}
|
||||
disabled={isCreatingMap}
|
||||
>
|
||||
<span className="icon plus" /> Create Mapping
|
||||
</button>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||
|
||||
ProvidersTable.propTypes = {
|
||||
mappings: arrayOf(
|
||||
shape({
|
||||
id: string,
|
||||
scheme: string,
|
||||
provider: string,
|
||||
providerOrganization: string,
|
||||
organizationId: string,
|
||||
})
|
||||
).isRequired,
|
||||
organizations: arrayOf(
|
||||
shape({
|
||||
id: string, // when optimistically created, organization will not have an id
|
||||
name: string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
onCreateMap: func.isRequired,
|
||||
onUpdateMap: func.isRequired,
|
||||
onDeleteMap: func.isRequired,
|
||||
isLoading: bool.isRequired,
|
||||
}
|
||||
export default ProvidersTable
|
|
@ -0,0 +1,150 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
|
||||
import ConfirmButtons from 'shared/components/ConfirmButtons'
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
import InputClickToEdit from 'shared/components/InputClickToEdit'
|
||||
|
||||
import {DEFAULT_MAPPING_ID} from 'src/admin/constants/chronografAdmin'
|
||||
|
||||
class ProvidersTableRow extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
...this.props.mapping,
|
||||
isDeleting: false,
|
||||
}
|
||||
}
|
||||
|
||||
handleDeleteClick = () => {
|
||||
this.setState({isDeleting: true})
|
||||
}
|
||||
|
||||
handleDismissDeleteConfirmation = () => {
|
||||
this.setState({isDeleting: false})
|
||||
}
|
||||
|
||||
handleDeleteMap = mapping => {
|
||||
const {onDelete} = this.props
|
||||
this.setState({isDeleting: false})
|
||||
onDelete(mapping)
|
||||
}
|
||||
|
||||
handleUpdateMapping = changes => {
|
||||
const {onUpdate, mapping} = this.props
|
||||
const newState = {...mapping, ...changes}
|
||||
this.setState(newState)
|
||||
onUpdate(mapping, newState)
|
||||
}
|
||||
|
||||
handleChangeProvider = provider => this.handleUpdateMapping({provider})
|
||||
|
||||
handleChangeProviderOrg = providerOrganization =>
|
||||
this.handleUpdateMapping({providerOrganization})
|
||||
|
||||
handleChooseOrganization = ({id: organizationId}) =>
|
||||
this.handleUpdateMapping({organizationId})
|
||||
|
||||
handleChooseScheme = ({text: scheme}) => this.handleUpdateMapping({scheme})
|
||||
|
||||
render() {
|
||||
const {
|
||||
scheme,
|
||||
provider,
|
||||
providerOrganization,
|
||||
organizationId,
|
||||
isDeleting,
|
||||
} = this.state
|
||||
const {organizations, mapping, schemes, rowIndex} = this.props
|
||||
|
||||
const selectedOrg = organizations.find(o => o.id === organizationId)
|
||||
const orgDropdownItems = organizations.map(role => ({
|
||||
...role,
|
||||
text: role.name,
|
||||
}))
|
||||
|
||||
const organizationIdClassName = isDeleting
|
||||
? 'fancytable--td provider--redirect deleting'
|
||||
: 'fancytable--td provider--redirect'
|
||||
|
||||
const isDefaultMapping = DEFAULT_MAPPING_ID === mapping.id
|
||||
return (
|
||||
<div className="fancytable--row">
|
||||
<Dropdown
|
||||
items={schemes}
|
||||
onChoose={this.handleChooseScheme}
|
||||
selected={scheme}
|
||||
className="fancytable--td provider--scheme"
|
||||
disabled={isDefaultMapping}
|
||||
/>
|
||||
<InputClickToEdit
|
||||
value={provider}
|
||||
wrapperClass="fancytable--td provider--provider"
|
||||
onUpdate={this.handleChangeProvider}
|
||||
disabled={isDefaultMapping}
|
||||
tabIndex={rowIndex}
|
||||
/>
|
||||
<InputClickToEdit
|
||||
value={providerOrganization}
|
||||
wrapperClass="fancytable--td provider--providerorg"
|
||||
onUpdate={this.handleChangeProviderOrg}
|
||||
disabled={isDefaultMapping}
|
||||
tabIndex={rowIndex}
|
||||
/>
|
||||
<div className="fancytable--td provider--arrow">
|
||||
<span />
|
||||
</div>
|
||||
<div className={organizationIdClassName}>
|
||||
<Dropdown
|
||||
items={orgDropdownItems}
|
||||
onChoose={this.handleChooseOrganization}
|
||||
selected={selectedOrg.name}
|
||||
className="dropdown-stretch"
|
||||
disabled={isDefaultMapping}
|
||||
/>
|
||||
</div>
|
||||
{isDeleting
|
||||
? <ConfirmButtons
|
||||
item={mapping}
|
||||
onCancel={this.handleDismissDeleteConfirmation}
|
||||
onConfirm={this.handleDeleteMap}
|
||||
onClickOutside={this.handleDismissDeleteConfirmation}
|
||||
/>
|
||||
: <button
|
||||
className="btn btn-sm btn-default btn-square"
|
||||
onClick={this.handleDeleteClick}
|
||||
>
|
||||
<span className="icon trash" />
|
||||
</button>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, func, number, shape, string} = PropTypes
|
||||
|
||||
ProvidersTableRow.propTypes = {
|
||||
mapping: shape({
|
||||
id: string,
|
||||
scheme: string,
|
||||
provider: string,
|
||||
providerOrganization: string,
|
||||
organizationId: string,
|
||||
}),
|
||||
organizations: arrayOf(
|
||||
shape({
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
})
|
||||
),
|
||||
schemes: arrayOf(
|
||||
shape({
|
||||
text: string.isRequired,
|
||||
})
|
||||
),
|
||||
rowIndex: number,
|
||||
onDelete: func.isRequired,
|
||||
onUpdate: func.isRequired,
|
||||
}
|
||||
|
||||
export default ProvidersTableRow
|
|
@ -0,0 +1,116 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
|
||||
import ConfirmButtons from 'shared/components/ConfirmButtons'
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
import InputClickToEdit from 'shared/components/InputClickToEdit'
|
||||
|
||||
class ProvidersTableRowNew extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
scheme: '*',
|
||||
provider: null,
|
||||
providerOrganization: null,
|
||||
organizationId: 'default',
|
||||
}
|
||||
}
|
||||
|
||||
handleChooseScheme = scheme => {
|
||||
this.setState({scheme: scheme.text})
|
||||
}
|
||||
|
||||
handleChangeProvider = provider => {
|
||||
this.setState({provider})
|
||||
}
|
||||
|
||||
handleChangeProviderOrg = providerOrganization => {
|
||||
this.setState({providerOrganization})
|
||||
}
|
||||
|
||||
handleChooseOrganization = org => {
|
||||
this.setState({organizationId: org.id})
|
||||
}
|
||||
|
||||
handleSaveNewMapping = () => {
|
||||
const {onCreate} = this.props
|
||||
onCreate(this.state)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {scheme, provider, providerOrganization, organizationId} = this.state
|
||||
|
||||
const {organizations, onCancel, schemes, rowIndex} = this.props
|
||||
|
||||
const selectedOrg = organizations.find(o => o.id === organizationId)
|
||||
|
||||
const dropdownItems = organizations.map(role => ({
|
||||
...role,
|
||||
text: role.name,
|
||||
}))
|
||||
|
||||
const preventCreate = !provider || !providerOrganization
|
||||
|
||||
return (
|
||||
<div className="fancytable--row">
|
||||
<Dropdown
|
||||
items={schemes}
|
||||
onChoose={this.handleChooseScheme}
|
||||
selected={scheme}
|
||||
className={'fancytable--td provider--scheme'}
|
||||
/>
|
||||
<InputClickToEdit
|
||||
value={provider}
|
||||
wrapperClass="fancytable--td provider--provider"
|
||||
onUpdate={this.handleChangeProvider}
|
||||
tabIndex={rowIndex}
|
||||
placeholder="google"
|
||||
/>
|
||||
<InputClickToEdit
|
||||
value={providerOrganization}
|
||||
wrapperClass="fancytable--td provider--providerorg"
|
||||
onUpdate={this.handleChangeProviderOrg}
|
||||
tabIndex={rowIndex}
|
||||
placeholder="*"
|
||||
/>
|
||||
<div className="fancytable--td provider--arrow">
|
||||
<span />
|
||||
</div>
|
||||
<div className="fancytable--td provider--redirect deleting">
|
||||
<Dropdown
|
||||
items={dropdownItems}
|
||||
onChoose={this.handleChooseOrganization}
|
||||
selected={selectedOrg.name}
|
||||
className="dropdown-stretch"
|
||||
/>
|
||||
</div>
|
||||
<ConfirmButtons
|
||||
onCancel={onCancel}
|
||||
onConfirm={this.handleSaveNewMapping}
|
||||
isDisabled={preventCreate}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, func, number, shape, string} = PropTypes
|
||||
|
||||
ProvidersTableRowNew.propTypes = {
|
||||
organizations: arrayOf(
|
||||
shape({
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
schemes: arrayOf(
|
||||
shape({
|
||||
text: string.isRequired,
|
||||
})
|
||||
),
|
||||
rowIndex: number,
|
||||
onCreate: func.isRequired,
|
||||
onCancel: func.isRequired,
|
||||
}
|
||||
|
||||
export default ProvidersTableRowNew
|
|
@ -13,3 +13,4 @@ export const USER_ROLES = [
|
|||
]
|
||||
|
||||
export const DEFAULT_ORG_ID = 'default'
|
||||
export const DEFAULT_MAPPING_ID = 'default'
|
||||
|
|
|
@ -46,6 +46,3 @@ export const NEW_DEFAULT_DATABASE = {
|
|||
isNew: true,
|
||||
retentionPolicies: [NEW_DEFAULT_RP],
|
||||
}
|
||||
|
||||
export const PUBLIC_TOOLTIP =
|
||||
'If turned off, new users cannot<br/>authenticate unless an <strong>Admin</strong> explicitly<br/>adds them to the organization.'
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import {bindActionCreators} from 'redux'
|
||||
|
||||
import * as adminChronografActionCreators from 'src/admin/actions/chronograf'
|
||||
import {publishAutoDismissingNotification} from 'shared/dispatchers'
|
||||
|
||||
import ProvidersTable from 'src/admin/components/chronograf/ProvidersTable'
|
||||
|
||||
class ProvidersPage extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {isLoading: true}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const {
|
||||
links,
|
||||
actions: {loadOrganizationsAsync, loadMappingsAsync},
|
||||
} = this.props
|
||||
|
||||
await Promise.all([
|
||||
loadOrganizationsAsync(links.organizations),
|
||||
loadMappingsAsync(links.mappings),
|
||||
])
|
||||
|
||||
this.setState({isLoading: false})
|
||||
}
|
||||
|
||||
handleCreateMap = mapping => {
|
||||
this.props.actions.createMappingAsync(this.props.links.mappings, mapping)
|
||||
}
|
||||
|
||||
handleUpdateMap = (staleMap, updatedMap) => {
|
||||
this.props.actions.updateMappingAsync(staleMap, updatedMap)
|
||||
}
|
||||
|
||||
handleDeleteMap = mapping => {
|
||||
this.props.actions.deleteMappingAsync(mapping)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {organizations, mappings = []} = this.props
|
||||
const {isLoading} = this.state
|
||||
|
||||
return (
|
||||
<ProvidersTable
|
||||
mappings={mappings}
|
||||
organizations={organizations}
|
||||
onCreateMap={this.handleCreateMap}
|
||||
onUpdateMap={this.handleUpdateMap}
|
||||
onDeleteMap={this.handleDeleteMap}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
|
||||
ProvidersPage.propTypes = {
|
||||
links: shape({
|
||||
organizations: string.isRequired,
|
||||
}),
|
||||
organizations: arrayOf(
|
||||
shape({
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
})
|
||||
),
|
||||
mappings: arrayOf(
|
||||
shape({
|
||||
id: string,
|
||||
scheme: string,
|
||||
provider: string,
|
||||
providerOrganization: string,
|
||||
organizationId: string,
|
||||
})
|
||||
),
|
||||
actions: shape({
|
||||
loadOrganizationsAsync: func.isRequired,
|
||||
}),
|
||||
notify: func.isRequired,
|
||||
}
|
||||
|
||||
const mapStateToProps = ({
|
||||
links,
|
||||
adminChronograf: {organizations, mappings},
|
||||
}) => ({
|
||||
links,
|
||||
organizations,
|
||||
mappings,
|
||||
})
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
actions: bindActionCreators(adminChronografActionCreators, dispatch),
|
||||
notify: bindActionCreators(publishAutoDismissingNotification, dispatch),
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ProvidersPage)
|
|
@ -36,14 +36,6 @@ class OrganizationsPage extends Component {
|
|||
getMe({shouldResetMe: false})
|
||||
}
|
||||
|
||||
handleTogglePublic = organization => {
|
||||
const {actionsAdmin: {updateOrganizationAsync}} = this.props
|
||||
updateOrganizationAsync(organization, {
|
||||
...organization,
|
||||
public: !organization.public,
|
||||
})
|
||||
}
|
||||
|
||||
handleChooseDefaultRole = (organization, defaultRole) => {
|
||||
const {actionsAdmin: {updateOrganizationAsync}} = this.props
|
||||
updateOrganizationAsync(organization, {...organization, defaultRole})
|
||||
|
@ -65,7 +57,6 @@ class OrganizationsPage extends Component {
|
|||
onCreateOrg={this.handleCreateOrganization}
|
||||
onDeleteOrg={this.handleDeleteOrganization}
|
||||
onRenameOrg={this.handleRenameOrganization}
|
||||
onTogglePublic={this.handleTogglePublic}
|
||||
onChooseDefaultRole={this.handleChooseDefaultRole}
|
||||
me={me}
|
||||
/>
|
||||
|
|
|
@ -3,6 +3,7 @@ import {isSameUser} from 'shared/reducers/helpers/auth'
|
|||
const initialState = {
|
||||
users: [],
|
||||
organizations: [],
|
||||
mappings: [],
|
||||
authConfig: {
|
||||
superAdminNewUsers: false,
|
||||
},
|
||||
|
@ -58,7 +59,10 @@ const adminChronograf = (state = initialState, action) => {
|
|||
|
||||
case 'CHRONOGRAF_ADD_ORGANIZATION': {
|
||||
const {organization} = action.payload
|
||||
return {...state, organizations: [organization, ...state.organizations]}
|
||||
return {
|
||||
...state,
|
||||
organizations: [organization, ...state.organizations],
|
||||
}
|
||||
}
|
||||
|
||||
case 'CHRONOGRAF_RENAME_ORGANIZATION': {
|
||||
|
@ -94,9 +98,57 @@ const adminChronograf = (state = initialState, action) => {
|
|||
),
|
||||
}
|
||||
}
|
||||
|
||||
case 'CHRONOGRAF_LOAD_MAPPINGS': {
|
||||
const {mappings} = action.payload
|
||||
return {
|
||||
...state,
|
||||
mappings,
|
||||
}
|
||||
}
|
||||
|
||||
case 'CHRONOGRAF_UPDATE_MAPPING': {
|
||||
const {staleMapping, updatedMapping} = action.payload
|
||||
return {
|
||||
...state,
|
||||
mappings: state.mappings.map(m =>
|
||||
replaceMapping(m, staleMapping, updatedMapping)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
case 'CHRONOGRAF_ADD_MAPPING': {
|
||||
const {mapping} = action.payload
|
||||
return {
|
||||
...state,
|
||||
mappings: [...state.mappings, mapping],
|
||||
}
|
||||
}
|
||||
|
||||
case 'CHRONOGRAF_REMOVE_MAPPING': {
|
||||
const {mapping} = action.payload
|
||||
return {
|
||||
...state,
|
||||
mappings: state.mappings.filter(
|
||||
m =>
|
||||
mapping._tempID
|
||||
? m._tempID !== mapping._tempID
|
||||
: m.id !== mapping.id
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
function replaceMapping(m, staleMapping, updatedMapping) {
|
||||
if (staleMapping._tempID && m._tempID === staleMapping._tempID) {
|
||||
return {...updatedMapping}
|
||||
} else if (m.id === staleMapping.id) {
|
||||
return {...updatedMapping}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
export default adminChronograf
|
||||
|
|
|
@ -28,9 +28,10 @@ import {
|
|||
DEFAULT_VALUE_MIN,
|
||||
DEFAULT_VALUE_MAX,
|
||||
GAUGE_COLORS,
|
||||
SINGLE_STAT_TEXT,
|
||||
SINGLE_STAT_BG,
|
||||
validateColors,
|
||||
validateGaugeColors,
|
||||
validateSingleStatColors,
|
||||
getSingleStatType,
|
||||
stringifyColorValues,
|
||||
} from 'src/dashboards/constants/gaugeColors'
|
||||
|
||||
class CellEditorOverlay extends Component {
|
||||
|
@ -49,7 +50,8 @@ class CellEditorOverlay extends Component {
|
|||
source,
|
||||
}))
|
||||
)
|
||||
const colorsTypeContainsText = _.some(colors, {type: SINGLE_STAT_TEXT})
|
||||
|
||||
const singleStatType = getSingleStatType(colors)
|
||||
|
||||
this.state = {
|
||||
cellWorkingName: name,
|
||||
|
@ -58,8 +60,9 @@ class CellEditorOverlay extends Component {
|
|||
activeQueryIndex: 0,
|
||||
isDisplayOptionsTabActive: false,
|
||||
axes,
|
||||
colorSingleStatText: colorsTypeContainsText,
|
||||
colors: validateColors(colors, type, colorsTypeContainsText),
|
||||
singleStatType,
|
||||
gaugeColors: validateGaugeColors(colors),
|
||||
singleStatColors: validateSingleStatColors(colors, singleStatType),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,27 +83,21 @@ class CellEditorOverlay extends Component {
|
|||
this.overlayRef.focus()
|
||||
}
|
||||
|
||||
handleAddThreshold = () => {
|
||||
const {colors, cellWorkingType} = this.state
|
||||
const sortedColors = _.sortBy(colors, color => Number(color.value))
|
||||
handleAddGaugeThreshold = () => {
|
||||
const {gaugeColors} = this.state
|
||||
const sortedColors = _.sortBy(gaugeColors, color => color.value)
|
||||
|
||||
if (sortedColors.length <= MAX_THRESHOLDS) {
|
||||
const randomColor = _.random(0, GAUGE_COLORS.length - 1)
|
||||
|
||||
const maxValue =
|
||||
cellWorkingType === 'gauge'
|
||||
? Number(sortedColors[sortedColors.length - 1].value)
|
||||
: DEFAULT_VALUE_MAX
|
||||
const minValue =
|
||||
cellWorkingType === 'gauge'
|
||||
? Number(sortedColors[0].value)
|
||||
: DEFAULT_VALUE_MIN
|
||||
const maxValue = sortedColors[sortedColors.length - 1].value
|
||||
const minValue = sortedColors[0].value
|
||||
|
||||
const colorsValues = _.mapValues(colors, 'value')
|
||||
const colorsValues = _.mapValues(gaugeColors, 'value')
|
||||
let randomValue
|
||||
|
||||
do {
|
||||
randomValue = `${_.round(_.random(minValue, maxValue, true), 2)}`
|
||||
randomValue = _.round(_.random(minValue, maxValue, true), 2)
|
||||
} while (_.includes(colorsValues, randomValue))
|
||||
|
||||
const newThreshold = {
|
||||
|
@ -111,68 +108,134 @@ class CellEditorOverlay extends Component {
|
|||
name: GAUGE_COLORS[randomColor].name,
|
||||
}
|
||||
|
||||
this.setState({colors: [...colors, newThreshold]})
|
||||
this.setState({gaugeColors: [...gaugeColors, newThreshold]})
|
||||
}
|
||||
}
|
||||
|
||||
handleAddSingleStatThreshold = () => {
|
||||
const {singleStatColors, singleStatType} = this.state
|
||||
|
||||
const randomColor = _.random(0, GAUGE_COLORS.length - 1)
|
||||
|
||||
const maxValue = DEFAULT_VALUE_MIN
|
||||
const minValue = DEFAULT_VALUE_MAX
|
||||
|
||||
let randomValue = _.round(_.random(minValue, maxValue, true), 2)
|
||||
|
||||
if (singleStatColors.length > 0) {
|
||||
const colorsValues = _.mapValues(singleStatColors, 'value')
|
||||
do {
|
||||
randomValue = _.round(_.random(minValue, maxValue, true), 2)
|
||||
} while (_.includes(colorsValues, randomValue))
|
||||
}
|
||||
|
||||
const newThreshold = {
|
||||
type: singleStatType,
|
||||
id: uuid.v4(),
|
||||
value: randomValue,
|
||||
hex: GAUGE_COLORS[randomColor].hex,
|
||||
name: GAUGE_COLORS[randomColor].name,
|
||||
}
|
||||
|
||||
this.setState({singleStatColors: [...singleStatColors, newThreshold]})
|
||||
}
|
||||
|
||||
handleDeleteThreshold = threshold => () => {
|
||||
const {colors} = this.state
|
||||
const {cellWorkingType} = this.state
|
||||
|
||||
const newColors = colors.filter(color => color.id !== threshold.id)
|
||||
if (cellWorkingType === 'gauge') {
|
||||
const gaugeColors = this.state.gaugeColors.filter(
|
||||
color => color.id !== threshold.id
|
||||
)
|
||||
|
||||
this.setState({colors: newColors})
|
||||
this.setState({gaugeColors})
|
||||
}
|
||||
|
||||
if (cellWorkingType === 'single-stat') {
|
||||
const singleStatColors = this.state.singleStatColors.filter(
|
||||
color => color.id !== threshold.id
|
||||
)
|
||||
|
||||
this.setState({singleStatColors})
|
||||
}
|
||||
}
|
||||
|
||||
handleChooseColor = threshold => chosenColor => {
|
||||
const {colors} = this.state
|
||||
const {cellWorkingType} = this.state
|
||||
|
||||
const newColors = colors.map(
|
||||
color =>
|
||||
color.id === threshold.id
|
||||
? {...color, hex: chosenColor.hex, name: chosenColor.name}
|
||||
: color
|
||||
)
|
||||
if (cellWorkingType === 'gauge') {
|
||||
const gaugeColors = this.state.gaugeColors.map(
|
||||
color =>
|
||||
color.id === threshold.id
|
||||
? {...color, hex: chosenColor.hex, name: chosenColor.name}
|
||||
: color
|
||||
)
|
||||
|
||||
this.setState({colors: newColors})
|
||||
this.setState({gaugeColors})
|
||||
}
|
||||
|
||||
if (cellWorkingType === 'single-stat') {
|
||||
const singleStatColors = this.state.singleStatColors.map(
|
||||
color =>
|
||||
color.id === threshold.id
|
||||
? {...color, hex: chosenColor.hex, name: chosenColor.name}
|
||||
: color
|
||||
)
|
||||
|
||||
this.setState({singleStatColors})
|
||||
}
|
||||
}
|
||||
|
||||
handleUpdateColorValue = (threshold, newValue) => {
|
||||
const {colors} = this.state
|
||||
const newColors = colors.map(
|
||||
color => (color.id === threshold.id ? {...color, value: newValue} : color)
|
||||
)
|
||||
this.setState({colors: newColors})
|
||||
handleUpdateColorValue = (threshold, value) => {
|
||||
const {cellWorkingType} = this.state
|
||||
|
||||
if (cellWorkingType === 'gauge') {
|
||||
const gaugeColors = this.state.gaugeColors.map(
|
||||
color => (color.id === threshold.id ? {...color, value} : color)
|
||||
)
|
||||
|
||||
this.setState({gaugeColors})
|
||||
}
|
||||
|
||||
if (cellWorkingType === 'single-stat') {
|
||||
const singleStatColors = this.state.singleStatColors.map(
|
||||
color => (color.id === threshold.id ? {...color, value} : color)
|
||||
)
|
||||
|
||||
this.setState({singleStatColors})
|
||||
}
|
||||
}
|
||||
|
||||
handleValidateColorValue = (threshold, e) => {
|
||||
const {colors, cellWorkingType} = this.state
|
||||
const sortedColors = _.sortBy(colors, color => Number(color.value))
|
||||
const thresholdValue = Number(threshold.value)
|
||||
const targetValueNumber = Number(e.target.value)
|
||||
handleValidateColorValue = (threshold, targetValue) => {
|
||||
const {gaugeColors, singleStatColors, cellWorkingType} = this.state
|
||||
const thresholdValue = threshold.value
|
||||
let allowedToUpdate = false
|
||||
|
||||
if (cellWorkingType === 'single-stat') {
|
||||
// If type is single-stat then value only has to be unique
|
||||
return !sortedColors.some(color => color.value === e.target.value)
|
||||
const sortedColors = _.sortBy(singleStatColors, color => color.value)
|
||||
return !sortedColors.some(color => color.value === targetValue)
|
||||
}
|
||||
|
||||
const minValue = Number(sortedColors[0].value)
|
||||
const maxValue = Number(sortedColors[sortedColors.length - 1].value)
|
||||
const sortedColors = _.sortBy(gaugeColors, color => color.value)
|
||||
|
||||
const minValue = sortedColors[0].value
|
||||
const maxValue = sortedColors[sortedColors.length - 1].value
|
||||
|
||||
// If lowest value, make sure it is less than the next threshold
|
||||
if (thresholdValue === minValue) {
|
||||
const nextValue = Number(sortedColors[1].value)
|
||||
allowedToUpdate = targetValueNumber < nextValue
|
||||
const nextValue = sortedColors[1].value
|
||||
allowedToUpdate = targetValue < nextValue
|
||||
}
|
||||
// If highest value, make sure it is greater than the previous threshold
|
||||
if (thresholdValue === maxValue) {
|
||||
const previousValue = Number(sortedColors[sortedColors.length - 2].value)
|
||||
allowedToUpdate = previousValue < targetValueNumber
|
||||
const previousValue = sortedColors[sortedColors.length - 2].value
|
||||
allowedToUpdate = previousValue < targetValue
|
||||
}
|
||||
// If not min or max, make sure new value is greater than min, less than max, and unique
|
||||
if (thresholdValue !== minValue && thresholdValue !== maxValue) {
|
||||
const greaterThanMin = targetValueNumber > minValue
|
||||
const lessThanMax = targetValueNumber < maxValue
|
||||
const greaterThanMin = targetValue > minValue
|
||||
const lessThanMax = targetValue < maxValue
|
||||
|
||||
const colorsWithoutMinOrMax = sortedColors.slice(
|
||||
1,
|
||||
|
@ -180,7 +243,7 @@ class CellEditorOverlay extends Component {
|
|||
)
|
||||
|
||||
const isUnique = !colorsWithoutMinOrMax.some(
|
||||
color => color.value === e.target.value
|
||||
color => color.value === targetValue
|
||||
)
|
||||
|
||||
allowedToUpdate = greaterThanMin && lessThanMax && isUnique
|
||||
|
@ -189,16 +252,15 @@ class CellEditorOverlay extends Component {
|
|||
return allowedToUpdate
|
||||
}
|
||||
|
||||
handleToggleSingleStatText = () => {
|
||||
const {colors, colorSingleStatText} = this.state
|
||||
const formattedColors = colors.map(color => ({
|
||||
handleToggleSingleStatType = type => () => {
|
||||
const singleStatColors = this.state.singleStatColors.map(color => ({
|
||||
...color,
|
||||
type: colorSingleStatText ? SINGLE_STAT_BG : SINGLE_STAT_TEXT,
|
||||
type,
|
||||
}))
|
||||
|
||||
this.setState({
|
||||
colorSingleStatText: !colorSingleStatText,
|
||||
colors: formattedColors,
|
||||
singleStatType: type,
|
||||
singleStatColors,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -298,7 +360,8 @@ class CellEditorOverlay extends Component {
|
|||
cellWorkingType: type,
|
||||
cellWorkingName: name,
|
||||
axes,
|
||||
colors,
|
||||
gaugeColors,
|
||||
singleStatColors,
|
||||
} = this.state
|
||||
|
||||
const {cell} = this.props
|
||||
|
@ -314,6 +377,13 @@ class CellEditorOverlay extends Component {
|
|||
}
|
||||
})
|
||||
|
||||
let colors = []
|
||||
if (type === 'gauge') {
|
||||
colors = stringifyColorValues(gaugeColors)
|
||||
} else if (type === 'single-stat' || type === 'line-plus-single-stat') {
|
||||
colors = stringifyColorValues(singleStatColors)
|
||||
}
|
||||
|
||||
this.props.onSave({
|
||||
...cell,
|
||||
name,
|
||||
|
@ -324,14 +394,8 @@ class CellEditorOverlay extends Component {
|
|||
})
|
||||
}
|
||||
|
||||
handleSelectGraphType = graphType => () => {
|
||||
const {colors, colorSingleStatText} = this.state
|
||||
const validatedColors = validateColors(
|
||||
colors,
|
||||
graphType,
|
||||
colorSingleStatText
|
||||
)
|
||||
this.setState({cellWorkingType: graphType, colors: validatedColors})
|
||||
handleSelectGraphType = cellWorkingType => () => {
|
||||
this.setState({cellWorkingType})
|
||||
}
|
||||
|
||||
handleClickDisplayOptionsTab = isDisplayOptionsTabActive => () => {
|
||||
|
@ -474,13 +538,14 @@ class CellEditorOverlay extends Component {
|
|||
|
||||
const {
|
||||
axes,
|
||||
colors,
|
||||
gaugeColors,
|
||||
singleStatColors,
|
||||
activeQueryIndex,
|
||||
cellWorkingName,
|
||||
cellWorkingType,
|
||||
isDisplayOptionsTabActive,
|
||||
queriesWorkingDraft,
|
||||
colorSingleStatText,
|
||||
singleStatType,
|
||||
} = this.state
|
||||
|
||||
const queryActions = {
|
||||
|
@ -492,6 +557,9 @@ class CellEditorOverlay extends Component {
|
|||
(!!query.measurement && !!query.database && !!query.fields.length) ||
|
||||
!!query.rawText
|
||||
|
||||
const visualizationColors =
|
||||
cellWorkingType === 'gauge' ? gaugeColors : singleStatColors
|
||||
|
||||
return (
|
||||
<div
|
||||
className={OVERLAY_TECHNOLOGY}
|
||||
|
@ -508,7 +576,7 @@ class CellEditorOverlay extends Component {
|
|||
>
|
||||
<Visualization
|
||||
axes={axes}
|
||||
colors={colors}
|
||||
colors={visualizationColors}
|
||||
type={cellWorkingType}
|
||||
name={cellWorkingName}
|
||||
timeRange={timeRange}
|
||||
|
@ -533,14 +601,16 @@ class CellEditorOverlay extends Component {
|
|||
{isDisplayOptionsTabActive
|
||||
? <DisplayOptions
|
||||
axes={axes}
|
||||
colors={colors}
|
||||
gaugeColors={gaugeColors}
|
||||
singleStatColors={singleStatColors}
|
||||
onChooseColor={this.handleChooseColor}
|
||||
onValidateColorValue={this.handleValidateColorValue}
|
||||
onUpdateColorValue={this.handleUpdateColorValue}
|
||||
onAddThreshold={this.handleAddThreshold}
|
||||
onAddGaugeThreshold={this.handleAddGaugeThreshold}
|
||||
onAddSingleStatThreshold={this.handleAddSingleStatThreshold}
|
||||
onDeleteThreshold={this.handleDeleteThreshold}
|
||||
onToggleSingleStatText={this.handleToggleSingleStatText}
|
||||
colorSingleStatText={colorSingleStatText}
|
||||
onToggleSingleStatType={this.handleToggleSingleStatType}
|
||||
singleStatType={singleStatType}
|
||||
onSetBase={this.handleSetBase}
|
||||
onSetLabel={this.handleSetLabel}
|
||||
onSetScale={this.handleSetScale}
|
||||
|
|
|
@ -35,7 +35,8 @@ class DisplayOptions extends Component {
|
|||
|
||||
renderOptions = () => {
|
||||
const {
|
||||
colors,
|
||||
gaugeColors,
|
||||
singleStatColors,
|
||||
onSetBase,
|
||||
onSetScale,
|
||||
onSetLabel,
|
||||
|
@ -43,13 +44,14 @@ class DisplayOptions extends Component {
|
|||
onSetPrefixSuffix,
|
||||
onSetYAxisBoundMin,
|
||||
onSetYAxisBoundMax,
|
||||
onAddThreshold,
|
||||
onAddGaugeThreshold,
|
||||
onAddSingleStatThreshold,
|
||||
onDeleteThreshold,
|
||||
onChooseColor,
|
||||
onValidateColorValue,
|
||||
onUpdateColorValue,
|
||||
colorSingleStatText,
|
||||
onToggleSingleStatText,
|
||||
singleStatType,
|
||||
onToggleSingleStatType,
|
||||
onSetSuffix,
|
||||
} = this.props
|
||||
const {axes, axes: {y: {suffix}}} = this.state
|
||||
|
@ -58,27 +60,27 @@ class DisplayOptions extends Component {
|
|||
case 'gauge':
|
||||
return (
|
||||
<GaugeOptions
|
||||
colors={colors}
|
||||
colors={gaugeColors}
|
||||
onChooseColor={onChooseColor}
|
||||
onValidateColorValue={onValidateColorValue}
|
||||
onUpdateColorValue={onUpdateColorValue}
|
||||
onAddThreshold={onAddThreshold}
|
||||
onAddThreshold={onAddGaugeThreshold}
|
||||
onDeleteThreshold={onDeleteThreshold}
|
||||
/>
|
||||
)
|
||||
case 'single-stat':
|
||||
return (
|
||||
<SingleStatOptions
|
||||
colors={colors}
|
||||
colors={singleStatColors}
|
||||
suffix={suffix}
|
||||
onSetSuffix={onSetSuffix}
|
||||
onChooseColor={onChooseColor}
|
||||
onValidateColorValue={onValidateColorValue}
|
||||
onUpdateColorValue={onUpdateColorValue}
|
||||
onAddThreshold={onAddThreshold}
|
||||
onAddThreshold={onAddSingleStatThreshold}
|
||||
onDeleteThreshold={onDeleteThreshold}
|
||||
colorSingleStatText={colorSingleStatText}
|
||||
onToggleSingleStatText={onToggleSingleStatText}
|
||||
singleStatType={singleStatType}
|
||||
onToggleSingleStatType={onToggleSingleStatType}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
|
@ -111,10 +113,11 @@ class DisplayOptions extends Component {
|
|||
)
|
||||
}
|
||||
}
|
||||
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||
const {arrayOf, func, number, shape, string} = PropTypes
|
||||
|
||||
DisplayOptions.propTypes = {
|
||||
onAddThreshold: func.isRequired,
|
||||
onAddGaugeThreshold: func.isRequired,
|
||||
onAddSingleStatThreshold: func.isRequired,
|
||||
onDeleteThreshold: func.isRequired,
|
||||
onChooseColor: func.isRequired,
|
||||
onValidateColorValue: func.isRequired,
|
||||
|
@ -129,18 +132,27 @@ DisplayOptions.propTypes = {
|
|||
onSetLabel: func.isRequired,
|
||||
onSetBase: func.isRequired,
|
||||
axes: shape({}).isRequired,
|
||||
colors: arrayOf(
|
||||
gaugeColors: arrayOf(
|
||||
shape({
|
||||
type: string.isRequired,
|
||||
hex: string.isRequired,
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
value: string.isRequired,
|
||||
value: number.isRequired,
|
||||
}).isRequired
|
||||
),
|
||||
singleStatColors: arrayOf(
|
||||
shape({
|
||||
type: string.isRequired,
|
||||
hex: string.isRequired,
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
value: number.isRequired,
|
||||
}).isRequired
|
||||
),
|
||||
queryConfigs: arrayOf(shape()).isRequired,
|
||||
colorSingleStatText: bool.isRequired,
|
||||
onToggleSingleStatText: func.isRequired,
|
||||
singleStatType: string.isRequired,
|
||||
onToggleSingleStatType: func.isRequired,
|
||||
}
|
||||
|
||||
export default DisplayOptions
|
||||
|
|
|
@ -19,7 +19,7 @@ const GaugeOptions = ({
|
|||
}) => {
|
||||
const disableMaxColor = colors.length > MIN_THRESHOLDS
|
||||
const disableAddThreshold = colors.length > MAX_THRESHOLDS
|
||||
const sortedColors = _.sortBy(colors, color => Number(color.value))
|
||||
const sortedColors = _.sortBy(colors, color => color.value)
|
||||
|
||||
return (
|
||||
<FancyScrollbar
|
||||
|
@ -58,7 +58,7 @@ const GaugeOptions = ({
|
|||
)
|
||||
}
|
||||
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
const {arrayOf, func, number, shape, string} = PropTypes
|
||||
|
||||
GaugeOptions.propTypes = {
|
||||
colors: arrayOf(
|
||||
|
@ -67,7 +67,7 @@ GaugeOptions.propTypes = {
|
|||
hex: string.isRequired,
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
value: string.isRequired,
|
||||
value: number.isRequired,
|
||||
}).isRequired
|
||||
),
|
||||
onAddThreshold: func.isRequired,
|
||||
|
|
|
@ -3,9 +3,20 @@ import _ from 'lodash'
|
|||
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
import Threshold from 'src/dashboards/components/Threshold'
|
||||
import ColorDropdown from 'shared/components/ColorDropdown'
|
||||
|
||||
import {MAX_THRESHOLDS} from 'src/dashboards/constants/gaugeColors'
|
||||
import {
|
||||
GAUGE_COLORS,
|
||||
MAX_THRESHOLDS,
|
||||
SINGLE_STAT_BASE,
|
||||
SINGLE_STAT_TEXT,
|
||||
SINGLE_STAT_BG,
|
||||
} from 'src/dashboards/constants/gaugeColors'
|
||||
|
||||
const formatColor = color => {
|
||||
const {hex, name} = color
|
||||
return {hex, name}
|
||||
}
|
||||
const SingleStatOptions = ({
|
||||
suffix,
|
||||
onSetSuffix,
|
||||
|
@ -15,12 +26,12 @@ const SingleStatOptions = ({
|
|||
onChooseColor,
|
||||
onValidateColorValue,
|
||||
onUpdateColorValue,
|
||||
colorSingleStatText,
|
||||
onToggleSingleStatText,
|
||||
singleStatType,
|
||||
onToggleSingleStatType,
|
||||
}) => {
|
||||
const disableAddThreshold = colors.length > MAX_THRESHOLDS
|
||||
|
||||
const sortedColors = _.sortBy(colors, color => Number(color.value))
|
||||
const sortedColors = _.sortBy(colors, color => color.value)
|
||||
|
||||
return (
|
||||
<FancyScrollbar
|
||||
|
@ -37,16 +48,27 @@ const SingleStatOptions = ({
|
|||
>
|
||||
<span className="icon plus" /> Add Threshold
|
||||
</button>
|
||||
{sortedColors.map(color =>
|
||||
<Threshold
|
||||
visualizationType="single-stat"
|
||||
threshold={color}
|
||||
key={color.id}
|
||||
onChooseColor={onChooseColor}
|
||||
onValidateColorValue={onValidateColorValue}
|
||||
onUpdateColorValue={onUpdateColorValue}
|
||||
onDeleteThreshold={onDeleteThreshold}
|
||||
/>
|
||||
{sortedColors.map(
|
||||
color =>
|
||||
color.id === SINGLE_STAT_BASE
|
||||
? <div className="gauge-controls--section" key={color.id}>
|
||||
<div className="gauge-controls--label">Base Color</div>
|
||||
<ColorDropdown
|
||||
colors={GAUGE_COLORS}
|
||||
selected={formatColor(color)}
|
||||
onChoose={onChooseColor(color)}
|
||||
stretchToFit={true}
|
||||
/>
|
||||
</div>
|
||||
: <Threshold
|
||||
visualizationType="single-stat"
|
||||
threshold={color}
|
||||
key={color.id}
|
||||
onChooseColor={onChooseColor}
|
||||
onValidateColorValue={onValidateColorValue}
|
||||
onUpdateColorValue={onUpdateColorValue}
|
||||
onDeleteThreshold={onDeleteThreshold}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="single-stat-controls">
|
||||
|
@ -54,14 +76,18 @@ const SingleStatOptions = ({
|
|||
<label>Coloring</label>
|
||||
<ul className="nav nav-tablist nav-tablist-sm">
|
||||
<li
|
||||
className={colorSingleStatText ? null : 'active'}
|
||||
onClick={onToggleSingleStatText}
|
||||
className={`${singleStatType === SINGLE_STAT_BG
|
||||
? 'active'
|
||||
: ''}`}
|
||||
onClick={onToggleSingleStatType(SINGLE_STAT_BG)}
|
||||
>
|
||||
Background
|
||||
</li>
|
||||
<li
|
||||
className={colorSingleStatText ? 'active' : null}
|
||||
onClick={onToggleSingleStatText}
|
||||
className={`${singleStatType === SINGLE_STAT_TEXT
|
||||
? 'active'
|
||||
: ''}`}
|
||||
onClick={onToggleSingleStatType(SINGLE_STAT_TEXT)}
|
||||
>
|
||||
Text
|
||||
</li>
|
||||
|
@ -83,7 +109,7 @@ const SingleStatOptions = ({
|
|||
)
|
||||
}
|
||||
|
||||
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||
const {arrayOf, func, number, shape, string} = PropTypes
|
||||
|
||||
SingleStatOptions.defaultProps = {
|
||||
colors: [],
|
||||
|
@ -96,7 +122,7 @@ SingleStatOptions.propTypes = {
|
|||
hex: string.isRequired,
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
value: string.isRequired,
|
||||
value: number.isRequired,
|
||||
}).isRequired
|
||||
),
|
||||
onAddThreshold: func.isRequired,
|
||||
|
@ -104,8 +130,8 @@ SingleStatOptions.propTypes = {
|
|||
onChooseColor: func.isRequired,
|
||||
onValidateColorValue: func.isRequired,
|
||||
onUpdateColorValue: func.isRequired,
|
||||
colorSingleStatText: bool.isRequired,
|
||||
onToggleSingleStatText: func.isRequired,
|
||||
singleStatType: string.isRequired,
|
||||
onToggleSingleStatType: func.isRequired,
|
||||
onSetSuffix: func.isRequired,
|
||||
suffix: string.isRequired,
|
||||
}
|
||||
|
|
|
@ -16,14 +16,15 @@ class Threshold extends Component {
|
|||
|
||||
handleChangeWorkingValue = e => {
|
||||
const {threshold, onValidateColorValue, onUpdateColorValue} = this.props
|
||||
const targetValue = Number(e.target.value)
|
||||
|
||||
const valid = onValidateColorValue(threshold, e)
|
||||
const valid = onValidateColorValue(threshold, targetValue)
|
||||
|
||||
if (valid) {
|
||||
onUpdateColorValue(threshold, e.target.value)
|
||||
onUpdateColorValue(threshold, targetValue)
|
||||
}
|
||||
|
||||
this.setState({valid, workingValue: e.target.value})
|
||||
this.setState({valid, workingValue: targetValue})
|
||||
}
|
||||
|
||||
handleBlur = () => {
|
||||
|
@ -98,7 +99,7 @@ class Threshold extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
const {bool, func, shape, string} = PropTypes
|
||||
const {bool, func, number, shape, string} = PropTypes
|
||||
|
||||
Threshold.propTypes = {
|
||||
visualizationType: string.isRequired,
|
||||
|
@ -107,7 +108,7 @@ Threshold.propTypes = {
|
|||
hex: string.isRequired,
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
value: string.isRequired,
|
||||
value: number.isRequired,
|
||||
}).isRequired,
|
||||
disableMaxColor: bool,
|
||||
onChooseColor: func.isRequired,
|
||||
|
|
|
@ -3,6 +3,8 @@ import RefreshingGraph from 'shared/components/RefreshingGraph'
|
|||
import buildQueries from 'utils/buildQueriesForGraphs'
|
||||
import VisualizationName from 'src/dashboards/components/VisualizationName'
|
||||
|
||||
import {stringifyColorValues} from 'src/dashboards/constants/gaugeColors'
|
||||
|
||||
const DashVisualization = (
|
||||
{
|
||||
axes,
|
||||
|
@ -23,7 +25,7 @@ const DashVisualization = (
|
|||
<VisualizationName defaultName={name} onCellRename={onCellRename} />
|
||||
<div className="graph-container">
|
||||
<RefreshingGraph
|
||||
colors={colors}
|
||||
colors={stringifyColorValues(colors)}
|
||||
axes={axes}
|
||||
type={type}
|
||||
queries={buildQueries(proxy, queryConfigs, timeRange)}
|
||||
|
@ -66,8 +68,8 @@ DashVisualization.propTypes = {
|
|||
hex: string.isRequired,
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
value: string.isRequired,
|
||||
}).isRequired
|
||||
value: number.isRequired,
|
||||
})
|
||||
),
|
||||
}
|
||||
|
||||
|
|
|
@ -4,13 +4,14 @@ export const MAX_THRESHOLDS = 5
|
|||
export const MIN_THRESHOLDS = 2
|
||||
|
||||
export const COLOR_TYPE_MIN = 'min'
|
||||
export const DEFAULT_VALUE_MIN = '0'
|
||||
export const DEFAULT_VALUE_MIN = 0
|
||||
export const COLOR_TYPE_MAX = 'max'
|
||||
export const DEFAULT_VALUE_MAX = '100'
|
||||
export const DEFAULT_VALUE_MAX = 100
|
||||
export const COLOR_TYPE_THRESHOLD = 'threshold'
|
||||
|
||||
export const SINGLE_STAT_TEXT = 'text'
|
||||
export const SINGLE_STAT_BG = 'background'
|
||||
export const SINGLE_STAT_BASE = 'base'
|
||||
|
||||
export const GAUGE_COLORS = [
|
||||
{
|
||||
|
@ -81,9 +82,13 @@ export const GAUGE_COLORS = [
|
|||
hex: '#545667',
|
||||
name: 'graphite',
|
||||
},
|
||||
{
|
||||
hex: '#ffffff',
|
||||
name: 'white',
|
||||
},
|
||||
]
|
||||
|
||||
export const DEFAULT_COLORS = [
|
||||
export const DEFAULT_GAUGE_COLORS = [
|
||||
{
|
||||
type: COLOR_TYPE_MIN,
|
||||
hex: GAUGE_COLORS[11].hex,
|
||||
|
@ -100,27 +105,73 @@ export const DEFAULT_COLORS = [
|
|||
},
|
||||
]
|
||||
|
||||
export const validateColors = (colors, type, colorSingleStatText) => {
|
||||
if (type === 'single-stat') {
|
||||
// Single stat colors should all have type of 'text' or 'background'
|
||||
const colorType = colorSingleStatText ? SINGLE_STAT_TEXT : SINGLE_STAT_BG
|
||||
return colors ? colors.map(color => ({...color, type: colorType})) : null
|
||||
}
|
||||
export const DEFAULT_SINGLESTAT_COLORS = [
|
||||
{
|
||||
type: SINGLE_STAT_TEXT,
|
||||
hex: GAUGE_COLORS[11].hex,
|
||||
id: SINGLE_STAT_BASE,
|
||||
name: GAUGE_COLORS[11].name,
|
||||
value: 0,
|
||||
},
|
||||
]
|
||||
|
||||
export const validateSingleStatColors = (colors, type) => {
|
||||
if (!colors || colors.length === 0) {
|
||||
return DEFAULT_COLORS
|
||||
}
|
||||
if (type === 'gauge') {
|
||||
// Gauge colors should have a type of min, any number of thresholds, and a max
|
||||
const formatttedColors = _.sortBy(colors, color =>
|
||||
Number(color.value)
|
||||
).map(c => ({
|
||||
...c,
|
||||
type: COLOR_TYPE_THRESHOLD,
|
||||
}))
|
||||
formatttedColors[0].type = COLOR_TYPE_MIN
|
||||
formatttedColors[formatttedColors.length - 1].type = COLOR_TYPE_MAX
|
||||
return formatttedColors
|
||||
return DEFAULT_SINGLESTAT_COLORS
|
||||
}
|
||||
|
||||
return colors.length >= MIN_THRESHOLDS ? colors : DEFAULT_COLORS
|
||||
let containsBaseColor = false
|
||||
|
||||
const formattedColors = colors.map(color => {
|
||||
if (color.id === SINGLE_STAT_BASE) {
|
||||
// Check for existance of base color
|
||||
containsBaseColor = true
|
||||
return {...color, value: Number(color.value), type}
|
||||
}
|
||||
// Single stat colors should all have type of 'text' or 'background'
|
||||
return {...color, value: Number(color.value), type}
|
||||
})
|
||||
|
||||
const formattedColorsWithBase = [
|
||||
...formattedColors,
|
||||
DEFAULT_SINGLESTAT_COLORS[0],
|
||||
]
|
||||
|
||||
return containsBaseColor ? formattedColors : formattedColorsWithBase
|
||||
}
|
||||
|
||||
export const getSingleStatType = colors => {
|
||||
const type = _.get(colors, ['0', 'type'], false)
|
||||
|
||||
if (type) {
|
||||
if (_.includes([SINGLE_STAT_TEXT, SINGLE_STAT_BG], type)) {
|
||||
return type
|
||||
}
|
||||
}
|
||||
|
||||
return SINGLE_STAT_TEXT
|
||||
}
|
||||
|
||||
export const validateGaugeColors = colors => {
|
||||
if (!colors || colors.length < MIN_THRESHOLDS) {
|
||||
return DEFAULT_GAUGE_COLORS
|
||||
}
|
||||
|
||||
// Gauge colors should have a type of min, any number of thresholds, and a max
|
||||
const formattedColors = _.sortBy(colors, color =>
|
||||
Number(color.value)
|
||||
).map(color => ({
|
||||
...color,
|
||||
value: Number(color.value),
|
||||
type: COLOR_TYPE_THRESHOLD,
|
||||
}))
|
||||
|
||||
formattedColors[0].type = COLOR_TYPE_MIN
|
||||
formattedColors[formattedColors.length - 1].type = COLOR_TYPE_MAX
|
||||
|
||||
return formattedColors
|
||||
}
|
||||
|
||||
export const stringifyColorValues = colors => {
|
||||
return colors.map(color => ({...color, value: `${color.value}`}))
|
||||
}
|
||||
|
|
|
@ -5,10 +5,15 @@ import _ from 'lodash'
|
|||
import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries'
|
||||
import {resultsToCSV} from 'src/shared/parsing/resultsToCSV.js'
|
||||
import download from 'src/external/download.js'
|
||||
import {TEMPLATES} from 'src/data_explorer/constants'
|
||||
|
||||
const getCSV = (query, errorThrown) => async () => {
|
||||
try {
|
||||
const {results} = await fetchTimeSeriesAsync({source: query.host, query})
|
||||
const {results} = await fetchTimeSeriesAsync({
|
||||
source: query.host,
|
||||
query,
|
||||
tempVars: TEMPLATES,
|
||||
})
|
||||
const {flag, name, CSVString} = resultsToCSV(results)
|
||||
if (flag === 'no_data') {
|
||||
errorThrown('no data', 'There are no data to download.')
|
||||
|
|
109
ui/src/index.js
109
ui/src/index.js
|
@ -74,12 +74,19 @@ window.addEventListener('keyup', event => {
|
|||
const history = syncHistoryWithStore(browserHistory, store)
|
||||
|
||||
const Root = React.createClass({
|
||||
getInitialState() {
|
||||
return {
|
||||
ready: false,
|
||||
}
|
||||
},
|
||||
|
||||
async componentWillMount() {
|
||||
this.flushErrorsQueue()
|
||||
|
||||
try {
|
||||
await this.getLinks()
|
||||
this.checkAuth()
|
||||
await this.checkAuth()
|
||||
this.setState({ready: true})
|
||||
} catch (error) {
|
||||
dispatch(errorThrown(error))
|
||||
}
|
||||
|
@ -115,48 +122,66 @@ const Root = React.createClass({
|
|||
},
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Router history={history}>
|
||||
<Route path="/" component={UserIsAuthenticated(CheckSources)} />
|
||||
<Route path="/login" component={UserIsNotAuthenticated(Login)} />
|
||||
<Route path="/purgatory" component={UserIsAuthenticated(Purgatory)} />
|
||||
<Route
|
||||
path="/sources/new"
|
||||
component={UserIsAuthenticated(SourcePage)}
|
||||
/>
|
||||
<Route path="/sources/:sourceID" component={UserIsAuthenticated(App)}>
|
||||
<Route component={CheckSources}>
|
||||
<Route path="status" component={StatusPage} />
|
||||
<Route path="hosts" component={HostsPage} />
|
||||
<Route path="hosts/:hostID" component={HostPage} />
|
||||
<Route path="chronograf/data-explorer" component={DataExplorer} />
|
||||
<Route path="dashboards" component={DashboardsPage} />
|
||||
<Route path="dashboards/:dashboardID" component={DashboardPage} />
|
||||
<Route path="alerts" component={AlertsApp} />
|
||||
<Route path="alert-rules" component={KapacitorRulesPage} />
|
||||
<Route path="alert-rules/:ruleID" component={KapacitorRulePage} />
|
||||
<Route path="alert-rules/new" component={KapacitorRulePage} />
|
||||
<Route path="tickscript/new" component={TickscriptPage} />
|
||||
<Route path="tickscript/:ruleID" component={TickscriptPage} />
|
||||
<Route path="kapacitors/new" component={KapacitorPage} />
|
||||
<Route path="kapacitors/:id/edit" component={KapacitorPage} />
|
||||
<Route
|
||||
path="kapacitors/:id/edit:hash"
|
||||
component={KapacitorPage}
|
||||
/>
|
||||
<Route path="kapacitor-tasks" component={KapacitorTasksPage} />
|
||||
<Route path="admin-chronograf" component={AdminChronografPage} />
|
||||
<Route path="admin-influxdb" component={AdminInfluxDBPage} />
|
||||
<Route path="manage-sources" component={ManageSources} />
|
||||
<Route path="manage-sources/new" component={SourcePage} />
|
||||
<Route path="manage-sources/:id/edit" component={SourcePage} />
|
||||
return !this.state.ready // eslint-disable-line no-negated-condition
|
||||
? <div className="page-spinner" />
|
||||
: <Provider store={store}>
|
||||
<Router history={history}>
|
||||
<Route path="/" component={UserIsAuthenticated(CheckSources)} />
|
||||
<Route path="/login" component={UserIsNotAuthenticated(Login)} />
|
||||
<Route
|
||||
path="/purgatory"
|
||||
component={UserIsAuthenticated(Purgatory)}
|
||||
/>
|
||||
<Route
|
||||
path="/sources/new"
|
||||
component={UserIsAuthenticated(SourcePage)}
|
||||
/>
|
||||
<Route
|
||||
path="/sources/:sourceID"
|
||||
component={UserIsAuthenticated(App)}
|
||||
>
|
||||
<Route component={CheckSources}>
|
||||
<Route path="status" component={StatusPage} />
|
||||
<Route path="hosts" component={HostsPage} />
|
||||
<Route path="hosts/:hostID" component={HostPage} />
|
||||
<Route
|
||||
path="chronograf/data-explorer"
|
||||
component={DataExplorer}
|
||||
/>
|
||||
<Route path="dashboards" component={DashboardsPage} />
|
||||
<Route
|
||||
path="dashboards/:dashboardID"
|
||||
component={DashboardPage}
|
||||
/>
|
||||
<Route path="alerts" component={AlertsApp} />
|
||||
<Route path="alert-rules" component={KapacitorRulesPage} />
|
||||
<Route
|
||||
path="alert-rules/:ruleID"
|
||||
component={KapacitorRulePage}
|
||||
/>
|
||||
<Route path="alert-rules/new" component={KapacitorRulePage} />
|
||||
<Route path="tickscript/new" component={TickscriptPage} />
|
||||
<Route path="tickscript/:ruleID" component={TickscriptPage} />
|
||||
<Route path="kapacitors/new" component={KapacitorPage} />
|
||||
<Route path="kapacitors/:id/edit" component={KapacitorPage} />
|
||||
<Route
|
||||
path="kapacitors/:id/edit:hash"
|
||||
component={KapacitorPage}
|
||||
/>
|
||||
<Route path="kapacitor-tasks" component={KapacitorTasksPage} />
|
||||
<Route
|
||||
path="admin-chronograf"
|
||||
component={AdminChronografPage}
|
||||
/>
|
||||
<Route path="admin-influxdb" component={AdminInfluxDBPage} />
|
||||
<Route path="manage-sources" component={ManageSources} />
|
||||
<Route path="manage-sources/new" component={SourcePage} />
|
||||
<Route path="manage-sources/:id/edit" component={SourcePage} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="*" component={NotFound} />
|
||||
</Router>
|
||||
</Provider>
|
||||
)
|
||||
<Route path="*" component={NotFound} />
|
||||
</Router>
|
||||
</Provider>
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ const LogItemHTTPError = ({logItem}) =>
|
|||
</div>
|
||||
<div className="logs-table--details">
|
||||
<div className="logs-table--service error">HTTP Server</div>
|
||||
<div className="logs-table--blah">
|
||||
<div className="logs-table--columns">
|
||||
<div className="logs-table--key-values error">
|
||||
ERROR: {logItem.msg}
|
||||
</div>
|
||||
|
|
|
@ -10,7 +10,7 @@ const LogItemInfluxDBDebug = ({logItem}) =>
|
|||
</div>
|
||||
<div className="logs-table--details">
|
||||
<div className="logs-table--service debug">InfluxDB</div>
|
||||
<div className="logs-table--blah">
|
||||
<div className="logs-table--columns">
|
||||
<div className="logs-table--key-values debug">
|
||||
DEBUG: {logItem.msg}
|
||||
<br />
|
||||
|
|
|
@ -10,7 +10,7 @@ const LogItemKapacitorDebug = ({logItem}) =>
|
|||
</div>
|
||||
<div className="logs-table--details">
|
||||
<div className="logs-table--service debug">Kapacitor</div>
|
||||
<div className="logs-table--blah">
|
||||
<div className="logs-table--columns">
|
||||
<div className="logs-table--key-values debug">
|
||||
DEBUG: {logItem.msg}
|
||||
</div>
|
||||
|
|
|
@ -10,7 +10,7 @@ const LogItemKapacitorError = ({logItem}) =>
|
|||
</div>
|
||||
<div className="logs-table--details">
|
||||
<div className="logs-table--service error">Kapacitor</div>
|
||||
<div className="logs-table--blah">
|
||||
<div className="logs-table--columns">
|
||||
<div className="logs-table--key-values error">
|
||||
ERROR: {logItem.msg}
|
||||
</div>
|
||||
|
|
|
@ -1,18 +1,26 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const renderKeysAndValues = object => {
|
||||
const renderKeysAndValues = (object, name) => {
|
||||
if (!object) {
|
||||
return <span className="logs-table--empty-cell">--</span>
|
||||
}
|
||||
const objKeys = Object.keys(object)
|
||||
const objValues = Object.values(object)
|
||||
|
||||
const objElements = objKeys.map((objKey, i) =>
|
||||
<div key={i} className="logs-table--key-value">
|
||||
{objKey}: <span>{objValues[i]}</span>
|
||||
const sortedObjKeys = Object.keys(object).sort()
|
||||
|
||||
return (
|
||||
<div className="logs-table--column">
|
||||
<h1>
|
||||
{`${sortedObjKeys.length} ${name}`}
|
||||
</h1>
|
||||
<div className="logs-table--scrollbox">
|
||||
{sortedObjKeys.map(objKey =>
|
||||
<div key={objKey} className="logs-table--key-value">
|
||||
{objKey}: <span>{object[objKey]}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return objElements
|
||||
}
|
||||
const LogItemKapacitorPoint = ({logItem}) =>
|
||||
<div className="logs-table--row">
|
||||
|
@ -24,15 +32,9 @@ const LogItemKapacitorPoint = ({logItem}) =>
|
|||
</div>
|
||||
<div className="logs-table--details">
|
||||
<div className="logs-table--service">Kapacitor Point</div>
|
||||
<div className="logs-table--blah">
|
||||
<div className="logs-table--key-values">
|
||||
TAGS<br />
|
||||
{renderKeysAndValues(logItem.tag)}
|
||||
</div>
|
||||
<div className="logs-table--key-values">
|
||||
FIELDS<br />
|
||||
{renderKeysAndValues(logItem.field)}
|
||||
</div>
|
||||
<div className="logs-table--columns">
|
||||
{renderKeysAndValues(logItem.tag, 'Tags')}
|
||||
{renderKeysAndValues(logItem.field, 'Fields')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,24 +1,23 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
import InfiniteScroll from 'shared/components/InfiniteScroll'
|
||||
import LogsTableRow from 'src/kapacitor/components/LogsTableRow'
|
||||
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
||||
|
||||
const numLogsToRender = 200
|
||||
|
||||
const LogsTable = ({logs}) =>
|
||||
<div className="logs-table--container">
|
||||
<div className="logs-table">
|
||||
<div className="logs-table--header">
|
||||
<h2 className="panel-title">Logs</h2>
|
||||
</div>
|
||||
<div className="logs-table--panel fancy-scroll--kapacitor">
|
||||
{logs.length
|
||||
? <InfiniteScroll
|
||||
className="logs-table"
|
||||
itemHeight={87}
|
||||
items={logs.map((log, i) =>
|
||||
<LogsTableRow key={log.key} logItem={log} index={i} />
|
||||
)}
|
||||
/>
|
||||
: <div className="page-spinner" />}
|
||||
{`${numLogsToRender} Most Recent Logs`}
|
||||
</div>
|
||||
<FancyScrollbar
|
||||
autoHide={false}
|
||||
className="logs-table--container fancy-scroll--kapacitor"
|
||||
>
|
||||
{logs
|
||||
.slice(0, numLogsToRender)
|
||||
.map(log => <LogsTableRow key={log.key} logItem={log} />)}
|
||||
</FancyScrollbar>
|
||||
</div>
|
||||
|
||||
const {arrayOf, shape, string} = PropTypes
|
||||
|
|
|
@ -8,31 +8,31 @@ import LogItemKapacitorError from 'src/kapacitor/components/LogItemKapacitorErro
|
|||
import LogItemKapacitorDebug from 'src/kapacitor/components/LogItemKapacitorDebug'
|
||||
import LogItemInfluxDBDebug from 'src/kapacitor/components/LogItemInfluxDBDebug'
|
||||
|
||||
const LogsTableRow = ({logItem, index}) => {
|
||||
const LogsTableRow = ({logItem}) => {
|
||||
if (logItem.service === 'sessions') {
|
||||
return <LogItemSession logItem={logItem} key={index} />
|
||||
return <LogItemSession logItem={logItem} />
|
||||
}
|
||||
if (logItem.service === 'http' && logItem.msg === 'http request') {
|
||||
return <LogItemHTTP logItem={logItem} key={index} />
|
||||
return <LogItemHTTP logItem={logItem} />
|
||||
}
|
||||
if (logItem.service === 'kapacitor' && logItem.msg === 'point') {
|
||||
return <LogItemKapacitorPoint logItem={logItem} key={index} />
|
||||
return <LogItemKapacitorPoint logItem={logItem} />
|
||||
}
|
||||
if (logItem.service === 'httpd_server_errors' && logItem.lvl === 'error') {
|
||||
return <LogItemHTTPError logItem={logItem} key={index} />
|
||||
return <LogItemHTTPError logItem={logItem} />
|
||||
}
|
||||
if (logItem.service === 'kapacitor' && logItem.lvl === 'error') {
|
||||
return <LogItemKapacitorError logItem={logItem} key={index} />
|
||||
return <LogItemKapacitorError logItem={logItem} />
|
||||
}
|
||||
if (logItem.service === 'kapacitor' && logItem.lvl === 'debug') {
|
||||
return <LogItemKapacitorDebug logItem={logItem} key={index} />
|
||||
return <LogItemKapacitorDebug logItem={logItem} />
|
||||
}
|
||||
if (logItem.service === 'influxdb' && logItem.lvl === 'debug') {
|
||||
return <LogItemInfluxDBDebug logItem={logItem} key={index} />
|
||||
return <LogItemInfluxDBDebug logItem={logItem} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="logs-table--row" key={index}>
|
||||
<div className="logs-table--row">
|
||||
<div className="logs-table--divider">
|
||||
<div className={`logs-table--level ${logItem.lvl}`} />
|
||||
<div className="logs-table--timestamp">
|
||||
|
@ -43,7 +43,7 @@ const LogsTableRow = ({logItem, index}) => {
|
|||
<div className="logs-table--service">
|
||||
{logItem.service || '--'}
|
||||
</div>
|
||||
<div className="logs-table--blah">
|
||||
<div className="logs-table--columns">
|
||||
<div className="logs-table--key-values">
|
||||
{logItem.msg || '--'}
|
||||
</div>
|
||||
|
@ -53,7 +53,7 @@ const LogsTableRow = ({logItem, index}) => {
|
|||
)
|
||||
}
|
||||
|
||||
const {number, shape, string} = PropTypes
|
||||
const {shape, string} = PropTypes
|
||||
|
||||
LogsTableRow.propTypes = {
|
||||
logItem: shape({
|
||||
|
@ -62,7 +62,6 @@ LogsTableRow.propTypes = {
|
|||
lvl: string.isRequired,
|
||||
msg: string.isRequired,
|
||||
}).isRequired,
|
||||
index: number,
|
||||
}
|
||||
|
||||
export default LogsTableRow
|
||||
|
|
|
@ -34,7 +34,10 @@ const Tickscript = ({
|
|||
isNewTickscript={isNewTickscript}
|
||||
/>
|
||||
<div className="page-contents--split">
|
||||
<div className="tickscript">
|
||||
<div
|
||||
className="tickscript"
|
||||
style={areLogsVisible ? {maxWidth: '50%'} : null}
|
||||
>
|
||||
<TickscriptEditorControls
|
||||
isNewTickscript={isNewTickscript}
|
||||
onSelectDbrps={onSelectDbrps}
|
||||
|
|
|
@ -33,11 +33,12 @@ class ColorDropdown extends Component {
|
|||
|
||||
render() {
|
||||
const {visible} = this.state
|
||||
const {colors, selected, disabled} = this.props
|
||||
const {colors, selected, disabled, stretchToFit} = this.props
|
||||
|
||||
const dropdownClassNames = visible
|
||||
? 'color-dropdown open'
|
||||
: 'color-dropdown'
|
||||
const dropdownClassNames = classnames('color-dropdown', {
|
||||
open: visible,
|
||||
'color-dropdown--stretch': stretchToFit,
|
||||
})
|
||||
const toggleClassNames = classnames(
|
||||
'btn btn-sm btn-default color-dropdown--toggle',
|
||||
{active: visible, 'color-dropdown__disabled': disabled}
|
||||
|
@ -103,6 +104,7 @@ ColorDropdown.propTypes = {
|
|||
name: string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
stretchToFit: bool,
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
|
|
|
@ -132,7 +132,7 @@ class DygraphLegend extends Component {
|
|||
isFilterVisible,
|
||||
} = this.state
|
||||
|
||||
const withValues = legend.series.filter(s => s.y)
|
||||
const withValues = legend.series.filter(s => !_.isNil(s.y))
|
||||
const sorted = _.sortBy(
|
||||
withValues,
|
||||
({y, label}) => (sortType === 'numeric' ? y : label)
|
||||
|
@ -158,6 +158,7 @@ class DygraphLegend extends Component {
|
|||
<div className="sort-btn--bottom">Z</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const renderSortNum = (
|
||||
<button
|
||||
className={classnames('sort-btn btn btn-sm btn-square', {
|
||||
|
|
|
@ -2,7 +2,10 @@ import React, {PropTypes, PureComponent} from 'react'
|
|||
import lastValues from 'shared/parsing/lastValues'
|
||||
import Gauge from 'shared/components/Gauge'
|
||||
|
||||
import {DEFAULT_COLORS} from 'src/dashboards/constants/gaugeColors'
|
||||
import {
|
||||
DEFAULT_GAUGE_COLORS,
|
||||
stringifyColorValues,
|
||||
} from 'src/dashboards/constants/gaugeColors'
|
||||
import {DASHBOARD_LAYOUT_ROW_HEIGHT} from 'shared/constants'
|
||||
|
||||
class GaugeChart extends PureComponent {
|
||||
|
@ -60,7 +63,7 @@ class GaugeChart extends PureComponent {
|
|||
const {arrayOf, bool, number, shape, string} = PropTypes
|
||||
|
||||
GaugeChart.defaultProps = {
|
||||
colors: DEFAULT_COLORS,
|
||||
colors: stringifyColorValues(DEFAULT_GAUGE_COLORS),
|
||||
}
|
||||
|
||||
GaugeChart.propTypes = {
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
|
||||
class InputClickToEdit extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isEditing: null,
|
||||
value: this.props.value,
|
||||
}
|
||||
}
|
||||
|
||||
handleCancel = () => {
|
||||
this.setState({
|
||||
isEditing: false,
|
||||
value: this.props.value,
|
||||
})
|
||||
}
|
||||
|
||||
handleInputClick = () => {
|
||||
this.setState({isEditing: true})
|
||||
}
|
||||
|
||||
handleInputBlur = e => {
|
||||
const {onUpdate, value} = this.props
|
||||
|
||||
if (value !== e.target.value) {
|
||||
onUpdate(e.target.value)
|
||||
}
|
||||
|
||||
this.setState({isEditing: false, value: e.target.value})
|
||||
}
|
||||
|
||||
handleKeyDown = e => {
|
||||
if (e.key === 'Enter') {
|
||||
this.handleInputBlur(e)
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
this.handleCancel()
|
||||
}
|
||||
}
|
||||
|
||||
handleFocus = e => {
|
||||
e.target.select()
|
||||
}
|
||||
|
||||
render() {
|
||||
const {isEditing, value} = this.state
|
||||
const {wrapperClass, disabled, tabIndex, placeholder} = this.props
|
||||
|
||||
const divStyle = value ? 'input-cte' : 'input-cte__empty'
|
||||
|
||||
return disabled
|
||||
? <div className={wrapperClass}>
|
||||
<div className="input-cte__disabled">
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
: <div className={wrapperClass}>
|
||||
{isEditing
|
||||
? <input
|
||||
type="text"
|
||||
className="form-control input-sm provider--input"
|
||||
defaultValue={value}
|
||||
onBlur={this.handleInputBlur}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
autoFocus={true}
|
||||
onFocus={this.handleFocus}
|
||||
ref={r => (this.inputRef = r)}
|
||||
tabIndex={tabIndex}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
: <div
|
||||
className={divStyle}
|
||||
onClick={this.handleInputClick}
|
||||
onFocus={this.handleInputClick}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
{value || placeholder}
|
||||
<span className="icon pencil" />
|
||||
</div>}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
const {func, bool, number, string} = PropTypes
|
||||
|
||||
InputClickToEdit.propTypes = {
|
||||
wrapperClass: string.isRequired,
|
||||
value: string,
|
||||
onUpdate: func.isRequired,
|
||||
disabled: bool,
|
||||
tabIndex: number,
|
||||
placeholder: string,
|
||||
}
|
||||
|
||||
export default InputClickToEdit
|
|
@ -40,6 +40,7 @@ class LineGraph extends Component {
|
|||
axes,
|
||||
cell,
|
||||
title,
|
||||
colors,
|
||||
onZoom,
|
||||
queries,
|
||||
timeRange,
|
||||
|
@ -91,6 +92,14 @@ class LineGraph extends Component {
|
|||
top: '8px',
|
||||
}
|
||||
|
||||
let prefix
|
||||
let suffix
|
||||
|
||||
if (axes) {
|
||||
prefix = axes.y.prefix
|
||||
suffix = axes.y.suffix
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dygraph graph--hasYLabel" style={{height: '100%'}}>
|
||||
{isRefreshing ? <GraphLoadingDots /> : null}
|
||||
|
@ -114,7 +123,14 @@ class LineGraph extends Component {
|
|||
isGraphFilled={showSingleStat ? false : isGraphFilled}
|
||||
/>
|
||||
{showSingleStat
|
||||
? <SingleStat data={data} cellHeight={cellHeight} />
|
||||
? <SingleStat
|
||||
prefix={prefix}
|
||||
suffix={suffix}
|
||||
data={data}
|
||||
lineGraph={true}
|
||||
colors={colors}
|
||||
cellHeight={cellHeight}
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
|
@ -178,6 +194,15 @@ LineGraph.propTypes = {
|
|||
resizeCoords: shape(),
|
||||
queries: arrayOf(shape({}).isRequired).isRequired,
|
||||
data: arrayOf(shape({}).isRequired).isRequired,
|
||||
colors: arrayOf(
|
||||
shape({
|
||||
type: string.isRequired,
|
||||
hex: string.isRequired,
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
value: string.isRequired,
|
||||
}).isRequired
|
||||
),
|
||||
}
|
||||
|
||||
export default LineGraph
|
||||
|
|
|
@ -76,6 +76,7 @@ const RefreshingGraph = ({
|
|||
return (
|
||||
<RefreshingLineGraph
|
||||
axes={axes}
|
||||
colors={colors}
|
||||
onZoom={onZoom}
|
||||
queries={queries}
|
||||
key={manualRefresh}
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
import React, {PropTypes, PureComponent} from 'react'
|
||||
import _ from 'lodash'
|
||||
import classnames from 'classnames'
|
||||
import lastValues from 'shared/parsing/lastValues'
|
||||
|
||||
import {SMALL_CELL_HEIGHT} from 'shared/graphs/helpers'
|
||||
import {SINGLE_STAT_TEXT} from 'src/dashboards/constants/gaugeColors'
|
||||
import {isBackgroundLight} from 'shared/constants/colorOperations'
|
||||
|
||||
const darkText = '#292933'
|
||||
const lightText = '#ffffff'
|
||||
import {generateSingleStatHexs} from 'shared/constants/colorOperations'
|
||||
|
||||
class SingleStat extends PureComponent {
|
||||
render() {
|
||||
const {data, cellHeight, isFetchingInitially, colors, suffix} = this.props
|
||||
const {
|
||||
data,
|
||||
cellHeight,
|
||||
isFetchingInitially,
|
||||
colors,
|
||||
prefix,
|
||||
suffix,
|
||||
lineGraph,
|
||||
} = this.props
|
||||
|
||||
// If data for this graph is being fetched for the first time, show a graph-wide spinner.
|
||||
if (isFetchingInitially) {
|
||||
|
@ -24,37 +28,20 @@ class SingleStat extends PureComponent {
|
|||
}
|
||||
|
||||
const lastValue = lastValues(data)[1]
|
||||
|
||||
const precision = 100.0
|
||||
const roundedValue = Math.round(+lastValue * precision) / precision
|
||||
let bgColor = null
|
||||
let textColor = null
|
||||
let className = 'single-stat'
|
||||
const colorizeText = !!colors.find(color => color.type === SINGLE_STAT_TEXT)
|
||||
|
||||
if (colors && colors.length > 0) {
|
||||
className = 'single-stat single-stat--colored'
|
||||
const sortedColors = _.sortBy(colors, color => Number(color.value))
|
||||
const nearestCrossedThreshold = sortedColors
|
||||
.filter(color => lastValue > color.value)
|
||||
.pop()
|
||||
|
||||
const colorizeText = _.some(colors, {type: SINGLE_STAT_TEXT})
|
||||
|
||||
if (colorizeText) {
|
||||
textColor = nearestCrossedThreshold
|
||||
? nearestCrossedThreshold.hex
|
||||
: '#292933'
|
||||
} else {
|
||||
bgColor = nearestCrossedThreshold
|
||||
? nearestCrossedThreshold.hex
|
||||
: '#292933'
|
||||
textColor = isBackgroundLight(bgColor) ? darkText : lightText
|
||||
}
|
||||
}
|
||||
const {bgColor, textColor} = generateSingleStatHexs(
|
||||
colors,
|
||||
lineGraph,
|
||||
colorizeText,
|
||||
lastValue
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
className="single-stat"
|
||||
style={{backgroundColor: bgColor, color: textColor}}
|
||||
>
|
||||
<span
|
||||
|
@ -62,8 +49,10 @@ class SingleStat extends PureComponent {
|
|||
'single-stat--small': cellHeight === SMALL_CELL_HEIGHT,
|
||||
})}
|
||||
>
|
||||
{prefix}
|
||||
{roundedValue}
|
||||
{suffix}
|
||||
{lineGraph && <div className="single-stat--shadow" />}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
|
@ -85,7 +74,9 @@ SingleStat.propTypes = {
|
|||
value: string.isRequired,
|
||||
}).isRequired
|
||||
),
|
||||
prefix: string,
|
||||
suffix: string,
|
||||
lineGraph: bool,
|
||||
}
|
||||
|
||||
export default SingleStat
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
import _ from 'lodash'
|
||||
import {
|
||||
GAUGE_COLORS,
|
||||
SINGLE_STAT_BASE,
|
||||
} from 'src/dashboards/constants/gaugeColors'
|
||||
|
||||
const hexToRgb = hex => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
return result
|
||||
|
@ -16,9 +22,93 @@ const averageRgbValues = valuesObject => {
|
|||
|
||||
const trueNeutralGrey = 128
|
||||
|
||||
export const isBackgroundLight = backgroundColor => {
|
||||
const averageBackground = averageRgbValues(hexToRgb(backgroundColor))
|
||||
const isLight = averageBackground > trueNeutralGrey
|
||||
const getLegibleTextColor = bgColorHex => {
|
||||
const averageBackground = averageRgbValues(hexToRgb(bgColorHex))
|
||||
const isBackgroundLight = averageBackground > trueNeutralGrey
|
||||
|
||||
return isLight
|
||||
const darkText = '#292933'
|
||||
const lightText = '#ffffff'
|
||||
|
||||
return isBackgroundLight ? darkText : lightText
|
||||
}
|
||||
|
||||
const findNearestCrossedThreshold = (colors, lastValue) => {
|
||||
const sortedColors = _.sortBy(colors, color => Number(color.value))
|
||||
const nearestCrossedThreshold = sortedColors
|
||||
.filter(color => lastValue > color.value)
|
||||
.pop()
|
||||
|
||||
return nearestCrossedThreshold
|
||||
}
|
||||
|
||||
export const generateSingleStatHexs = (
|
||||
colors,
|
||||
containsLineGraph,
|
||||
colorizeText,
|
||||
lastValue
|
||||
) => {
|
||||
const defaultColoring = {bgColor: null, textColor: GAUGE_COLORS[11].hex}
|
||||
|
||||
if (!colors.length || !lastValue) {
|
||||
return defaultColoring
|
||||
}
|
||||
|
||||
// baseColor is expected in all cases
|
||||
const baseColor = colors.find(color => (color.id = SINGLE_STAT_BASE)) || {
|
||||
hex: defaultColoring.textColor,
|
||||
}
|
||||
|
||||
// If the single stat is above a line graph never have a background color
|
||||
if (containsLineGraph) {
|
||||
return baseColor
|
||||
? {bgColor: null, textColor: baseColor.hex}
|
||||
: defaultColoring
|
||||
}
|
||||
|
||||
// When there is only a base color and it's applied to the text
|
||||
if (colorizeText && colors.length === 1) {
|
||||
return baseColor
|
||||
? {bgColor: null, textColor: baseColor.hex}
|
||||
: defaultColoring
|
||||
}
|
||||
|
||||
// When there's multiple colors and they're applied to the text
|
||||
if (colorizeText && colors.length > 1) {
|
||||
const nearestCrossedThreshold = findNearestCrossedThreshold(
|
||||
colors,
|
||||
lastValue
|
||||
)
|
||||
const bgColor = null
|
||||
const textColor = nearestCrossedThreshold.hex
|
||||
|
||||
return {bgColor, textColor}
|
||||
}
|
||||
|
||||
// When there is only a base color and it's applued to the background
|
||||
if (colors.length === 1) {
|
||||
const bgColor = baseColor.hex
|
||||
const textColor = getLegibleTextColor(bgColor)
|
||||
|
||||
return {bgColor, textColor}
|
||||
}
|
||||
|
||||
// When there are multiple colors and they're applied to the background
|
||||
if (colors.length > 1) {
|
||||
const nearestCrossedThreshold = findNearestCrossedThreshold(
|
||||
colors,
|
||||
lastValue
|
||||
)
|
||||
|
||||
const bgColor = nearestCrossedThreshold
|
||||
? nearestCrossedThreshold.hex
|
||||
: baseColor.hex
|
||||
const textColor = getLegibleTextColor(bgColor)
|
||||
|
||||
return {bgColor, textColor}
|
||||
}
|
||||
|
||||
// If all else fails, use safe default
|
||||
const bgColor = null
|
||||
const textColor = baseColor.hex
|
||||
return {bgColor, textColor}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
@import 'components/custom-time-range';
|
||||
@import 'components/dygraphs';
|
||||
@import 'components/fancy-scrollbars';
|
||||
@import 'components/fancy-table';
|
||||
@import 'components/fill-query';
|
||||
@import 'components/flip-toggle';
|
||||
@import 'components/function-selector';
|
||||
|
@ -70,6 +71,7 @@
|
|||
@import 'pages/admin';
|
||||
@import 'pages/users';
|
||||
@import 'pages/tickscript-editor';
|
||||
@import 'pages/manage-providers';
|
||||
|
||||
// TODO
|
||||
@import 'unsorted';
|
||||
|
|
|
@ -242,8 +242,16 @@ button.btn.btn-primary.btn-sm.gauge-controls--add-threshold {
|
|||
|
||||
.gauge-controls--input {
|
||||
flex: 1 0 0;
|
||||
margin: 0 4px;
|
||||
margin: 0 0 0 4px;
|
||||
}
|
||||
.gauge-controls--section .color-dropdown {
|
||||
margin-left: 4px;
|
||||
}
|
||||
.gauge-controls--section .color-dropdown.color-dropdown--stretch {
|
||||
width: auto;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Cell Editor Overlay - Single-Stat Controls
|
||||
|
|
|
@ -23,10 +23,17 @@
|
|||
.CodeMirror-vscrollbar {
|
||||
@include custom-scrollbar-round($g2-kevlar,$g6-smoke);
|
||||
}
|
||||
.CodeMirror-hscrollbar {
|
||||
@include custom-scrollbar-round($g0-obsidian,$g6-smoke);
|
||||
}
|
||||
.cm-s-material .CodeMirror-gutters {
|
||||
background-color: fade-out($g4-onyx, 0.7);
|
||||
@include gradient-v($g2-kevlar, $g0-obsidian)
|
||||
border: none;
|
||||
}
|
||||
.cm-s-material .CodeMirror-gutters .CodeMirror-gutter {
|
||||
background-color: fade-out($g4-onyx, 0.75);
|
||||
height: calc(100% + 30px);
|
||||
}
|
||||
.CodeMirror-gutter.CodeMirror-linenumbers {
|
||||
width: 60px;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,10 @@ $color-dropdown--circle: 14px;
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.color-dropdown.color-dropdown--stretch {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.color-dropdown--toggle {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
|
|
@ -80,6 +80,7 @@
|
|||
height: calc(100% - 2px);
|
||||
pointer-events: none;
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.25s ease, color 0.25s ease;
|
||||
@include no-user-select();
|
||||
color: $c-laser;
|
||||
|
||||
|
@ -92,15 +93,13 @@
|
|||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
.single-stat.single-stat--colored {
|
||||
transition: background-color 0.25s ease, color 0.25s ease;
|
||||
}
|
||||
.single-stat--value {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%,-50%);
|
||||
width: calc(100% - 32px);
|
||||
width: auto;
|
||||
max-width: calc(100% - 32px);
|
||||
text-align: center;
|
||||
font-size: 54px;
|
||||
line-height: 54px;
|
||||
|
@ -115,15 +114,18 @@
|
|||
}
|
||||
}
|
||||
.single-stat--shadow {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
.single-stat--shadow:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 110%;
|
||||
width: 90%;
|
||||
height: 0;
|
||||
transform: translate(-50%,-50%);
|
||||
box-shadow: fade-out($g2-kevlar, 0.3) 0 0 50px 30px;
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
Styles for Fancy Tables
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
$fancytable--table--margin: 4px;
|
||||
|
||||
.fancytable--row,
|
||||
.fancytable--labels {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
|
||||
> div:not(.confirm-buttons) {
|
||||
margin-right: $fancytable--table--margin;
|
||||
}
|
||||
}
|
||||
.fancytable--row {
|
||||
margin-bottom: $fancytable--table--margin;
|
||||
position: relative;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.fancytable--labels {
|
||||
border-bottom: 2px solid $g5-pepper;
|
||||
margin-bottom: 10px;
|
||||
@include no-user-select();
|
||||
}
|
||||
.fancytable--th,
|
||||
.fancytable--td {
|
||||
font-weight: 500;
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.fancytable--th {
|
||||
color: $g17-whisper;
|
||||
padding: 0 11px;
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
.fancytable--td {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: $g13-mist;
|
||||
}
|
|
@ -45,9 +45,20 @@
|
|||
> span.icon {opacity: 1;}
|
||||
}
|
||||
}
|
||||
|
||||
.input-cte__disabled {
|
||||
border-color: $g4-onyx;
|
||||
background-color: $g4-onyx;
|
||||
font-style: italic;
|
||||
color: $g9-mountain;
|
||||
}
|
||||
|
||||
.input-cte__empty {
|
||||
@extend .input-cte;
|
||||
font-style: italic;
|
||||
color: $g9-mountain;
|
||||
|
||||
&:hover {
|
||||
color: $g9-mountain;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,11 +9,10 @@ $logs-row-indent: 6px;
|
|||
$logs-level-dot: 8px;
|
||||
$logs-margin: 4px;
|
||||
|
||||
.logs-table--container {
|
||||
.logs-table {
|
||||
width: 50%;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
@include gradient-v($g3-castle,$g1-raven);
|
||||
}
|
||||
.logs-table--header {
|
||||
display: flex;
|
||||
|
@ -23,28 +22,28 @@ $logs-margin: 4px;
|
|||
height: $logs-table-header-height;
|
||||
padding: 0 $logs-table-padding 0 ($logs-table-padding / 2);
|
||||
background-color: $g4-onyx;
|
||||
white-space: nowrap;
|
||||
font-size: 17px;
|
||||
@include no-user-select();
|
||||
letter-spacing: 0.015em;
|
||||
font-weight: 500;
|
||||
}
|
||||
.logs-table--panel {
|
||||
.logs-table--container {
|
||||
position: absolute !important;
|
||||
top: $logs-table-header-height;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: calc(100% - #{$logs-table-header-height}) !important;
|
||||
@include gradient-v(mix($g3-castle, $g2-kevlar),mix($g1-raven, $g0-obsidian));
|
||||
}
|
||||
|
||||
.logs-table {
|
||||
height: 100%;
|
||||
}
|
||||
.logs-table--row {
|
||||
height: 87px; // Fixed height, required for Infinite Scroll, allows for 2 tags / fields per line
|
||||
position: relative;
|
||||
padding: 8px ($logs-table-padding - 16px) 8px ($logs-table-padding / 2);
|
||||
border-bottom: 2px solid $g3-castle;
|
||||
transition: background-color 0.25s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: $g4-onyx;
|
||||
}
|
||||
&:first-child {
|
||||
&:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
@ -62,21 +61,22 @@ $logs-margin: 4px;
|
|||
&.debug {background-color: $c-comet;}
|
||||
&.info {background-color: $g6-smoke;}
|
||||
&.warn {background-color: $c-pineapple;}
|
||||
&.ok {background-color: $c-rainforest;}
|
||||
&.ok {background-color: $c-pool;}
|
||||
&.error {background-color: $c-dreamsicle;}
|
||||
}
|
||||
.logs-table--timestamp {
|
||||
font-family: $code-font;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
font-size: 13px;
|
||||
color: $g9-mountain;
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.logs-table--details {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
font-size: 13px;
|
||||
color: $g13-mist;
|
||||
color: $g11-sidewalk;
|
||||
font-weight: 600;
|
||||
padding-left: ($logs-level-dot + $logs-row-indent);
|
||||
|
||||
|
@ -85,6 +85,10 @@ $logs-margin: 4px;
|
|||
}
|
||||
|
||||
/* Logs Table Item Types */
|
||||
.logs-table--service,
|
||||
.logs-table--column h1 {
|
||||
margin-top: 2px;
|
||||
}
|
||||
.logs-table--session {
|
||||
text-transform: capitalize;
|
||||
font-style: italic;
|
||||
|
@ -92,16 +96,33 @@ $logs-margin: 4px;
|
|||
.logs-table--service {
|
||||
width: 140px;
|
||||
}
|
||||
.logs-table--blah {
|
||||
.logs-table--columns {
|
||||
display: flex;
|
||||
flex: 1 0 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.logs-table--key-values {
|
||||
.logs-table--column {
|
||||
color: $g11-sidewalk;
|
||||
flex: 1 0 50%;
|
||||
}
|
||||
.logs-table--column h1 {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
letter-spacing: normal;
|
||||
line-height: 1.42857143em;
|
||||
text-transform: uppercase;
|
||||
color: $g16-pearl;
|
||||
}
|
||||
.logs-table--key-value {
|
||||
white-space: nowrap;
|
||||
span {
|
||||
color: $c-rainforest;
|
||||
}
|
||||
}
|
||||
.logs-table--key-value span {
|
||||
color: $c-pool;
|
||||
.logs-table--scrollbox {
|
||||
width: 100%;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
@include custom-scrollbar-round($g0-obsidian,$c-rainforest);
|
||||
}
|
||||
|
|
|
@ -4,91 +4,45 @@
|
|||
Is not actually a table
|
||||
*/
|
||||
|
||||
.orgs-table--org {
|
||||
$orgs-table--active-width: 102px;
|
||||
$orgs-table--public-width: 90px;
|
||||
$orgs-table--default-role-width: 130px;
|
||||
$orgs-table--delete-width: 30px;
|
||||
|
||||
.orgs-table--name {
|
||||
flex: 1 0 0;
|
||||
}
|
||||
.orgs-table--public {
|
||||
width: $orgs-table--public-width;
|
||||
text-align: center;
|
||||
}
|
||||
.orgs-table--default-role {
|
||||
width: $orgs-table--default-role-width;
|
||||
}
|
||||
.orgs-table--delete {
|
||||
width: $orgs-table--delete-width;
|
||||
}
|
||||
.orgs-table--active {
|
||||
width: $orgs-table--active-width;
|
||||
justify-content: center;
|
||||
@include no-user-select();
|
||||
|
||||
.btn {width: 100%;}
|
||||
}
|
||||
.orgs-table--default-role.deleting {
|
||||
width: (
|
||||
$orgs-table--default-role-width - $fancytable--table--margin -
|
||||
$orgs-table--delete-width
|
||||
);
|
||||
}
|
||||
.orgs-table--public-toggle {
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
position: relative;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.orgs-table--id {
|
||||
padding: 0 11px;
|
||||
width: 60px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
font-size: 13px;
|
||||
color: $g13-mist;
|
||||
font-weight: 500;
|
||||
}
|
||||
.orgs-table--active {
|
||||
padding: 0 4px 0 0;
|
||||
width: 102px;
|
||||
height: 30px;
|
||||
|
||||
button.btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.orgs-table--name,
|
||||
.orgs-table--name-disabled,
|
||||
input[type="text"].form-control.orgs-table--input {
|
||||
flex: 1 0 0;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.orgs-table--name,
|
||||
.orgs-table--name-disabled {
|
||||
@include no-user-select();
|
||||
padding: 0 11px;
|
||||
border-radius: 4px;
|
||||
height: 30px;
|
||||
line-height: 28px;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
}
|
||||
.orgs-table--name {
|
||||
border-color: $g2-kevlar;
|
||||
background-color: $g2-kevlar;
|
||||
color: $g13-mist;
|
||||
position: relative;
|
||||
transition: color 0.4s ease, background-color 0.4s ease,
|
||||
border-color 0.4s ease;
|
||||
|
||||
> span.icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 11px;
|
||||
transform: translateY(-50%);
|
||||
color: $g8-storm;
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $g20-white;
|
||||
background-color: $g5-pepper;
|
||||
border-color: $g5-pepper;
|
||||
cursor: text;
|
||||
|
||||
> span.icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.orgs-table--public {
|
||||
height: 30px;
|
||||
margin-right: 4px;
|
||||
text-align: center;
|
||||
width: 88px;
|
||||
justify-content: center;
|
||||
background-color: $g4-onyx;
|
||||
border-radius: 4px;
|
||||
line-height: 30px;
|
||||
position: relative;
|
||||
|
||||
> .slide-toggle {
|
||||
|
@ -103,69 +57,3 @@ input[type="text"].form-control.orgs-table--input {
|
|||
@include no-user-select();
|
||||
}
|
||||
}
|
||||
|
||||
.orgs-table--default-role,
|
||||
.orgs-table--default-role-disabled {
|
||||
width: 100px;
|
||||
height: 30px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.orgs-table--default-role.editing {
|
||||
width: 96px;
|
||||
}
|
||||
.orgs-table--default-role-disabled {
|
||||
background-color: $g4-onyx;
|
||||
font-style: italic;
|
||||
color: $g9-mountain;
|
||||
padding: 0 11px;
|
||||
line-height: 30px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
@include no-user-select();
|
||||
}
|
||||
.orgs-table--delete {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
/* Table Headers */
|
||||
.orgs-table--org-labels {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 2px solid $g5-pepper;
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
@include no-user-select();
|
||||
|
||||
> .orgs-table--name,
|
||||
> .orgs-table--name:hover,
|
||||
> .orgs-table--public,
|
||||
> .orgs-table--active {
|
||||
transition: none;
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
> .orgs-table--id,
|
||||
> .orgs-table--name,
|
||||
> .orgs-table--name:hover,
|
||||
> .orgs-table--default-role,
|
||||
> .orgs-table--public,
|
||||
> .orgs-table--active {
|
||||
color: $g17-whisper;
|
||||
font-weight: 500;
|
||||
}
|
||||
> .orgs-table--default-role,
|
||||
> .orgs-table--public,
|
||||
> .orgs-table--active {
|
||||
line-height: 30px;
|
||||
font-size: 13px;
|
||||
padding: 0 11px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Config table beneath organizations table */
|
||||
.panel .panel-body table.table.superadmin-config {
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
|
|
@ -71,16 +71,18 @@ $scrollbar-offset: 3px;
|
|||
width: $scrollbar-width;
|
||||
border-top-right-radius: $radius;
|
||||
border-top-left-radius: $radius;
|
||||
border-bottom-right-radius: $radius;
|
||||
border-bottom-left-radius: $radius;
|
||||
border-bottom-right-radius: $radius;
|
||||
|
||||
&-button {
|
||||
background-color: $trackColor;
|
||||
}
|
||||
&-track {
|
||||
background-color: $trackColor;
|
||||
border-top-right-radius: $radius;
|
||||
border-bottom-right-radius: $radius;
|
||||
border-top-right-radius: ($scrollbar-width / 2);
|
||||
border-top-left-radius: ($scrollbar-width / 2);
|
||||
border-bottom-left-radius: ($scrollbar-width / 2);
|
||||
border-bottom-right-radius: ($scrollbar-width / 2);
|
||||
}
|
||||
&-track-piece {
|
||||
background-color: $trackColor;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue