diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a5f8b0d0d0..3b8239bd437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,48 @@ -# 3.0.0 (unrelased master branch) +# 3.0.0-beta5 (2016-04-15) + +### Bug fixes +* **grafana-cli**: Fixed issue grafana-cli tool, did not detect the right plugin dir, fixes [#4723](https://github.com/grafana/grafana/issues/4723) +* **Graph**: Fixed issue with light theme text color issue in tooltip, fixes [#4702](https://github.com/grafana/grafana/issues/4702) +* **Snapshot**: Fixed issue with empty snapshots, fixes [#4706](https://github.com/grafana/grafana/issues/4706) + +# 3.0.0-beta4 (2016-04-13) + +### Bug fixes +* **Home dashboard**: Fixed issue with permission denied error on home dashboard, fixes [#4686](https://github.com/grafana/grafana/issues/4686) +* **Templating**: Fixed issue templating variables that use regex extraction, fixes [#4672](https://github.com/grafana/grafana/issues/4672) + +# 3.0.0-beta3 (2016-04-12) + +### Enhancements +* **InfluxDB**: Changed multi query encoding to work with InfluxDB 0.11 & 0.12, closes [#4533](https://github.com/grafana/grafana/issues/4533) +* **Timepicker**: Add arrows and shortcuts for moving back and forth in current dashboard, closes [#119](https://github.com/grafana/grafana/issues/119) + +### Bug fixes +* **Postgres**: Fixed page render crash when using postgres, fixes [#4558](https://github.com/grafana/grafana/issues/4558) +* **Table panel**: Fixed table panel bug when trying to show annotations in table panel, fixes [#4563](https://github.com/grafana/grafana/issues/4563) +* **App Config**: Fixed app config issue showing content of other app config, fixes [#4575](https://github.com/grafana/grafana/issues/4575) +* **Graph Panel**: Fixed legend option max not updating, fixes [#4601](https://github.com/grafana/grafana/issues/4601) +* **Graph Panel**: Fixed issue where newly added graph panels shared same axes config, fixes [#4582](https://github.com/grafana/grafana/issues/4582) +* **Graph Panel**: Fixed issue with axis labels overlapping Y-axis, fixes [#4626](https://github.com/grafana/grafana/issues/4626) +* **InfluxDB**: Fixed issue with templating query containing template variable, fixes [#4602](https://github.com/grafana/grafana/issues/4602) +* **Graph Panel**: Fixed issue with hiding series and stacking, fixes [#4557](https://github.com/grafana/grafana/issues/4557) +* **Graph Panel**: Fixed issue with legend height in table mode with few series, affected iframe embedding as well, fixes [#4640](https://github.com/grafana/grafana/issues/4640) + +# 3.0.0-beta2 (2016-04-04) + +### New Features (introduces since 3.0-beta1) +* **Preferences**: Set home dashboard on user and org level, closes [#1678](https://github.com/grafana/grafana/issues/1678) +* **Preferences**: Set timezone on user and org level, closes [#3214](https://github.com/grafana/grafana/issues/3214), [#1200](https://github.com/grafana/grafana/issues/1200) +* **Preferences**: Set theme on user and org level, closes [#3214](https://github.com/grafana/grafana/issues/3214), [#1917](https://github.com/grafana/grafana/issues/1917) + +### Bug fixes +* **Dashboard**: Fixed dashboard panel layout for mobile devices, fixes [#4529](https://github.com/grafana/grafana/issues/4529) +* **Table Panel**: Fixed issue with table panel sort, fixes [#4532](https://github.com/grafana/grafana/issues/4532) +* **Page Load Crash**: A Datasource with null jsonData would make Grafana fail to load page, fixes [#4536](https://github.com/grafana/grafana/issues/4536) +* **Metrics tab**: Fix for missing datasource name in datasource selector, fixes [#4541](https://github.com/grafana/grafana/issues/4540) +* **Graph**: Fix legend in table mode with series on right-y axis, fixes [#4551](https://github.com/grafana/grafana/issues/4551), [#1145](https://github.com/grafana/grafana/issues/1145) + +# 3.0.0-beta1 (2016-03-31) ### New Features * **Playlists**: Playlists can now be persisted and started from urls, closes [#3655](https://github.com/grafana/grafana/issues/3655) @@ -14,6 +58,7 @@ * **Plugin API**: Both datasource and panel plugin api (and plugin.json schema) have been updated, requiring an update to plugins. See [plugin api](https://github.com/grafana/grafana/blob/master/public/app/plugins/plugin_api.md) for more info. * **InfluxDB 0.8.x** The data source for the old version of influxdb (0.8.x) is no longer included in default builds, but can easily be installed via improved plugin system, closes [#3523](https://github.com/grafana/grafana/issues/3523) * **KairosDB** The data source is no longer included in default builds, but can easily be installed via improved plugin system, closes [#3524](https://github.com/grafana/grafana/issues/3524) +* **Templating**: Templating value formats (glob/regex/pipe etc) are now handled automatically and not specified by the user, this makes variable values possible to reuse in many contexts. It can in some edge cases break existing dashboards that have template variables that do not reload on dashboard load. To fix any issue just go into template variable options and update the variable (so it's values are reloaded.). ### Enhancements * **LDAP**: Support for nested LDAP Groups, closes [#4401](https://github.com/grafana/grafana/issues/4401), [#3808](https://github.com/grafana/grafana/issues/3808) diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index b5cb1a460df..76a9bbf0d18 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1,6 +1,7 @@ { "ImportPath": "github.com/grafana/grafana", "GoVersion": "go1.5.1", + "GodepVersion": "v60", "Packages": [ "./pkg/..." ], @@ -159,8 +160,8 @@ }, { "ImportPath": "github.com/go-ldap/ldap", - "Comment": "v1-19-g83e6542", - "Rev": "83e65426fd1c06626e88aa8a085e5bfed0208e29" + "Comment": "v2.2.1", + "Rev": "07a7330929b9ee80495c88a4439657d89c7dbd87" }, { "ImportPath": "github.com/go-macaron/binding", @@ -209,6 +210,10 @@ "Comment": "v0.4.4-44-gf561133", "Rev": "f56113384f2c63dfe4cd8e768e349f1c35122b58" }, + { + "ImportPath": "github.com/gorilla/websocket", + "Rev": "c45a635370221f34fea2d5163fd156fcb4e38e8a" + }, { "ImportPath": "github.com/gosimple/slug", "Rev": "8d258463b4459f161f51d6a357edacd3eef9d663" diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/.travis.yml b/Godeps/_workspace/src/github.com/go-ldap/ldap/.travis.yml index f90ee667c09..3a5402596d0 100644 --- a/Godeps/_workspace/src/github.com/go-ldap/ldap/.travis.yml +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/.travis.yml @@ -2,10 +2,13 @@ language: go go: - 1.2 - 1.3 + - 1.4 + - 1.5 - tip +go_import_path: gopkg.in/ldap.v2 install: - go get gopkg.in/asn1-ber.v1 - - go get gopkg.in/ldap.v1 + - go get gopkg.in/ldap.v2 - go get code.google.com/p/go.tools/cmd/cover || go get golang.org/x/tools/cmd/cover - go build -v ./... script: diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/README.md b/Godeps/_workspace/src/github.com/go-ldap/ldap/README.md index c940520461e..f49b4d6a1b3 100644 --- a/Godeps/_workspace/src/github.com/go-ldap/ldap/README.md +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/README.md @@ -1,8 +1,20 @@ -[![GoDoc](https://godoc.org/gopkg.in/ldap.v1?status.svg)](https://godoc.org/gopkg.in/ldap.v1) [![Build Status](https://travis-ci.org/go-ldap/ldap.svg)](https://travis-ci.org/go-ldap/ldap) +[![GoDoc](https://godoc.org/gopkg.in/ldap.v2?status.svg)](https://godoc.org/gopkg.in/ldap.v2) +[![Build Status](https://travis-ci.org/go-ldap/ldap.svg)](https://travis-ci.org/go-ldap/ldap) # Basic LDAP v3 functionality for the GO programming language. -## Required Librarys: +## Install + +For the latest version use: + + go get gopkg.in/ldap.v2 + +Import the latest version with: + + import "gopkg.in/ldap.v2" + + +## Required Libraries: - gopkg.in/asn1-ber.v1 @@ -14,6 +26,9 @@ - Compiling string filters to LDAP filters - Paging Search Results - Modify Requests / Responses + - Add Requests / Responses + - Delete Requests / Responses + - Better Unicode support ## Examples: @@ -26,23 +41,15 @@ ## TODO: - - Add Requests / Responses - - Delete Requests / Responses - - Modify DN Requests / Responses - - Compare Requests / Responses - - Implement Tests / Benchmarks + - [x] Add Requests / Responses + - [x] Delete Requests / Responses + - [x] Modify DN Requests / Responses + - [ ] Compare Requests / Responses + - [ ] Implement Tests / Benchmarks + + --- -This feature is disabled at the moment, because in some cases the "Search Request Done" packet will be handled before the last "Search Request Entry": - - - Mulitple internal goroutines to handle network traffic - Makes library goroutine safe - Can perform multiple search requests at the same time and return - the results to the proper goroutine. All requests are blocking requests, - so the goroutine does not need special handling - ---- - The Go gopher was designed by Renee French. (http://reneefrench.blogspot.com/) The design is licensed under the Creative Commons 3.0 Attributions license. Read this article for more details: http://blog.golang.org/gopher diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/add.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/add.go new file mode 100644 index 00000000000..643ce5ffe4c --- /dev/null +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/add.go @@ -0,0 +1,104 @@ +// +// https://tools.ietf.org/html/rfc4511 +// +// AddRequest ::= [APPLICATION 8] SEQUENCE { +// entry LDAPDN, +// attributes AttributeList } +// +// AttributeList ::= SEQUENCE OF attribute Attribute + +package ldap + +import ( + "errors" + "log" + + "gopkg.in/asn1-ber.v1" +) + +type Attribute struct { + attrType string + attrVals []string +} + +func (a *Attribute) encode() *ber.Packet { + seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attribute") + seq.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, a.attrType, "Type")) + set := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSet, nil, "AttributeValue") + for _, value := range a.attrVals { + set.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, value, "Vals")) + } + seq.AppendChild(set) + return seq +} + +type AddRequest struct { + dn string + attributes []Attribute +} + +func (a AddRequest) encode() *ber.Packet { + request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationAddRequest, nil, "Add Request") + request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, a.dn, "DN")) + attributes := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attributes") + for _, attribute := range a.attributes { + attributes.AppendChild(attribute.encode()) + } + request.AppendChild(attributes) + return request +} + +func (a *AddRequest) Attribute(attrType string, attrVals []string) { + a.attributes = append(a.attributes, Attribute{attrType: attrType, attrVals: attrVals}) +} + +func NewAddRequest(dn string) *AddRequest { + return &AddRequest{ + dn: dn, + } + +} + +func (l *Conn) Add(addRequest *AddRequest) error { + messageID := l.nextMessageID() + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") + packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "MessageID")) + packet.AppendChild(addRequest.encode()) + + l.Debug.PrintPacket(packet) + + channel, err := l.sendMessage(packet) + if err != nil { + return err + } + if channel == nil { + return NewError(ErrorNetwork, errors.New("ldap: could not send message")) + } + defer l.finishMessage(messageID) + + l.Debug.Printf("%d: waiting for response", messageID) + packet = <-channel + l.Debug.Printf("%d: got response %p", messageID, packet) + if packet == nil { + return NewError(ErrorNetwork, errors.New("ldap: could not retrieve message")) + } + + if l.Debug { + if err := addLDAPDescriptions(packet); err != nil { + return err + } + ber.PrintPacket(packet) + } + + if packet.Children[1].Tag == ApplicationAddResponse { + resultCode, resultDescription := getLDAPResultCode(packet) + if resultCode != 0 { + return NewError(resultCode, errors.New(resultDescription)) + } + } else { + log.Printf("Unexpected Response: %d", packet.Children[1].Tag) + } + + l.Debug.Printf("%d: returning", messageID) + return nil +} diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/client.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/client.go new file mode 100644 index 00000000000..d3401f9e61e --- /dev/null +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/client.go @@ -0,0 +1,23 @@ +package ldap + +import "crypto/tls" + +// Client knows how to interact with an LDAP server +type Client interface { + Start() + StartTLS(config *tls.Config) error + Close() + + Bind(username, password string) error + SimpleBind(simpleBindRequest *SimpleBindRequest) (*SimpleBindResult, error) + + Add(addRequest *AddRequest) error + Del(delRequest *DelRequest) error + Modify(modifyRequest *ModifyRequest) error + + Compare(dn, attribute, value string) (bool, error) + PasswordModify(passwordModifyRequest *PasswordModifyRequest) (*PasswordModifyResult, error) + + Search(searchRequest *SearchRequest) (*SearchResult, error) + SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error) +} diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/conn.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/conn.go index c51e1afe87d..2f16443f6e4 100644 --- a/Godeps/_workspace/src/github.com/go-ldap/ldap/conn.go +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/conn.go @@ -8,11 +8,12 @@ import ( "crypto/tls" "errors" "fmt" - "gopkg.in/asn1-ber.v1" "log" "net" "sync" "time" + + "gopkg.in/asn1-ber.v1" ) const ( @@ -53,6 +54,8 @@ type Conn struct { messageMutex sync.Mutex } +var _ Client = &Conn{} + // DefaultTimeout is a package-level variable that sets the timeout value // used for the Dial and DialTLS methods. // @@ -176,7 +179,7 @@ func (l *Conn) StartTLS(config *tls.Config) error { ber.PrintPacket(packet) } - if packet.Children[1].Children[0].Value.(int64) == 0 { + if resultCode, message := getLDAPResultCode(packet); resultCode == LDAPResultSuccess { conn := tls.Client(l.conn, config) if err := conn.Handshake(); err != nil { @@ -186,6 +189,8 @@ func (l *Conn) StartTLS(config *tls.Config) error { l.isTLS = true l.conn = conn + } else { + return NewError(resultCode, fmt.Errorf("ldap: cannot StartTLS (%s)", message)) } go l.reader() diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/control.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/control.go index 562fbe43090..4d82980933e 100644 --- a/Godeps/_workspace/src/github.com/go-ldap/ldap/control.go +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/control.go @@ -16,11 +16,13 @@ const ( ControlTypeBeheraPasswordPolicy = "1.3.6.1.4.1.42.2.27.8.5.1" ControlTypeVChuPasswordMustChange = "2.16.840.1.113730.3.4.4" ControlTypeVChuPasswordWarning = "2.16.840.1.113730.3.4.5" + ControlTypeManageDsaIT = "2.16.840.1.113730.3.4.2" ) var ControlTypeMap = map[string]string{ ControlTypePaging: "Paging", ControlTypeBeheraPasswordPolicy: "Password Policy - Behera Draft", + ControlTypeManageDsaIT: "Manage DSA IT", } type Control interface { @@ -165,6 +167,36 @@ func (c *ControlVChuPasswordWarning) String() string { c.Expire) } +type ControlManageDsaIT struct { + Criticality bool +} + +func (c *ControlManageDsaIT) GetControlType() string { + return ControlTypeManageDsaIT +} + +func (c *ControlManageDsaIT) Encode() *ber.Packet { + //FIXME + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") + packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeManageDsaIT, "Control Type ("+ControlTypeMap[ControlTypeManageDsaIT]+")")) + if c.Criticality { + packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.Criticality, "Criticality")) + } + return packet +} + +func (c *ControlManageDsaIT) String() string { + return fmt.Sprintf( + "Control Type: %s (%q) Criticality: %t", + ControlTypeMap[ControlTypeManageDsaIT], + ControlTypeManageDsaIT, + c.Criticality) +} + +func NewControlManageDsaIT(Criticality bool) *ControlManageDsaIT { + return &ControlManageDsaIT{Criticality: Criticality} +} + func FindControl(controls []Control, controlType string) Control { for _, c := range controls { if c.GetControlType() == controlType { diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/del.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/del.go new file mode 100644 index 00000000000..2f0eae1cd29 --- /dev/null +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/del.go @@ -0,0 +1,79 @@ +// +// https://tools.ietf.org/html/rfc4511 +// +// DelRequest ::= [APPLICATION 10] LDAPDN + +package ldap + +import ( + "errors" + "log" + + "gopkg.in/asn1-ber.v1" +) + +type DelRequest struct { + DN string + Controls []Control +} + +func (d DelRequest) encode() *ber.Packet { + request := ber.Encode(ber.ClassApplication, ber.TypePrimitive, ApplicationDelRequest, d.DN, "Del Request") + request.Data.Write([]byte(d.DN)) + return request +} + +func NewDelRequest(DN string, + Controls []Control) *DelRequest { + return &DelRequest{ + DN: DN, + Controls: Controls, + } +} + +func (l *Conn) Del(delRequest *DelRequest) error { + messageID := l.nextMessageID() + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") + packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "MessageID")) + packet.AppendChild(delRequest.encode()) + if delRequest.Controls != nil { + packet.AppendChild(encodeControls(delRequest.Controls)) + } + + l.Debug.PrintPacket(packet) + + channel, err := l.sendMessage(packet) + if err != nil { + return err + } + if channel == nil { + return NewError(ErrorNetwork, errors.New("ldap: could not send message")) + } + defer l.finishMessage(messageID) + + l.Debug.Printf("%d: waiting for response", messageID) + packet = <-channel + l.Debug.Printf("%d: got response %p", messageID, packet) + if packet == nil { + return NewError(ErrorNetwork, errors.New("ldap: could not retrieve message")) + } + + if l.Debug { + if err := addLDAPDescriptions(packet); err != nil { + return err + } + ber.PrintPacket(packet) + } + + if packet.Children[1].Tag == ApplicationDelResponse { + resultCode, resultDescription := getLDAPResultCode(packet) + if resultCode != 0 { + return NewError(resultCode, errors.New(resultDescription)) + } + } else { + log.Printf("Unexpected Response: %d", packet.Children[1].Tag) + } + + l.Debug.Printf("%d: returning", messageID) + return nil +} diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/dn.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/dn.go index 31d52db4b23..5d83c5e9ab9 100644 --- a/Godeps/_workspace/src/github.com/go-ldap/ldap/dn.go +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/dn.go @@ -47,17 +47,17 @@ package ldap import ( "bytes" + enchex "encoding/hex" "errors" "fmt" "strings" - enchex "encoding/hex" ber "gopkg.in/asn1-ber.v1" ) type AttributeTypeAndValue struct { - Type string - Value string + Type string + Value string } type RelativeDN struct { @@ -71,7 +71,7 @@ type DN struct { func ParseDN(str string) (*DN, error) { dn := new(DN) dn.RDNs = make([]*RelativeDN, 0) - rdn := new (RelativeDN) + rdn := new(RelativeDN) rdn.Attributes = make([]*AttributeTypeAndValue, 0) buffer := bytes.Buffer{} attribute := new(AttributeTypeAndValue) @@ -115,7 +115,7 @@ func ParseDN(str string) (*DN, error) { index := strings.IndexAny(str[i:], ",+") data := str if index > 0 { - data = str[i:i+index] + data = str[i : i+index] } else { data = str[i:] } @@ -126,7 +126,7 @@ func ParseDN(str string) (*DN, error) { } packet := ber.DecodePacket(raw_ber) buffer.WriteString(packet.Data.String()) - i += len(data)-1 + i += len(data) - 1 } } else if char == ',' || char == '+' { // We're done with this RDN or value, push it diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/dn_test.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/dn_test.go index 6740e1819c7..39817c42741 100644 --- a/Godeps/_workspace/src/github.com/go-ldap/ldap/dn_test.go +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/dn_test.go @@ -1,38 +1,40 @@ -package ldap +package ldap_test import ( "reflect" "testing" + + "gopkg.in/ldap.v2" ) func TestSuccessfulDNParsing(t *testing.T) { - testcases := map[string]DN { - "": DN{[]*RelativeDN{}}, - "cn=Jim\\2C \\22Hasse Hö\\22 Hansson!,dc=dummy,dc=com": DN{[]*RelativeDN{ - &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"cn", "Jim, \"Hasse Hö\" Hansson!"},}}, - &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"dc", "dummy"},}}, - &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"dc", "com"}, }},}}, - "UID=jsmith,DC=example,DC=net": DN{[]*RelativeDN{ - &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"UID", "jsmith"},}}, - &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"DC", "example"},}}, - &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"DC", "net"}, }},}}, - "OU=Sales+CN=J. Smith,DC=example,DC=net": DN{[]*RelativeDN{ - &RelativeDN{[]*AttributeTypeAndValue{ - &AttributeTypeAndValue{"OU", "Sales"}, - &AttributeTypeAndValue{"CN", "J. Smith"},}}, - &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"DC", "example"},}}, - &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"DC", "net"}, }},}}, - "1.3.6.1.4.1.1466.0=#04024869": DN{[]*RelativeDN{ - &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"1.3.6.1.4.1.1466.0", "Hi"},}},}}, - "1.3.6.1.4.1.1466.0=#04024869,DC=net": DN{[]*RelativeDN{ - &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"1.3.6.1.4.1.1466.0", "Hi"},}}, - &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"DC", "net"}, }},}}, - "CN=Lu\\C4\\8Di\\C4\\87": DN{[]*RelativeDN{ - &RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"CN", "Lučić"},}},}}, + testcases := map[string]ldap.DN{ + "": ldap.DN{[]*ldap.RelativeDN{}}, + "cn=Jim\\2C \\22Hasse Hö\\22 Hansson!,dc=dummy,dc=com": ldap.DN{[]*ldap.RelativeDN{ + &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"cn", "Jim, \"Hasse Hö\" Hansson!"}}}, + &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"dc", "dummy"}}}, + &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"dc", "com"}}}}}, + "UID=jsmith,DC=example,DC=net": ldap.DN{[]*ldap.RelativeDN{ + &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"UID", "jsmith"}}}, + &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"DC", "example"}}}, + &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"DC", "net"}}}}}, + "OU=Sales+CN=J. Smith,DC=example,DC=net": ldap.DN{[]*ldap.RelativeDN{ + &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{ + &ldap.AttributeTypeAndValue{"OU", "Sales"}, + &ldap.AttributeTypeAndValue{"CN", "J. Smith"}}}, + &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"DC", "example"}}}, + &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"DC", "net"}}}}}, + "1.3.6.1.4.1.1466.0=#04024869": ldap.DN{[]*ldap.RelativeDN{ + &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"1.3.6.1.4.1.1466.0", "Hi"}}}}}, + "1.3.6.1.4.1.1466.0=#04024869,DC=net": ldap.DN{[]*ldap.RelativeDN{ + &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"1.3.6.1.4.1.1466.0", "Hi"}}}, + &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"DC", "net"}}}}}, + "CN=Lu\\C4\\8Di\\C4\\87": ldap.DN{[]*ldap.RelativeDN{ + &ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"CN", "Lučić"}}}}}, } for test, answer := range testcases { - dn, err := ParseDN(test) + dn, err := ldap.ParseDN(test) if err != nil { t.Errorf(err.Error()) continue @@ -49,16 +51,16 @@ func TestSuccessfulDNParsing(t *testing.T) { } func TestErrorDNParsing(t *testing.T) { - testcases := map[string]string { - "*": "DN ended with incomplete type, value pair", - "cn=Jim\\0Test": "Failed to decode escaped character: encoding/hex: invalid byte: U+0054 'T'", - "cn=Jim\\0": "Got corrupted escaped character", + testcases := map[string]string{ + "*": "DN ended with incomplete type, value pair", + "cn=Jim\\0Test": "Failed to decode escaped character: encoding/hex: invalid byte: U+0054 'T'", + "cn=Jim\\0": "Got corrupted escaped character", "DC=example,=net": "DN ended with incomplete type, value pair", - "1=#0402486": "Failed to decode BER encoding: encoding/hex: odd length hex string", + "1=#0402486": "Failed to decode BER encoding: encoding/hex: odd length hex string", } for test, answer := range testcases { - _, err := ParseDN(test) + _, err := ldap.ParseDN(test) if err == nil { t.Errorf("Expected %s to fail parsing but succeeded\n", test) } else if err.Error() != answer { @@ -66,5 +68,3 @@ func TestErrorDNParsing(t *testing.T) { } } } - - diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/error.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/error.go new file mode 100644 index 00000000000..2dbc30ac085 --- /dev/null +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/error.go @@ -0,0 +1,137 @@ +package ldap + +import ( + "fmt" + + "gopkg.in/asn1-ber.v1" +) + +// LDAP Result Codes +const ( + LDAPResultSuccess = 0 + LDAPResultOperationsError = 1 + LDAPResultProtocolError = 2 + LDAPResultTimeLimitExceeded = 3 + LDAPResultSizeLimitExceeded = 4 + LDAPResultCompareFalse = 5 + LDAPResultCompareTrue = 6 + LDAPResultAuthMethodNotSupported = 7 + LDAPResultStrongAuthRequired = 8 + LDAPResultReferral = 10 + LDAPResultAdminLimitExceeded = 11 + LDAPResultUnavailableCriticalExtension = 12 + LDAPResultConfidentialityRequired = 13 + LDAPResultSaslBindInProgress = 14 + LDAPResultNoSuchAttribute = 16 + LDAPResultUndefinedAttributeType = 17 + LDAPResultInappropriateMatching = 18 + LDAPResultConstraintViolation = 19 + LDAPResultAttributeOrValueExists = 20 + LDAPResultInvalidAttributeSyntax = 21 + LDAPResultNoSuchObject = 32 + LDAPResultAliasProblem = 33 + LDAPResultInvalidDNSyntax = 34 + LDAPResultAliasDereferencingProblem = 36 + LDAPResultInappropriateAuthentication = 48 + LDAPResultInvalidCredentials = 49 + LDAPResultInsufficientAccessRights = 50 + LDAPResultBusy = 51 + LDAPResultUnavailable = 52 + LDAPResultUnwillingToPerform = 53 + LDAPResultLoopDetect = 54 + LDAPResultNamingViolation = 64 + LDAPResultObjectClassViolation = 65 + LDAPResultNotAllowedOnNonLeaf = 66 + LDAPResultNotAllowedOnRDN = 67 + LDAPResultEntryAlreadyExists = 68 + LDAPResultObjectClassModsProhibited = 69 + LDAPResultAffectsMultipleDSAs = 71 + LDAPResultOther = 80 + + ErrorNetwork = 200 + ErrorFilterCompile = 201 + ErrorFilterDecompile = 202 + ErrorDebugging = 203 + ErrorUnexpectedMessage = 204 + ErrorUnexpectedResponse = 205 +) + +var LDAPResultCodeMap = map[uint8]string{ + LDAPResultSuccess: "Success", + LDAPResultOperationsError: "Operations Error", + LDAPResultProtocolError: "Protocol Error", + LDAPResultTimeLimitExceeded: "Time Limit Exceeded", + LDAPResultSizeLimitExceeded: "Size Limit Exceeded", + LDAPResultCompareFalse: "Compare False", + LDAPResultCompareTrue: "Compare True", + LDAPResultAuthMethodNotSupported: "Auth Method Not Supported", + LDAPResultStrongAuthRequired: "Strong Auth Required", + LDAPResultReferral: "Referral", + LDAPResultAdminLimitExceeded: "Admin Limit Exceeded", + LDAPResultUnavailableCriticalExtension: "Unavailable Critical Extension", + LDAPResultConfidentialityRequired: "Confidentiality Required", + LDAPResultSaslBindInProgress: "Sasl Bind In Progress", + LDAPResultNoSuchAttribute: "No Such Attribute", + LDAPResultUndefinedAttributeType: "Undefined Attribute Type", + LDAPResultInappropriateMatching: "Inappropriate Matching", + LDAPResultConstraintViolation: "Constraint Violation", + LDAPResultAttributeOrValueExists: "Attribute Or Value Exists", + LDAPResultInvalidAttributeSyntax: "Invalid Attribute Syntax", + LDAPResultNoSuchObject: "No Such Object", + LDAPResultAliasProblem: "Alias Problem", + LDAPResultInvalidDNSyntax: "Invalid DN Syntax", + LDAPResultAliasDereferencingProblem: "Alias Dereferencing Problem", + LDAPResultInappropriateAuthentication: "Inappropriate Authentication", + LDAPResultInvalidCredentials: "Invalid Credentials", + LDAPResultInsufficientAccessRights: "Insufficient Access Rights", + LDAPResultBusy: "Busy", + LDAPResultUnavailable: "Unavailable", + LDAPResultUnwillingToPerform: "Unwilling To Perform", + LDAPResultLoopDetect: "Loop Detect", + LDAPResultNamingViolation: "Naming Violation", + LDAPResultObjectClassViolation: "Object Class Violation", + LDAPResultNotAllowedOnNonLeaf: "Not Allowed On Non Leaf", + LDAPResultNotAllowedOnRDN: "Not Allowed On RDN", + LDAPResultEntryAlreadyExists: "Entry Already Exists", + LDAPResultObjectClassModsProhibited: "Object Class Mods Prohibited", + LDAPResultAffectsMultipleDSAs: "Affects Multiple DSAs", + LDAPResultOther: "Other", +} + +func getLDAPResultCode(packet *ber.Packet) (code uint8, description string) { + if len(packet.Children) >= 2 { + response := packet.Children[1] + if response.ClassType == ber.ClassApplication && response.TagType == ber.TypeConstructed && len(response.Children) >= 3 { + // Children[1].Children[2] is the diagnosticMessage which is guaranteed to exist as seen here: https://tools.ietf.org/html/rfc4511#section-4.1.9 + return uint8(response.Children[0].Value.(int64)), response.Children[2].Value.(string) + } + } + + return ErrorNetwork, "Invalid packet format" +} + +type Error struct { + Err error + ResultCode uint8 +} + +func (e *Error) Error() string { + return fmt.Sprintf("LDAP Result Code %d %q: %s", e.ResultCode, LDAPResultCodeMap[e.ResultCode], e.Err.Error()) +} + +func NewError(resultCode uint8, err error) error { + return &Error{ResultCode: resultCode, Err: err} +} + +func IsErrorWithCode(err error, desiredResultCode uint8) bool { + if err == nil { + return false + } + + serverError, ok := err.(*Error) + if !ok { + return false + } + + return serverError.ResultCode == desiredResultCode +} diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/example_test.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/example_test.go index 0d441f43b99..b018a966489 100644 --- a/Godeps/_workspace/src/github.com/go-ldap/ldap/example_test.go +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/example_test.go @@ -5,10 +5,10 @@ import ( "fmt" "log" - "github.com/go-ldap/ldap" + "gopkg.in/ldap.v2" ) -// ExampleConn_Bind demonstrats how to bind a connection to an ldap user +// ExampleConn_Bind demonstrates how to bind a connection to an ldap user // allowing access to restricted attrabutes that user has access to func ExampleConn_Bind() { l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", "ldap.example.com", 389)) diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/filter.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/filter.go index 1ee1ff89d87..63bcec1e3ae 100644 --- a/Godeps/_workspace/src/github.com/go-ldap/ldap/filter.go +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/filter.go @@ -5,9 +5,12 @@ package ldap import ( + "bytes" + hexpac "encoding/hex" "errors" "fmt" "strings" + "unicode/utf8" "gopkg.in/asn1-ber.v1" ) @@ -50,6 +53,20 @@ var FilterSubstringsMap = map[uint64]string{ FilterSubstringsFinal: "Substrings Final", } +const ( + MatchingRuleAssertionMatchingRule = 1 + MatchingRuleAssertionType = 2 + MatchingRuleAssertionMatchValue = 3 + MatchingRuleAssertionDNAttributes = 4 +) + +var MatchingRuleAssertionMap = map[uint64]string{ + MatchingRuleAssertionMatchingRule: "Matching Rule Assertion Matching Rule", + MatchingRuleAssertionType: "Matching Rule Assertion Type", + MatchingRuleAssertionMatchValue: "Matching Rule Assertion Match Value", + MatchingRuleAssertionDNAttributes: "Matching Rule Assertion DN Attributes", +} + func CompileFilter(filter string) (*ber.Packet, error) { if len(filter) == 0 || filter[0] != '(' { return nil, NewError(ErrorFilterCompile, errors.New("ldap: filter does not start with an '('")) @@ -108,7 +125,7 @@ func DecompileFilter(packet *ber.Packet) (ret string, err error) { if i == 0 && child.Tag != FilterSubstringsInitial { ret += "*" } - ret += ber.DecodeString(child.Data.Bytes()) + ret += EscapeFilter(ber.DecodeString(child.Data.Bytes())) if child.Tag != FilterSubstringsFinal { ret += "*" } @@ -116,22 +133,53 @@ func DecompileFilter(packet *ber.Packet) (ret string, err error) { case FilterEqualityMatch: ret += ber.DecodeString(packet.Children[0].Data.Bytes()) ret += "=" - ret += ber.DecodeString(packet.Children[1].Data.Bytes()) + ret += EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes())) case FilterGreaterOrEqual: ret += ber.DecodeString(packet.Children[0].Data.Bytes()) ret += ">=" - ret += ber.DecodeString(packet.Children[1].Data.Bytes()) + ret += EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes())) case FilterLessOrEqual: ret += ber.DecodeString(packet.Children[0].Data.Bytes()) ret += "<=" - ret += ber.DecodeString(packet.Children[1].Data.Bytes()) + ret += EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes())) case FilterPresent: ret += ber.DecodeString(packet.Data.Bytes()) ret += "=*" case FilterApproxMatch: ret += ber.DecodeString(packet.Children[0].Data.Bytes()) ret += "~=" - ret += ber.DecodeString(packet.Children[1].Data.Bytes()) + ret += EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes())) + case FilterExtensibleMatch: + attr := "" + dnAttributes := false + matchingRule := "" + value := "" + + for _, child := range packet.Children { + switch child.Tag { + case MatchingRuleAssertionMatchingRule: + matchingRule = ber.DecodeString(child.Data.Bytes()) + case MatchingRuleAssertionType: + attr = ber.DecodeString(child.Data.Bytes()) + case MatchingRuleAssertionMatchValue: + value = ber.DecodeString(child.Data.Bytes()) + case MatchingRuleAssertionDNAttributes: + dnAttributes = child.Value.(bool) + } + } + + if len(attr) > 0 { + ret += attr + } + if dnAttributes { + ret += ":dn" + } + if len(matchingRule) > 0 { + ret += ":" + ret += matchingRule + } + ret += ":=" + ret += EscapeFilter(value) } ret += ")" @@ -155,58 +203,143 @@ func compileFilterSet(filter string, pos int, parent *ber.Packet) (int, error) { } func compileFilter(filter string, pos int) (*ber.Packet, int, error) { - var packet *ber.Packet - var err error + var ( + packet *ber.Packet + err error + ) defer func() { if r := recover(); r != nil { err = NewError(ErrorFilterCompile, errors.New("ldap: error compiling filter")) } }() - newPos := pos - switch filter[pos] { + + currentRune, currentWidth := utf8.DecodeRuneInString(filter[newPos:]) + + switch currentRune { + case utf8.RuneError: + return nil, 0, NewError(ErrorFilterCompile, fmt.Errorf("ldap: error reading rune at position %d", newPos)) case '(': - packet, newPos, err = compileFilter(filter, pos+1) + packet, newPos, err = compileFilter(filter, pos+currentWidth) newPos++ return packet, newPos, err case '&': packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterAnd, nil, FilterMap[FilterAnd]) - newPos, err = compileFilterSet(filter, pos+1, packet) + newPos, err = compileFilterSet(filter, pos+currentWidth, packet) return packet, newPos, err case '|': packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterOr, nil, FilterMap[FilterOr]) - newPos, err = compileFilterSet(filter, pos+1, packet) + newPos, err = compileFilterSet(filter, pos+currentWidth, packet) return packet, newPos, err case '!': packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterNot, nil, FilterMap[FilterNot]) var child *ber.Packet - child, newPos, err = compileFilter(filter, pos+1) + child, newPos, err = compileFilter(filter, pos+currentWidth) packet.AppendChild(child) return packet, newPos, err default: + READING_ATTR := 0 + READING_EXTENSIBLE_MATCHING_RULE := 1 + READING_CONDITION := 2 + + state := READING_ATTR + attribute := "" + extensibleDNAttributes := false + extensibleMatchingRule := "" condition := "" - for newPos < len(filter) && filter[newPos] != ')' { - switch { - case packet != nil: - condition += fmt.Sprintf("%c", filter[newPos]) - case filter[newPos] == '=': - packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterEqualityMatch, nil, FilterMap[FilterEqualityMatch]) - case filter[newPos] == '>' && filter[newPos+1] == '=': - packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterGreaterOrEqual, nil, FilterMap[FilterGreaterOrEqual]) - newPos++ - case filter[newPos] == '<' && filter[newPos+1] == '=': - packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterLessOrEqual, nil, FilterMap[FilterLessOrEqual]) - newPos++ - case filter[newPos] == '~' && filter[newPos+1] == '=': - packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterApproxMatch, nil, FilterMap[FilterLessOrEqual]) - newPos++ - case packet == nil: - attribute += fmt.Sprintf("%c", filter[newPos]) + + for newPos < len(filter) { + remainingFilter := filter[newPos:] + currentRune, currentWidth = utf8.DecodeRuneInString(remainingFilter) + if currentRune == ')' { + break + } + if currentRune == utf8.RuneError { + return packet, newPos, NewError(ErrorFilterCompile, fmt.Errorf("ldap: error reading rune at position %d", newPos)) + } + + switch state { + case READING_ATTR: + switch { + // Extensible rule, with only DN-matching + case currentRune == ':' && strings.HasPrefix(remainingFilter, ":dn:="): + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch]) + extensibleDNAttributes = true + state = READING_CONDITION + newPos += 5 + + // Extensible rule, with DN-matching and a matching OID + case currentRune == ':' && strings.HasPrefix(remainingFilter, ":dn:"): + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch]) + extensibleDNAttributes = true + state = READING_EXTENSIBLE_MATCHING_RULE + newPos += 4 + + // Extensible rule, with attr only + case currentRune == ':' && strings.HasPrefix(remainingFilter, ":="): + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch]) + state = READING_CONDITION + newPos += 2 + + // Extensible rule, with no DN attribute matching + case currentRune == ':': + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch]) + state = READING_EXTENSIBLE_MATCHING_RULE + newPos += 1 + + // Equality condition + case currentRune == '=': + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterEqualityMatch, nil, FilterMap[FilterEqualityMatch]) + state = READING_CONDITION + newPos += 1 + + // Greater-than or equal + case currentRune == '>' && strings.HasPrefix(remainingFilter, ">="): + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterGreaterOrEqual, nil, FilterMap[FilterGreaterOrEqual]) + state = READING_CONDITION + newPos += 2 + + // Less-than or equal + case currentRune == '<' && strings.HasPrefix(remainingFilter, "<="): + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterLessOrEqual, nil, FilterMap[FilterLessOrEqual]) + state = READING_CONDITION + newPos += 2 + + // Approx + case currentRune == '~' && strings.HasPrefix(remainingFilter, "~="): + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterApproxMatch, nil, FilterMap[FilterApproxMatch]) + state = READING_CONDITION + newPos += 2 + + // Still reading the attribute name + default: + attribute += fmt.Sprintf("%c", currentRune) + newPos += currentWidth + } + + case READING_EXTENSIBLE_MATCHING_RULE: + switch { + + // Matching rule OID is done + case currentRune == ':' && strings.HasPrefix(remainingFilter, ":="): + state = READING_CONDITION + newPos += 2 + + // Still reading the matching rule oid + default: + extensibleMatchingRule += fmt.Sprintf("%c", currentRune) + newPos += currentWidth + } + + case READING_CONDITION: + // append to the condition + condition += fmt.Sprintf("%c", currentRune) + newPos += currentWidth } - newPos++ } + if newPos == len(filter) { err = NewError(ErrorFilterCompile, errors.New("ldap: unexpected end of filter")) return packet, newPos, err @@ -217,6 +350,36 @@ func compileFilter(filter string, pos int) (*ber.Packet, int, error) { } switch { + case packet.Tag == FilterExtensibleMatch: + // MatchingRuleAssertion ::= SEQUENCE { + // matchingRule [1] MatchingRuleID OPTIONAL, + // type [2] AttributeDescription OPTIONAL, + // matchValue [3] AssertionValue, + // dnAttributes [4] BOOLEAN DEFAULT FALSE + // } + + // Include the matching rule oid, if specified + if len(extensibleMatchingRule) > 0 { + packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionMatchingRule, extensibleMatchingRule, MatchingRuleAssertionMap[MatchingRuleAssertionMatchingRule])) + } + + // Include the attribute, if specified + if len(attribute) > 0 { + packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionType, attribute, MatchingRuleAssertionMap[MatchingRuleAssertionType])) + } + + // Add the value (only required child) + encodedString, err := escapedStringToEncodedBytes(condition) + if err != nil { + return packet, newPos, err + } + packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionMatchValue, encodedString, MatchingRuleAssertionMap[MatchingRuleAssertionMatchValue])) + + // Defaults to false, so only include in the sequence if true + if extensibleDNAttributes { + packet.AppendChild(ber.NewBoolean(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionDNAttributes, extensibleDNAttributes, MatchingRuleAssertionMap[MatchingRuleAssertionDNAttributes])) + } + case packet.Tag == FilterEqualityMatch && condition == "*": packet = ber.NewString(ber.ClassContext, ber.TypePrimitive, FilterPresent, attribute, FilterMap[FilterPresent]) case packet.Tag == FilterEqualityMatch && strings.Contains(condition, "*"): @@ -238,15 +401,56 @@ func compileFilter(filter string, pos int) (*ber.Packet, int, error) { default: tag = FilterSubstringsAny } - seq.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, tag, part, FilterSubstringsMap[uint64(tag)])) + encodedString, err := escapedStringToEncodedBytes(part) + if err != nil { + return packet, newPos, err + } + seq.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, tag, encodedString, FilterSubstringsMap[uint64(tag)])) } packet.AppendChild(seq) default: + encodedString, err := escapedStringToEncodedBytes(condition) + if err != nil { + return packet, newPos, err + } packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute, "Attribute")) - packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, condition, "Condition")) + packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, encodedString, "Condition")) } - newPos++ + newPos += currentWidth return packet, newPos, err } } + +// Convert from "ABC\xx\xx\xx" form to literal bytes for transport +func escapedStringToEncodedBytes(escapedString string) (string, error) { + var buffer bytes.Buffer + i := 0 + for i < len(escapedString) { + currentRune, currentWidth := utf8.DecodeRuneInString(escapedString[i:]) + if currentRune == utf8.RuneError { + return "", NewError(ErrorFilterCompile, fmt.Errorf("ldap: error reading rune at position %d", i)) + } + + // Check for escaped hex characters and convert them to their literal value for transport. + if currentRune == '\\' { + // http://tools.ietf.org/search/rfc4515 + // \ (%x5C) is not a valid character unless it is followed by two HEX characters due to not + // being a member of UTF1SUBSET. + if i+2 > len(escapedString) { + return "", NewError(ErrorFilterCompile, errors.New("ldap: missing characters for escape in filter")) + } + if escByte, decodeErr := hexpac.DecodeString(escapedString[i+1 : i+3]); decodeErr != nil { + return "", NewError(ErrorFilterCompile, errors.New("ldap: invalid characters for escape in filter")) + } else { + buffer.WriteByte(escByte[0]) + i += 2 // +1 from end of loop, so 3 total for \xx. + } + } else { + buffer.WriteRune(currentRune) + } + + i += currentWidth + } + return buffer.String(), nil +} diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/filter_test.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/filter_test.go index 673ef080235..ae1b79b0c0d 100644 --- a/Godeps/_workspace/src/github.com/go-ldap/ldap/filter_test.go +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/filter_test.go @@ -1,54 +1,220 @@ -package ldap +package ldap_test import ( + "strings" "testing" "gopkg.in/asn1-ber.v1" + "gopkg.in/ldap.v2" ) type compileTest struct { - filterStr string - filterType int + filterStr string + + expectedFilter string + expectedType int + expectedErr string } var testFilters = []compileTest{ - compileTest{filterStr: "(&(sn=Miller)(givenName=Bob))", filterType: FilterAnd}, - compileTest{filterStr: "(|(sn=Miller)(givenName=Bob))", filterType: FilterOr}, - compileTest{filterStr: "(!(sn=Miller))", filterType: FilterNot}, - compileTest{filterStr: "(sn=Miller)", filterType: FilterEqualityMatch}, - compileTest{filterStr: "(sn=Mill*)", filterType: FilterSubstrings}, - compileTest{filterStr: "(sn=*Mill)", filterType: FilterSubstrings}, - compileTest{filterStr: "(sn=*Mill*)", filterType: FilterSubstrings}, - compileTest{filterStr: "(sn=*i*le*)", filterType: FilterSubstrings}, - compileTest{filterStr: "(sn=Mi*l*r)", filterType: FilterSubstrings}, - compileTest{filterStr: "(sn=Mi*le*)", filterType: FilterSubstrings}, - compileTest{filterStr: "(sn=*i*ler)", filterType: FilterSubstrings}, - compileTest{filterStr: "(sn>=Miller)", filterType: FilterGreaterOrEqual}, - compileTest{filterStr: "(sn<=Miller)", filterType: FilterLessOrEqual}, - compileTest{filterStr: "(sn=*)", filterType: FilterPresent}, - compileTest{filterStr: "(sn~=Miller)", filterType: FilterApproxMatch}, + compileTest{ + filterStr: "(&(sn=Miller)(givenName=Bob))", + expectedFilter: "(&(sn=Miller)(givenName=Bob))", + expectedType: ldap.FilterAnd, + }, + compileTest{ + filterStr: "(|(sn=Miller)(givenName=Bob))", + expectedFilter: "(|(sn=Miller)(givenName=Bob))", + expectedType: ldap.FilterOr, + }, + compileTest{ + filterStr: "(!(sn=Miller))", + expectedFilter: "(!(sn=Miller))", + expectedType: ldap.FilterNot, + }, + compileTest{ + filterStr: "(sn=Miller)", + expectedFilter: "(sn=Miller)", + expectedType: ldap.FilterEqualityMatch, + }, + compileTest{ + filterStr: "(sn=Mill*)", + expectedFilter: "(sn=Mill*)", + expectedType: ldap.FilterSubstrings, + }, + compileTest{ + filterStr: "(sn=*Mill)", + expectedFilter: "(sn=*Mill)", + expectedType: ldap.FilterSubstrings, + }, + compileTest{ + filterStr: "(sn=*Mill*)", + expectedFilter: "(sn=*Mill*)", + expectedType: ldap.FilterSubstrings, + }, + compileTest{ + filterStr: "(sn=*i*le*)", + expectedFilter: "(sn=*i*le*)", + expectedType: ldap.FilterSubstrings, + }, + compileTest{ + filterStr: "(sn=Mi*l*r)", + expectedFilter: "(sn=Mi*l*r)", + expectedType: ldap.FilterSubstrings, + }, + // substring filters escape properly + compileTest{ + filterStr: `(sn=Mi*함*r)`, + expectedFilter: `(sn=Mi*\ed\95\a8*r)`, + expectedType: ldap.FilterSubstrings, + }, + // already escaped substring filters don't get double-escaped + compileTest{ + filterStr: `(sn=Mi*\ed\95\a8*r)`, + expectedFilter: `(sn=Mi*\ed\95\a8*r)`, + expectedType: ldap.FilterSubstrings, + }, + compileTest{ + filterStr: "(sn=Mi*le*)", + expectedFilter: "(sn=Mi*le*)", + expectedType: ldap.FilterSubstrings, + }, + compileTest{ + filterStr: "(sn=*i*ler)", + expectedFilter: "(sn=*i*ler)", + expectedType: ldap.FilterSubstrings, + }, + compileTest{ + filterStr: "(sn>=Miller)", + expectedFilter: "(sn>=Miller)", + expectedType: ldap.FilterGreaterOrEqual, + }, + compileTest{ + filterStr: "(sn<=Miller)", + expectedFilter: "(sn<=Miller)", + expectedType: ldap.FilterLessOrEqual, + }, + compileTest{ + filterStr: "(sn=*)", + expectedFilter: "(sn=*)", + expectedType: ldap.FilterPresent, + }, + compileTest{ + filterStr: "(sn~=Miller)", + expectedFilter: "(sn~=Miller)", + expectedType: ldap.FilterApproxMatch, + }, + compileTest{ + filterStr: `(objectGUID='\fc\fe\a3\ab\f9\90N\aaGm\d5I~\d12)`, + expectedFilter: `(objectGUID='\fc\fe\a3\ab\f9\90N\aaGm\d5I~\d12)`, + expectedType: ldap.FilterEqualityMatch, + }, + compileTest{ + filterStr: `(objectGUID=абвгдеёжзийклмнопрстуфхцчшщъыьэюя)`, + expectedFilter: `(objectGUID=\d0\b0\d0\b1\d0\b2\d0\b3\d0\b4\d0\b5\d1\91\d0\b6\d0\b7\d0\b8\d0\b9\d0\ba\d0\bb\d0\bc\d0\bd\d0\be\d0\bf\d1\80\d1\81\d1\82\d1\83\d1\84\d1\85\d1\86\d1\87\d1\88\d1\89\d1\8a\d1\8b\d1\8c\d1\8d\d1\8e\d1\8f)`, + expectedType: ldap.FilterEqualityMatch, + }, + compileTest{ + filterStr: `(objectGUID=함수목록)`, + expectedFilter: `(objectGUID=\ed\95\a8\ec\88\98\eb\aa\a9\eb\a1\9d)`, + expectedType: ldap.FilterEqualityMatch, + }, + compileTest{ + filterStr: `(objectGUID=`, + expectedFilter: ``, + expectedType: 0, + expectedErr: "unexpected end of filter", + }, + compileTest{ + filterStr: `(objectGUID=함수목록`, + expectedFilter: ``, + expectedType: 0, + expectedErr: "unexpected end of filter", + }, + compileTest{ + filterStr: `(&(objectclass=inetorgperson)(cn=中文))`, + expectedFilter: `(&(objectclass=inetorgperson)(cn=\e4\b8\ad\e6\96\87))`, + expectedType: 0, + }, + // attr extension + compileTest{ + filterStr: `(memberOf:=foo)`, + expectedFilter: `(memberOf:=foo)`, + expectedType: ldap.FilterExtensibleMatch, + }, + // attr+named matching rule extension + compileTest{ + filterStr: `(memberOf:test:=foo)`, + expectedFilter: `(memberOf:test:=foo)`, + expectedType: ldap.FilterExtensibleMatch, + }, + // attr+oid matching rule extension + compileTest{ + filterStr: `(cn:1.2.3.4.5:=Fred Flintstone)`, + expectedFilter: `(cn:1.2.3.4.5:=Fred Flintstone)`, + expectedType: ldap.FilterExtensibleMatch, + }, + // attr+dn+oid matching rule extension + compileTest{ + filterStr: `(sn:dn:2.4.6.8.10:=Barney Rubble)`, + expectedFilter: `(sn:dn:2.4.6.8.10:=Barney Rubble)`, + expectedType: ldap.FilterExtensibleMatch, + }, + // attr+dn extension + compileTest{ + filterStr: `(o:dn:=Ace Industry)`, + expectedFilter: `(o:dn:=Ace Industry)`, + expectedType: ldap.FilterExtensibleMatch, + }, + // dn extension + compileTest{ + filterStr: `(:dn:2.4.6.8.10:=Dino)`, + expectedFilter: `(:dn:2.4.6.8.10:=Dino)`, + expectedType: ldap.FilterExtensibleMatch, + }, + compileTest{ + filterStr: `(memberOf:1.2.840.113556.1.4.1941:=CN=User1,OU=blah,DC=mydomain,DC=net)`, + expectedFilter: `(memberOf:1.2.840.113556.1.4.1941:=CN=User1,OU=blah,DC=mydomain,DC=net)`, + expectedType: ldap.FilterExtensibleMatch, + }, + // compileTest{ filterStr: "()", filterType: FilterExtensibleMatch }, } +var testInvalidFilters = []string{ + `(objectGUID=\zz)`, + `(objectGUID=\a)`, +} + func TestFilter(t *testing.T) { // Test Compiler and Decompiler for _, i := range testFilters { - filter, err := CompileFilter(i.filterStr) + filter, err := ldap.CompileFilter(i.filterStr) if err != nil { - t.Errorf("Problem compiling %s - %s", i.filterStr, err.Error()) - } else if filter.Tag != ber.Tag(i.filterType) { - t.Errorf("%q Expected %q got %q", i.filterStr, FilterMap[uint64(i.filterType)], FilterMap[uint64(filter.Tag)]) + if i.expectedErr == "" || !strings.Contains(err.Error(), i.expectedErr) { + t.Errorf("Problem compiling '%s' - '%v' (expected error to contain '%v')", i.filterStr, err, i.expectedErr) + } + } else if filter.Tag != ber.Tag(i.expectedType) { + t.Errorf("%q Expected %q got %q", i.filterStr, ldap.FilterMap[uint64(i.expectedType)], ldap.FilterMap[uint64(filter.Tag)]) } else { - o, err := DecompileFilter(filter) + o, err := ldap.DecompileFilter(filter) if err != nil { t.Errorf("Problem compiling %s - %s", i.filterStr, err.Error()) - } else if i.filterStr != o { - t.Errorf("%q expected, got %q", i.filterStr, o) + } else if i.expectedFilter != o { + t.Errorf("%q expected, got %q", i.expectedFilter, o) } } } } +func TestInvalidFilter(t *testing.T) { + for _, filterStr := range testInvalidFilters { + if _, err := ldap.CompileFilter(filterStr); err == nil { + t.Errorf("Problem compiling %s - expected err", filterStr) + } + } +} + func BenchmarkFilterCompile(b *testing.B) { b.StopTimer() filters := make([]string, len(testFilters)) @@ -61,7 +227,7 @@ func BenchmarkFilterCompile(b *testing.B) { maxIdx := len(filters) b.StartTimer() for i := 0; i < b.N; i++ { - CompileFilter(filters[i%maxIdx]) + ldap.CompileFilter(filters[i%maxIdx]) } } @@ -71,12 +237,12 @@ func BenchmarkFilterDecompile(b *testing.B) { // Test Compiler and Decompiler for idx, i := range testFilters { - filters[idx], _ = CompileFilter(i.filterStr) + filters[idx], _ = ldap.CompileFilter(i.filterStr) } maxIdx := len(filters) b.StartTimer() for i := 0; i < b.N; i++ { - DecompileFilter(filters[i%maxIdx]) + ldap.DecompileFilter(filters[i%maxIdx]) } } diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/ldap.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/ldap.go index e91972ff4c1..1620aaea6ee 100644 --- a/Godeps/_workspace/src/github.com/go-ldap/ldap/ldap.go +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/ldap.go @@ -6,7 +6,6 @@ package ldap import ( "errors" - "fmt" "io/ioutil" "os" @@ -60,98 +59,6 @@ var ApplicationMap = map[uint8]string{ ApplicationExtendedResponse: "Extended Response", } -// LDAP Result Codes -const ( - LDAPResultSuccess = 0 - LDAPResultOperationsError = 1 - LDAPResultProtocolError = 2 - LDAPResultTimeLimitExceeded = 3 - LDAPResultSizeLimitExceeded = 4 - LDAPResultCompareFalse = 5 - LDAPResultCompareTrue = 6 - LDAPResultAuthMethodNotSupported = 7 - LDAPResultStrongAuthRequired = 8 - LDAPResultReferral = 10 - LDAPResultAdminLimitExceeded = 11 - LDAPResultUnavailableCriticalExtension = 12 - LDAPResultConfidentialityRequired = 13 - LDAPResultSaslBindInProgress = 14 - LDAPResultNoSuchAttribute = 16 - LDAPResultUndefinedAttributeType = 17 - LDAPResultInappropriateMatching = 18 - LDAPResultConstraintViolation = 19 - LDAPResultAttributeOrValueExists = 20 - LDAPResultInvalidAttributeSyntax = 21 - LDAPResultNoSuchObject = 32 - LDAPResultAliasProblem = 33 - LDAPResultInvalidDNSyntax = 34 - LDAPResultAliasDereferencingProblem = 36 - LDAPResultInappropriateAuthentication = 48 - LDAPResultInvalidCredentials = 49 - LDAPResultInsufficientAccessRights = 50 - LDAPResultBusy = 51 - LDAPResultUnavailable = 52 - LDAPResultUnwillingToPerform = 53 - LDAPResultLoopDetect = 54 - LDAPResultNamingViolation = 64 - LDAPResultObjectClassViolation = 65 - LDAPResultNotAllowedOnNonLeaf = 66 - LDAPResultNotAllowedOnRDN = 67 - LDAPResultEntryAlreadyExists = 68 - LDAPResultObjectClassModsProhibited = 69 - LDAPResultAffectsMultipleDSAs = 71 - LDAPResultOther = 80 - - ErrorNetwork = 200 - ErrorFilterCompile = 201 - ErrorFilterDecompile = 202 - ErrorDebugging = 203 - ErrorUnexpectedMessage = 204 - ErrorUnexpectedResponse = 205 -) - -var LDAPResultCodeMap = map[uint8]string{ - LDAPResultSuccess: "Success", - LDAPResultOperationsError: "Operations Error", - LDAPResultProtocolError: "Protocol Error", - LDAPResultTimeLimitExceeded: "Time Limit Exceeded", - LDAPResultSizeLimitExceeded: "Size Limit Exceeded", - LDAPResultCompareFalse: "Compare False", - LDAPResultCompareTrue: "Compare True", - LDAPResultAuthMethodNotSupported: "Auth Method Not Supported", - LDAPResultStrongAuthRequired: "Strong Auth Required", - LDAPResultReferral: "Referral", - LDAPResultAdminLimitExceeded: "Admin Limit Exceeded", - LDAPResultUnavailableCriticalExtension: "Unavailable Critical Extension", - LDAPResultConfidentialityRequired: "Confidentiality Required", - LDAPResultSaslBindInProgress: "Sasl Bind In Progress", - LDAPResultNoSuchAttribute: "No Such Attribute", - LDAPResultUndefinedAttributeType: "Undefined Attribute Type", - LDAPResultInappropriateMatching: "Inappropriate Matching", - LDAPResultConstraintViolation: "Constraint Violation", - LDAPResultAttributeOrValueExists: "Attribute Or Value Exists", - LDAPResultInvalidAttributeSyntax: "Invalid Attribute Syntax", - LDAPResultNoSuchObject: "No Such Object", - LDAPResultAliasProblem: "Alias Problem", - LDAPResultInvalidDNSyntax: "Invalid DN Syntax", - LDAPResultAliasDereferencingProblem: "Alias Dereferencing Problem", - LDAPResultInappropriateAuthentication: "Inappropriate Authentication", - LDAPResultInvalidCredentials: "Invalid Credentials", - LDAPResultInsufficientAccessRights: "Insufficient Access Rights", - LDAPResultBusy: "Busy", - LDAPResultUnavailable: "Unavailable", - LDAPResultUnwillingToPerform: "Unwilling To Perform", - LDAPResultLoopDetect: "Loop Detect", - LDAPResultNamingViolation: "Naming Violation", - LDAPResultObjectClassViolation: "Object Class Violation", - LDAPResultNotAllowedOnNonLeaf: "Not Allowed On Non Leaf", - LDAPResultNotAllowedOnRDN: "Not Allowed On RDN", - LDAPResultEntryAlreadyExists: "Entry Already Exists", - LDAPResultObjectClassModsProhibited: "Object Class Mods Prohibited", - LDAPResultAffectsMultipleDSAs: "Affects Multiple DSAs", - LDAPResultOther: "Other", -} - // Ldap Behera Password Policy Draft 10 (https://tools.ietf.org/html/draft-behera-ldap-password-policy-10) const ( BeheraPasswordExpired = 0 @@ -318,8 +225,8 @@ func addRequestDescriptions(packet *ber.Packet) { } func addDefaultLDAPResponseDescriptions(packet *ber.Packet) { - resultCode := packet.Children[1].Children[0].Value.(int64) - packet.Children[1].Children[0].Description = "Result Code (" + LDAPResultCodeMap[uint8(resultCode)] + ")" + resultCode, _ := getLDAPResultCode(packet) + packet.Children[1].Children[0].Description = "Result Code (" + LDAPResultCodeMap[resultCode] + ")" packet.Children[1].Children[1].Description = "Matched DN" packet.Children[1].Children[2].Description = "Error Message" if len(packet.Children[1].Children) > 3 { @@ -343,30 +250,6 @@ func DebugBinaryFile(fileName string) error { return nil } -type Error struct { - Err error - ResultCode uint8 -} - -func (e *Error) Error() string { - return fmt.Sprintf("LDAP Result Code %d %q: %s", e.ResultCode, LDAPResultCodeMap[e.ResultCode], e.Err.Error()) -} - -func NewError(resultCode uint8, err error) error { - return &Error{ResultCode: resultCode, Err: err} -} - -func getLDAPResultCode(packet *ber.Packet) (code uint8, description string) { - if len(packet.Children) >= 2 { - response := packet.Children[1] - if response.ClassType == ber.ClassApplication && response.TagType == ber.TypeConstructed && len(response.Children) >= 3 { - return uint8(response.Children[0].Value.(int64)), response.Children[2].Value.(string) - } - } - - return ErrorNetwork, "Invalid packet format" -} - var hex = "0123456789abcdef" func mustEscape(c byte) bool { diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/ldap_test.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/ldap_test.go index e9933f99a69..9f430518001 100644 --- a/Godeps/_workspace/src/github.com/go-ldap/ldap/ldap_test.go +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/ldap_test.go @@ -1,9 +1,11 @@ -package ldap +package ldap_test import ( "crypto/tls" "fmt" "testing" + + "gopkg.in/ldap.v2" ) var ldapServer = "ldap.itd.umich.edu" @@ -21,7 +23,7 @@ var attributes = []string{ func TestDial(t *testing.T) { fmt.Printf("TestDial: starting...\n") - l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) + l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) if err != nil { t.Errorf(err.Error()) return @@ -32,7 +34,7 @@ func TestDial(t *testing.T) { func TestDialTLS(t *testing.T) { fmt.Printf("TestDialTLS: starting...\n") - l, err := DialTLS("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapTLSPort), &tls.Config{InsecureSkipVerify: true}) + l, err := ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapTLSPort), &tls.Config{InsecureSkipVerify: true}) if err != nil { t.Errorf(err.Error()) return @@ -43,7 +45,7 @@ func TestDialTLS(t *testing.T) { func TestStartTLS(t *testing.T) { fmt.Printf("TestStartTLS: starting...\n") - l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) + l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) if err != nil { t.Errorf(err.Error()) return @@ -58,16 +60,16 @@ func TestStartTLS(t *testing.T) { func TestSearch(t *testing.T) { fmt.Printf("TestSearch: starting...\n") - l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) + l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) if err != nil { t.Errorf(err.Error()) return } defer l.Close() - searchRequest := NewSearchRequest( + searchRequest := ldap.NewSearchRequest( baseDN, - ScopeWholeSubtree, DerefAlways, 0, 0, false, + ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false, filter[0], attributes, nil) @@ -83,16 +85,16 @@ func TestSearch(t *testing.T) { func TestSearchStartTLS(t *testing.T) { fmt.Printf("TestSearchStartTLS: starting...\n") - l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) + l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) if err != nil { t.Errorf(err.Error()) return } defer l.Close() - searchRequest := NewSearchRequest( + searchRequest := ldap.NewSearchRequest( baseDN, - ScopeWholeSubtree, DerefAlways, 0, 0, false, + ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false, filter[0], attributes, nil) @@ -123,7 +125,7 @@ func TestSearchStartTLS(t *testing.T) { func TestSearchWithPaging(t *testing.T) { fmt.Printf("TestSearchWithPaging: starting...\n") - l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) + l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) if err != nil { t.Errorf(err.Error()) return @@ -136,9 +138,9 @@ func TestSearchWithPaging(t *testing.T) { return } - searchRequest := NewSearchRequest( + searchRequest := ldap.NewSearchRequest( baseDN, - ScopeWholeSubtree, DerefAlways, 0, 0, false, + ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false, filter[2], attributes, nil) @@ -149,12 +151,38 @@ func TestSearchWithPaging(t *testing.T) { } fmt.Printf("TestSearchWithPaging: %s -> num of entries = %d\n", searchRequest.Filter, len(sr.Entries)) + + searchRequest = ldap.NewSearchRequest( + baseDN, + ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false, + filter[2], + attributes, + []ldap.Control{ldap.NewControlPaging(5)}) + sr, err = l.SearchWithPaging(searchRequest, 5) + if err != nil { + t.Errorf(err.Error()) + return + } + + fmt.Printf("TestSearchWithPaging: %s -> num of entries = %d\n", searchRequest.Filter, len(sr.Entries)) + + searchRequest = ldap.NewSearchRequest( + baseDN, + ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false, + filter[2], + attributes, + []ldap.Control{ldap.NewControlPaging(500)}) + sr, err = l.SearchWithPaging(searchRequest, 5) + if err == nil { + t.Errorf("expected an error when paging size in control in search request doesn't match size given in call, got none") + return + } } -func searchGoroutine(t *testing.T, l *Conn, results chan *SearchResult, i int) { - searchRequest := NewSearchRequest( +func searchGoroutine(t *testing.T, l *ldap.Conn, results chan *ldap.SearchResult, i int) { + searchRequest := ldap.NewSearchRequest( baseDN, - ScopeWholeSubtree, DerefAlways, 0, 0, false, + ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false, filter[i], attributes, nil) @@ -169,17 +197,17 @@ func searchGoroutine(t *testing.T, l *Conn, results chan *SearchResult, i int) { func testMultiGoroutineSearch(t *testing.T, TLS bool, startTLS bool) { fmt.Printf("TestMultiGoroutineSearch: starting...\n") - var l *Conn + var l *ldap.Conn var err error if TLS { - l, err = DialTLS("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapTLSPort), &tls.Config{InsecureSkipVerify: true}) + l, err = ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapTLSPort), &tls.Config{InsecureSkipVerify: true}) if err != nil { t.Errorf(err.Error()) return } defer l.Close() } else { - l, err = Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) + l, err = ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) if err != nil { t.Errorf(err.Error()) return @@ -195,9 +223,9 @@ func testMultiGoroutineSearch(t *testing.T, TLS bool, startTLS bool) { } } - results := make([]chan *SearchResult, len(filter)) + results := make([]chan *ldap.SearchResult, len(filter)) for i := range filter { - results[i] = make(chan *SearchResult) + results[i] = make(chan *ldap.SearchResult) go searchGoroutine(t, l, results[i], i) } for i := range filter { @@ -217,17 +245,17 @@ func TestMultiGoroutineSearch(t *testing.T) { } func TestEscapeFilter(t *testing.T) { - if got, want := EscapeFilter("a\x00b(c)d*e\\f"), `a\00b\28c\29d\2ae\5cf`; got != want { + if got, want := ldap.EscapeFilter("a\x00b(c)d*e\\f"), `a\00b\28c\29d\2ae\5cf`; got != want { t.Errorf("Got %s, expected %s", want, got) } - if got, want := EscapeFilter("Lučić"), `Lu\c4\8di\c4\87`; got != want { + if got, want := ldap.EscapeFilter("Lučić"), `Lu\c4\8di\c4\87`; got != want { t.Errorf("Got %s, expected %s", want, got) } } func TestCompare(t *testing.T) { fmt.Printf("TestCompare: starting...\n") - l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) + l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort)) if err != nil { t.Fatal(err.Error()) } @@ -243,5 +271,5 @@ func TestCompare(t *testing.T) { return } - fmt.Printf("TestCompare: -> num of entries = %d\n", sr) + fmt.Printf("TestCompare: -> %v\n", sr) } diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/search.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/search.go index 5ae3c449477..23a2cf2b202 100644 --- a/Godeps/_workspace/src/github.com/go-ldap/ldap/search.go +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/search.go @@ -62,6 +62,7 @@ package ldap import ( "errors" "fmt" + "sort" "strings" "gopkg.in/asn1-ber.v1" @@ -93,6 +94,26 @@ var DerefMap = map[int]string{ DerefAlways: "DerefAlways", } +// NewEntry returns an Entry object with the specified distinguished name and attribute key-value pairs. +// The map of attributes is accessed in alphabetical order of the keys in order to ensure that, for the +// same input map of attributes, the output entry will contain the same order of attributes +func NewEntry(dn string, attributes map[string][]string) *Entry { + var attributeNames []string + for attributeName := range attributes { + attributeNames = append(attributeNames, attributeName) + } + sort.Strings(attributeNames) + + var encodedAttributes []*EntryAttribute + for _, attributeName := range attributeNames { + encodedAttributes = append(encodedAttributes, NewEntryAttribute(attributeName, attributes[attributeName])) + } + return &Entry{ + DN: dn, + Attributes: encodedAttributes, + } +} + type Entry struct { DN string Attributes []*EntryAttribute @@ -146,6 +167,19 @@ func (e *Entry) PrettyPrint(indent int) { } } +// NewEntryAttribute returns a new EntryAttribute with the desired key-value pair +func NewEntryAttribute(name string, values []string) *EntryAttribute { + var bytes [][]byte + for _, value := range values { + bytes = append(bytes, []byte(value)) + } + return &EntryAttribute{ + Name: name, + Values: values, + ByteValues: bytes, + } +} + type EntryAttribute struct { Name string Values []string @@ -234,13 +268,32 @@ func NewSearchRequest( } } +// SearchWithPaging accepts a search request and desired page size in order to execute LDAP queries to fulfill the +// search request. All paged LDAP query responses will be buffered and the final result will be returned atomically. +// The following four cases are possible given the arguments: +// - given SearchRequest missing a control of type ControlTypePaging: we will add one with the desired paging size +// - given SearchRequest contains a control of type ControlTypePaging that isn't actually a ControlPaging: fail without issuing any queries +// - given SearchRequest contains a control of type ControlTypePaging with pagingSize equal to the size requested: no change to the search request +// - given SearchRequest contains a control of type ControlTypePaging with pagingSize not equal to the size requested: fail without issuing any queries +// A requested pagingSize of 0 is interpreted as no limit by LDAP servers. func (l *Conn) SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error) { - if searchRequest.Controls == nil { - searchRequest.Controls = make([]Control, 0) + var pagingControl *ControlPaging + + control := FindControl(searchRequest.Controls, ControlTypePaging) + if control == nil { + pagingControl = NewControlPaging(pagingSize) + searchRequest.Controls = append(searchRequest.Controls, pagingControl) + } else { + castControl, ok := control.(*ControlPaging) + if !ok { + return nil, fmt.Errorf("Expected paging control to be of type *ControlPaging, got %v", control) + } + if castControl.PagingSize != pagingSize { + return nil, fmt.Errorf("Paging size given in search request (%d) conflicts with size given in search call (%d)", castControl.PagingSize, pagingSize) + } + pagingControl = castControl } - pagingControl := NewControlPaging(pagingSize) - searchRequest.Controls = append(searchRequest.Controls, pagingControl) searchResult := new(SearchResult) for { result, err := l.Search(searchRequest) diff --git a/Godeps/_workspace/src/github.com/go-ldap/ldap/search_test.go b/Godeps/_workspace/src/github.com/go-ldap/ldap/search_test.go new file mode 100644 index 00000000000..efb8147d1a5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/go-ldap/ldap/search_test.go @@ -0,0 +1,31 @@ +package ldap + +import ( + "reflect" + "testing" +) + +// TestNewEntry tests that repeated calls to NewEntry return the same value with the same input +func TestNewEntry(t *testing.T) { + dn := "testDN" + attributes := map[string][]string{ + "alpha": {"value"}, + "beta": {"value"}, + "gamma": {"value"}, + "delta": {"value"}, + "epsilon": {"value"}, + } + exectedEntry := NewEntry(dn, attributes) + + iteration := 0 + for { + if iteration == 100 { + break + } + testEntry := NewEntry(dn, attributes) + if !reflect.DeepEqual(exectedEntry, testEntry) { + t.Fatalf("consequent calls to NewEntry did not yield the same result:\n\texpected:\n\t%s\n\tgot:\n\t%s\n", exectedEntry, testEntry) + } + iteration = iteration + 1 + } +} diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/.gitignore b/Godeps/_workspace/src/github.com/gorilla/websocket/.gitignore new file mode 100644 index 00000000000..00268614f04 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/.gitignore @@ -0,0 +1,22 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/.travis.yml b/Godeps/_workspace/src/github.com/gorilla/websocket/.travis.yml new file mode 100644 index 00000000000..ace2e2f41f9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/.travis.yml @@ -0,0 +1,20 @@ +language: go +sudo: false + +matrix: + include: + - go: 1.2 + - go: 1.3 + - go: 1.4 + - go: 1.5 + - go: 1.6 + - go: tip + +install: + - go get golang.org/x/tools/cmd/vet + +script: + - go get -t -v ./... + - diff -u <(echo -n) <(gofmt -d .) + - go vet . + - go test -v -race ./... diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/AUTHORS b/Godeps/_workspace/src/github.com/gorilla/websocket/AUTHORS new file mode 100644 index 00000000000..b003eca0ca1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/AUTHORS @@ -0,0 +1,8 @@ +# This is the official list of Gorilla WebSocket authors for copyright +# purposes. +# +# Please keep the list sorted. + +Gary Burd +Joachim Bauch + diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/LICENSE b/Godeps/_workspace/src/github.com/gorilla/websocket/LICENSE new file mode 100644 index 00000000000..9171c972252 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/README.md b/Godeps/_workspace/src/github.com/gorilla/websocket/README.md new file mode 100644 index 00000000000..9d71959ea1a --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/README.md @@ -0,0 +1,61 @@ +# Gorilla WebSocket + +Gorilla WebSocket is a [Go](http://golang.org/) implementation of the +[WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. + +### Documentation + +* [API Reference](http://godoc.org/github.com/gorilla/websocket) +* [Chat example](https://github.com/gorilla/websocket/tree/master/examples/chat) +* [Command example](https://github.com/gorilla/websocket/tree/master/examples/command) +* [Client and server example](https://github.com/gorilla/websocket/tree/master/examples/echo) +* [File watch example](https://github.com/gorilla/websocket/tree/master/examples/filewatch) + +### Status + +The Gorilla WebSocket package provides a complete and tested implementation of +the [WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. The +package API is stable. + +### Installation + + go get github.com/gorilla/websocket + +### Protocol Compliance + +The Gorilla WebSocket package passes the server tests in the [Autobahn Test +Suite](http://autobahn.ws/testsuite) using the application in the [examples/autobahn +subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn). + +### Gorilla WebSocket compared with other packages + + + + + + + + + + + + + + + + + + +
github.com/gorillagolang.org/x/net
RFC 6455 Features
Passes Autobahn Test SuiteYesNo
Receive fragmented messageYesNo, see note 1
Send close messageYesNo
Send pings and receive pongsYesNo
Get the type of a received data messageYesYes, see note 2
Other Features
Limit size of received messageYesNo
Read message using io.ReaderYesNo, see note 3
Write message using io.WriteCloserYesNo, see note 3
+ +Notes: + +1. Large messages are fragmented in [Chrome's new WebSocket implementation](http://www.ietf.org/mail-archive/web/hybi/current/msg10503.html). +2. The application can get the type of a received data message by implementing + a [Codec marshal](http://godoc.org/golang.org/x/net/websocket#Codec.Marshal) + function. +3. The go.net io.Reader and io.Writer operate across WebSocket frame boundaries. + Read returns when the input buffer is full or a frame boundary is + encountered. Each call to Write sends a single frame message. The Gorilla + io.Reader and io.WriteCloser operate on a single WebSocket message. + diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/client.go b/Godeps/_workspace/src/github.com/gorilla/websocket/client.go new file mode 100644 index 00000000000..a353e18565c --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/client.go @@ -0,0 +1,350 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bufio" + "bytes" + "crypto/tls" + "encoding/base64" + "errors" + "io" + "io/ioutil" + "net" + "net/http" + "net/url" + "strings" + "time" +) + +// ErrBadHandshake is returned when the server response to opening handshake is +// invalid. +var ErrBadHandshake = errors.New("websocket: bad handshake") + +// NewClient creates a new client connection using the given net connection. +// The URL u specifies the host and request URI. Use requestHeader to specify +// the origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies +// (Cookie). Use the response.Header to get the selected subprotocol +// (Sec-WebSocket-Protocol) and cookies (Set-Cookie). +// +// If the WebSocket handshake fails, ErrBadHandshake is returned along with a +// non-nil *http.Response so that callers can handle redirects, authentication, +// etc. +// +// Deprecated: Use Dialer instead. +func NewClient(netConn net.Conn, u *url.URL, requestHeader http.Header, readBufSize, writeBufSize int) (c *Conn, response *http.Response, err error) { + d := Dialer{ + ReadBufferSize: readBufSize, + WriteBufferSize: writeBufSize, + NetDial: func(net, addr string) (net.Conn, error) { + return netConn, nil + }, + } + return d.Dial(u.String(), requestHeader) +} + +// A Dialer contains options for connecting to WebSocket server. +type Dialer struct { + // NetDial specifies the dial function for creating TCP connections. If + // NetDial is nil, net.Dial is used. + NetDial func(network, addr string) (net.Conn, error) + + // Proxy specifies a function to return a proxy for a given + // Request. If the function returns a non-nil error, the + // request is aborted with the provided error. + // If Proxy is nil or returns a nil *URL, no proxy is used. + Proxy func(*http.Request) (*url.URL, error) + + // TLSClientConfig specifies the TLS configuration to use with tls.Client. + // If nil, the default configuration is used. + TLSClientConfig *tls.Config + + // HandshakeTimeout specifies the duration for the handshake to complete. + HandshakeTimeout time.Duration + + // Input and output buffer sizes. If the buffer size is zero, then a + // default value of 4096 is used. + ReadBufferSize, WriteBufferSize int + + // Subprotocols specifies the client's requested subprotocols. + Subprotocols []string +} + +var errMalformedURL = errors.New("malformed ws or wss URL") + +// parseURL parses the URL. +// +// This function is a replacement for the standard library url.Parse function. +// In Go 1.4 and earlier, url.Parse loses information from the path. +func parseURL(s string) (*url.URL, error) { + // From the RFC: + // + // ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ] + // wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ] + + var u url.URL + switch { + case strings.HasPrefix(s, "ws://"): + u.Scheme = "ws" + s = s[len("ws://"):] + case strings.HasPrefix(s, "wss://"): + u.Scheme = "wss" + s = s[len("wss://"):] + default: + return nil, errMalformedURL + } + + if i := strings.Index(s, "?"); i >= 0 { + u.RawQuery = s[i+1:] + s = s[:i] + } + + if i := strings.Index(s, "/"); i >= 0 { + u.Opaque = s[i:] + s = s[:i] + } else { + u.Opaque = "/" + } + + u.Host = s + + if strings.Contains(u.Host, "@") { + // Don't bother parsing user information because user information is + // not allowed in websocket URIs. + return nil, errMalformedURL + } + + return &u, nil +} + +func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) { + hostPort = u.Host + hostNoPort = u.Host + if i := strings.LastIndex(u.Host, ":"); i > strings.LastIndex(u.Host, "]") { + hostNoPort = hostNoPort[:i] + } else { + switch u.Scheme { + case "wss": + hostPort += ":443" + case "https": + hostPort += ":443" + default: + hostPort += ":80" + } + } + return hostPort, hostNoPort +} + +// DefaultDialer is a dialer with all fields set to the default zero values. +var DefaultDialer = &Dialer{ + Proxy: http.ProxyFromEnvironment, +} + +// Dial creates a new client connection. Use requestHeader to specify the +// origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie). +// Use the response.Header to get the selected subprotocol +// (Sec-WebSocket-Protocol) and cookies (Set-Cookie). +// +// If the WebSocket handshake fails, ErrBadHandshake is returned along with a +// non-nil *http.Response so that callers can handle redirects, authentication, +// etcetera. The response body may not contain the entire response and does not +// need to be closed by the application. +func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) { + + if d == nil { + d = &Dialer{ + Proxy: http.ProxyFromEnvironment, + } + } + + challengeKey, err := generateChallengeKey() + if err != nil { + return nil, nil, err + } + + u, err := parseURL(urlStr) + if err != nil { + return nil, nil, err + } + + switch u.Scheme { + case "ws": + u.Scheme = "http" + case "wss": + u.Scheme = "https" + default: + return nil, nil, errMalformedURL + } + + if u.User != nil { + // User name and password are not allowed in websocket URIs. + return nil, nil, errMalformedURL + } + + req := &http.Request{ + Method: "GET", + URL: u, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: make(http.Header), + Host: u.Host, + } + + // Set the request headers using the capitalization for names and values in + // RFC examples. Although the capitalization shouldn't matter, there are + // servers that depend on it. The Header.Set method is not used because the + // method canonicalizes the header names. + req.Header["Upgrade"] = []string{"websocket"} + req.Header["Connection"] = []string{"Upgrade"} + req.Header["Sec-WebSocket-Key"] = []string{challengeKey} + req.Header["Sec-WebSocket-Version"] = []string{"13"} + if len(d.Subprotocols) > 0 { + req.Header["Sec-WebSocket-Protocol"] = []string{strings.Join(d.Subprotocols, ", ")} + } + for k, vs := range requestHeader { + switch { + case k == "Host": + if len(vs) > 0 { + req.Host = vs[0] + } + case k == "Upgrade" || + k == "Connection" || + k == "Sec-Websocket-Key" || + k == "Sec-Websocket-Version" || + (k == "Sec-Websocket-Protocol" && len(d.Subprotocols) > 0): + return nil, nil, errors.New("websocket: duplicate header not allowed: " + k) + default: + req.Header[k] = vs + } + } + + hostPort, hostNoPort := hostPortNoPort(u) + + var proxyURL *url.URL + // Check wether the proxy method has been configured + if d.Proxy != nil { + proxyURL, err = d.Proxy(req) + } + if err != nil { + return nil, nil, err + } + + var targetHostPort string + if proxyURL != nil { + targetHostPort, _ = hostPortNoPort(proxyURL) + } else { + targetHostPort = hostPort + } + + var deadline time.Time + if d.HandshakeTimeout != 0 { + deadline = time.Now().Add(d.HandshakeTimeout) + } + + netDial := d.NetDial + if netDial == nil { + netDialer := &net.Dialer{Deadline: deadline} + netDial = netDialer.Dial + } + + netConn, err := netDial("tcp", targetHostPort) + if err != nil { + return nil, nil, err + } + + defer func() { + if netConn != nil { + netConn.Close() + } + }() + + if err := netConn.SetDeadline(deadline); err != nil { + return nil, nil, err + } + + if proxyURL != nil { + connectHeader := make(http.Header) + if user := proxyURL.User; user != nil { + proxyUser := user.Username() + if proxyPassword, passwordSet := user.Password(); passwordSet { + credential := base64.StdEncoding.EncodeToString([]byte(proxyUser + ":" + proxyPassword)) + connectHeader.Set("Proxy-Authorization", "Basic "+credential) + } + } + connectReq := &http.Request{ + Method: "CONNECT", + URL: &url.URL{Opaque: hostPort}, + Host: hostPort, + Header: connectHeader, + } + + connectReq.Write(netConn) + + // Read response. + // Okay to use and discard buffered reader here, because + // TLS server will not speak until spoken to. + br := bufio.NewReader(netConn) + resp, err := http.ReadResponse(br, connectReq) + if err != nil { + return nil, nil, err + } + if resp.StatusCode != 200 { + f := strings.SplitN(resp.Status, " ", 2) + return nil, nil, errors.New(f[1]) + } + } + + if u.Scheme == "https" { + cfg := d.TLSClientConfig + if cfg == nil { + cfg = &tls.Config{ServerName: hostNoPort} + } else if cfg.ServerName == "" { + shallowCopy := *cfg + cfg = &shallowCopy + cfg.ServerName = hostNoPort + } + tlsConn := tls.Client(netConn, cfg) + netConn = tlsConn + if err := tlsConn.Handshake(); err != nil { + return nil, nil, err + } + if !cfg.InsecureSkipVerify { + if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil { + return nil, nil, err + } + } + } + + conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize) + + if err := req.Write(netConn); err != nil { + return nil, nil, err + } + + resp, err := http.ReadResponse(conn.br, req) + if err != nil { + return nil, nil, err + } + if resp.StatusCode != 101 || + !strings.EqualFold(resp.Header.Get("Upgrade"), "websocket") || + !strings.EqualFold(resp.Header.Get("Connection"), "upgrade") || + resp.Header.Get("Sec-Websocket-Accept") != computeAcceptKey(challengeKey) { + // Before closing the network connection on return from this + // function, slurp up some of the response to aid application + // debugging. + buf := make([]byte, 1024) + n, _ := io.ReadFull(resp.Body, buf) + resp.Body = ioutil.NopCloser(bytes.NewReader(buf[:n])) + return nil, resp, ErrBadHandshake + } + + resp.Body = ioutil.NopCloser(bytes.NewReader([]byte{})) + conn.subprotocol = resp.Header.Get("Sec-Websocket-Protocol") + + netConn.SetDeadline(time.Time{}) + netConn = nil // to avoid close in defer. + return conn, resp, nil +} diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/conn.go b/Godeps/_workspace/src/github.com/gorilla/websocket/conn.go new file mode 100644 index 00000000000..eff26c6328a --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/conn.go @@ -0,0 +1,915 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bufio" + "encoding/binary" + "errors" + "io" + "io/ioutil" + "math/rand" + "net" + "strconv" + "time" +) + +const ( + maxFrameHeaderSize = 2 + 8 + 4 // Fixed header + length + mask + maxControlFramePayloadSize = 125 + finalBit = 1 << 7 + maskBit = 1 << 7 + writeWait = time.Second + + defaultReadBufferSize = 4096 + defaultWriteBufferSize = 4096 + + continuationFrame = 0 + noFrame = -1 +) + +// Close codes defined in RFC 6455, section 11.7. +const ( + CloseNormalClosure = 1000 + CloseGoingAway = 1001 + CloseProtocolError = 1002 + CloseUnsupportedData = 1003 + CloseNoStatusReceived = 1005 + CloseAbnormalClosure = 1006 + CloseInvalidFramePayloadData = 1007 + ClosePolicyViolation = 1008 + CloseMessageTooBig = 1009 + CloseMandatoryExtension = 1010 + CloseInternalServerErr = 1011 + CloseTLSHandshake = 1015 +) + +// The message types are defined in RFC 6455, section 11.8. +const ( + // TextMessage denotes a text data message. The text message payload is + // interpreted as UTF-8 encoded text data. + TextMessage = 1 + + // BinaryMessage denotes a binary data message. + BinaryMessage = 2 + + // CloseMessage denotes a close control message. The optional message + // payload contains a numeric code and text. Use the FormatCloseMessage + // function to format a close message payload. + CloseMessage = 8 + + // PingMessage denotes a ping control message. The optional message payload + // is UTF-8 encoded text. + PingMessage = 9 + + // PongMessage denotes a ping control message. The optional message payload + // is UTF-8 encoded text. + PongMessage = 10 +) + +// ErrCloseSent is returned when the application writes a message to the +// connection after sending a close message. +var ErrCloseSent = errors.New("websocket: close sent") + +// ErrReadLimit is returned when reading a message that is larger than the +// read limit set for the connection. +var ErrReadLimit = errors.New("websocket: read limit exceeded") + +// netError satisfies the net Error interface. +type netError struct { + msg string + temporary bool + timeout bool +} + +func (e *netError) Error() string { return e.msg } +func (e *netError) Temporary() bool { return e.temporary } +func (e *netError) Timeout() bool { return e.timeout } + +// CloseError represents close frame. +type CloseError struct { + + // Code is defined in RFC 6455, section 11.7. + Code int + + // Text is the optional text payload. + Text string +} + +func (e *CloseError) Error() string { + s := []byte("websocket: close ") + s = strconv.AppendInt(s, int64(e.Code), 10) + switch e.Code { + case CloseNormalClosure: + s = append(s, " (normal)"...) + case CloseGoingAway: + s = append(s, " (going away)"...) + case CloseProtocolError: + s = append(s, " (protocol error)"...) + case CloseUnsupportedData: + s = append(s, " (unsupported data)"...) + case CloseNoStatusReceived: + s = append(s, " (no status)"...) + case CloseAbnormalClosure: + s = append(s, " (abnormal closure)"...) + case CloseInvalidFramePayloadData: + s = append(s, " (invalid payload data)"...) + case ClosePolicyViolation: + s = append(s, " (policy violation)"...) + case CloseMessageTooBig: + s = append(s, " (message too big)"...) + case CloseMandatoryExtension: + s = append(s, " (mandatory extension missing)"...) + case CloseInternalServerErr: + s = append(s, " (internal server error)"...) + case CloseTLSHandshake: + s = append(s, " (TLS handshake error)"...) + } + if e.Text != "" { + s = append(s, ": "...) + s = append(s, e.Text...) + } + return string(s) +} + +// IsCloseError returns boolean indicating whether the error is a *CloseError +// with one of the specified codes. +func IsCloseError(err error, codes ...int) bool { + if e, ok := err.(*CloseError); ok { + for _, code := range codes { + if e.Code == code { + return true + } + } + } + return false +} + +// IsUnexpectedCloseError returns boolean indicating whether the error is a +// *CloseError with a code not in the list of expected codes. +func IsUnexpectedCloseError(err error, expectedCodes ...int) bool { + if e, ok := err.(*CloseError); ok { + for _, code := range expectedCodes { + if e.Code == code { + return false + } + } + return true + } + return false +} + +var ( + errWriteTimeout = &netError{msg: "websocket: write timeout", timeout: true, temporary: true} + errUnexpectedEOF = &CloseError{Code: CloseAbnormalClosure, Text: io.ErrUnexpectedEOF.Error()} + errBadWriteOpCode = errors.New("websocket: bad write message type") + errWriteClosed = errors.New("websocket: write closed") + errInvalidControlFrame = errors.New("websocket: invalid control frame") +) + +func hideTempErr(err error) error { + if e, ok := err.(net.Error); ok && e.Temporary() { + err = &netError{msg: e.Error(), timeout: e.Timeout()} + } + return err +} + +func isControl(frameType int) bool { + return frameType == CloseMessage || frameType == PingMessage || frameType == PongMessage +} + +func isData(frameType int) bool { + return frameType == TextMessage || frameType == BinaryMessage +} + +func maskBytes(key [4]byte, pos int, b []byte) int { + for i := range b { + b[i] ^= key[pos&3] + pos++ + } + return pos & 3 +} + +func newMaskKey() [4]byte { + n := rand.Uint32() + return [4]byte{byte(n), byte(n >> 8), byte(n >> 16), byte(n >> 24)} +} + +// Conn represents a WebSocket connection. +type Conn struct { + conn net.Conn + isServer bool + subprotocol string + + // Write fields + mu chan bool // used as mutex to protect write to conn and closeSent + closeSent bool // true if close message was sent + + // Message writer fields. + writeErr error + writeBuf []byte // frame is constructed in this buffer. + writePos int // end of data in writeBuf. + writeFrameType int // type of the current frame. + writeSeq int // incremented to invalidate message writers. + writeDeadline time.Time + isWriting bool // for best-effort concurrent write detection + + // Read fields + readErr error + br *bufio.Reader + readRemaining int64 // bytes remaining in current frame. + readFinal bool // true the current message has more frames. + readSeq int // incremented to invalidate message readers. + readLength int64 // Message size. + readLimit int64 // Maximum message size. + readMaskPos int + readMaskKey [4]byte + handlePong func(string) error + handlePing func(string) error + readErrCount int +} + +func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int) *Conn { + mu := make(chan bool, 1) + mu <- true + + if readBufferSize == 0 { + readBufferSize = defaultReadBufferSize + } + if writeBufferSize == 0 { + writeBufferSize = defaultWriteBufferSize + } + + c := &Conn{ + isServer: isServer, + br: bufio.NewReaderSize(conn, readBufferSize), + conn: conn, + mu: mu, + readFinal: true, + writeBuf: make([]byte, writeBufferSize+maxFrameHeaderSize), + writeFrameType: noFrame, + writePos: maxFrameHeaderSize, + } + c.SetPingHandler(nil) + c.SetPongHandler(nil) + return c +} + +// Subprotocol returns the negotiated protocol for the connection. +func (c *Conn) Subprotocol() string { + return c.subprotocol +} + +// Close closes the underlying network connection without sending or waiting for a close frame. +func (c *Conn) Close() error { + return c.conn.Close() +} + +// LocalAddr returns the local network address. +func (c *Conn) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +// RemoteAddr returns the remote network address. +func (c *Conn) RemoteAddr() net.Addr { + return c.conn.RemoteAddr() +} + +// Write methods + +func (c *Conn) write(frameType int, deadline time.Time, bufs ...[]byte) error { + <-c.mu + defer func() { c.mu <- true }() + + if c.closeSent { + return ErrCloseSent + } else if frameType == CloseMessage { + c.closeSent = true + } + + c.conn.SetWriteDeadline(deadline) + for _, buf := range bufs { + if len(buf) > 0 { + n, err := c.conn.Write(buf) + if n != len(buf) { + // Close on partial write. + c.conn.Close() + } + if err != nil { + return err + } + } + } + return nil +} + +// WriteControl writes a control message with the given deadline. The allowed +// message types are CloseMessage, PingMessage and PongMessage. +func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) error { + if !isControl(messageType) { + return errBadWriteOpCode + } + if len(data) > maxControlFramePayloadSize { + return errInvalidControlFrame + } + + b0 := byte(messageType) | finalBit + b1 := byte(len(data)) + if !c.isServer { + b1 |= maskBit + } + + buf := make([]byte, 0, maxFrameHeaderSize+maxControlFramePayloadSize) + buf = append(buf, b0, b1) + + if c.isServer { + buf = append(buf, data...) + } else { + key := newMaskKey() + buf = append(buf, key[:]...) + buf = append(buf, data...) + maskBytes(key, 0, buf[6:]) + } + + d := time.Hour * 1000 + if !deadline.IsZero() { + d = deadline.Sub(time.Now()) + if d < 0 { + return errWriteTimeout + } + } + + timer := time.NewTimer(d) + select { + case <-c.mu: + timer.Stop() + case <-timer.C: + return errWriteTimeout + } + defer func() { c.mu <- true }() + + if c.closeSent { + return ErrCloseSent + } else if messageType == CloseMessage { + c.closeSent = true + } + + c.conn.SetWriteDeadline(deadline) + n, err := c.conn.Write(buf) + if n != 0 && n != len(buf) { + c.conn.Close() + } + return hideTempErr(err) +} + +// NextWriter returns a writer for the next message to send. The writer's +// Close method flushes the complete message to the network. +// +// There can be at most one open writer on a connection. NextWriter closes the +// previous writer if the application has not already done so. +func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error) { + if c.writeErr != nil { + return nil, c.writeErr + } + + if c.writeFrameType != noFrame { + if err := c.flushFrame(true, nil); err != nil { + return nil, err + } + } + + if !isControl(messageType) && !isData(messageType) { + return nil, errBadWriteOpCode + } + + c.writeFrameType = messageType + return messageWriter{c, c.writeSeq}, nil +} + +func (c *Conn) flushFrame(final bool, extra []byte) error { + length := c.writePos - maxFrameHeaderSize + len(extra) + + // Check for invalid control frames. + if isControl(c.writeFrameType) && + (!final || length > maxControlFramePayloadSize) { + c.writeSeq++ + c.writeFrameType = noFrame + c.writePos = maxFrameHeaderSize + return errInvalidControlFrame + } + + b0 := byte(c.writeFrameType) + if final { + b0 |= finalBit + } + b1 := byte(0) + if !c.isServer { + b1 |= maskBit + } + + // Assume that the frame starts at beginning of c.writeBuf. + framePos := 0 + if c.isServer { + // Adjust up if mask not included in the header. + framePos = 4 + } + + switch { + case length >= 65536: + c.writeBuf[framePos] = b0 + c.writeBuf[framePos+1] = b1 | 127 + binary.BigEndian.PutUint64(c.writeBuf[framePos+2:], uint64(length)) + case length > 125: + framePos += 6 + c.writeBuf[framePos] = b0 + c.writeBuf[framePos+1] = b1 | 126 + binary.BigEndian.PutUint16(c.writeBuf[framePos+2:], uint16(length)) + default: + framePos += 8 + c.writeBuf[framePos] = b0 + c.writeBuf[framePos+1] = b1 | byte(length) + } + + if !c.isServer { + key := newMaskKey() + copy(c.writeBuf[maxFrameHeaderSize-4:], key[:]) + maskBytes(key, 0, c.writeBuf[maxFrameHeaderSize:c.writePos]) + if len(extra) > 0 { + c.writeErr = errors.New("websocket: internal error, extra used in client mode") + return c.writeErr + } + } + + // Write the buffers to the connection with best-effort detection of + // concurrent writes. See the concurrency section in the package + // documentation for more info. + + if c.isWriting { + panic("concurrent write to websocket connection") + } + c.isWriting = true + + c.writeErr = c.write(c.writeFrameType, c.writeDeadline, c.writeBuf[framePos:c.writePos], extra) + + if !c.isWriting { + panic("concurrent write to websocket connection") + } + c.isWriting = false + + // Setup for next frame. + c.writePos = maxFrameHeaderSize + c.writeFrameType = continuationFrame + if final { + c.writeSeq++ + c.writeFrameType = noFrame + } + return c.writeErr +} + +type messageWriter struct { + c *Conn + seq int +} + +func (w messageWriter) err() error { + c := w.c + if c.writeSeq != w.seq { + return errWriteClosed + } + if c.writeErr != nil { + return c.writeErr + } + return nil +} + +func (w messageWriter) ncopy(max int) (int, error) { + n := len(w.c.writeBuf) - w.c.writePos + if n <= 0 { + if err := w.c.flushFrame(false, nil); err != nil { + return 0, err + } + n = len(w.c.writeBuf) - w.c.writePos + } + if n > max { + n = max + } + return n, nil +} + +func (w messageWriter) write(final bool, p []byte) (int, error) { + if err := w.err(); err != nil { + return 0, err + } + + if len(p) > 2*len(w.c.writeBuf) && w.c.isServer { + // Don't buffer large messages. + err := w.c.flushFrame(final, p) + if err != nil { + return 0, err + } + return len(p), nil + } + + nn := len(p) + for len(p) > 0 { + n, err := w.ncopy(len(p)) + if err != nil { + return 0, err + } + copy(w.c.writeBuf[w.c.writePos:], p[:n]) + w.c.writePos += n + p = p[n:] + } + return nn, nil +} + +func (w messageWriter) Write(p []byte) (int, error) { + return w.write(false, p) +} + +func (w messageWriter) WriteString(p string) (int, error) { + if err := w.err(); err != nil { + return 0, err + } + + nn := len(p) + for len(p) > 0 { + n, err := w.ncopy(len(p)) + if err != nil { + return 0, err + } + copy(w.c.writeBuf[w.c.writePos:], p[:n]) + w.c.writePos += n + p = p[n:] + } + return nn, nil +} + +func (w messageWriter) ReadFrom(r io.Reader) (nn int64, err error) { + if err := w.err(); err != nil { + return 0, err + } + for { + if w.c.writePos == len(w.c.writeBuf) { + err = w.c.flushFrame(false, nil) + if err != nil { + break + } + } + var n int + n, err = r.Read(w.c.writeBuf[w.c.writePos:]) + w.c.writePos += n + nn += int64(n) + if err != nil { + if err == io.EOF { + err = nil + } + break + } + } + return nn, err +} + +func (w messageWriter) Close() error { + if err := w.err(); err != nil { + return err + } + return w.c.flushFrame(true, nil) +} + +// WriteMessage is a helper method for getting a writer using NextWriter, +// writing the message and closing the writer. +func (c *Conn) WriteMessage(messageType int, data []byte) error { + wr, err := c.NextWriter(messageType) + if err != nil { + return err + } + w := wr.(messageWriter) + if _, err := w.write(true, data); err != nil { + return err + } + if c.writeSeq == w.seq { + if err := c.flushFrame(true, nil); err != nil { + return err + } + } + return nil +} + +// SetWriteDeadline sets the write deadline on the underlying network +// connection. After a write has timed out, the websocket state is corrupt and +// all future writes will return an error. A zero value for t means writes will +// not time out. +func (c *Conn) SetWriteDeadline(t time.Time) error { + c.writeDeadline = t + return nil +} + +// Read methods + +// readFull is like io.ReadFull except that io.EOF is never returned. +func (c *Conn) readFull(p []byte) (err error) { + var n int + for n < len(p) && err == nil { + var nn int + nn, err = c.br.Read(p[n:]) + n += nn + } + if n == len(p) { + err = nil + } else if err == io.EOF { + err = errUnexpectedEOF + } + return +} + +func (c *Conn) advanceFrame() (int, error) { + + // 1. Skip remainder of previous frame. + + if c.readRemaining > 0 { + if _, err := io.CopyN(ioutil.Discard, c.br, c.readRemaining); err != nil { + return noFrame, err + } + } + + // 2. Read and parse first two bytes of frame header. + + var b [8]byte + if err := c.readFull(b[:2]); err != nil { + return noFrame, err + } + + final := b[0]&finalBit != 0 + frameType := int(b[0] & 0xf) + reserved := int((b[0] >> 4) & 0x7) + mask := b[1]&maskBit != 0 + c.readRemaining = int64(b[1] & 0x7f) + + if reserved != 0 { + return noFrame, c.handleProtocolError("unexpected reserved bits " + strconv.Itoa(reserved)) + } + + switch frameType { + case CloseMessage, PingMessage, PongMessage: + if c.readRemaining > maxControlFramePayloadSize { + return noFrame, c.handleProtocolError("control frame length > 125") + } + if !final { + return noFrame, c.handleProtocolError("control frame not final") + } + case TextMessage, BinaryMessage: + if !c.readFinal { + return noFrame, c.handleProtocolError("message start before final message frame") + } + c.readFinal = final + case continuationFrame: + if c.readFinal { + return noFrame, c.handleProtocolError("continuation after final message frame") + } + c.readFinal = final + default: + return noFrame, c.handleProtocolError("unknown opcode " + strconv.Itoa(frameType)) + } + + // 3. Read and parse frame length. + + switch c.readRemaining { + case 126: + if err := c.readFull(b[:2]); err != nil { + return noFrame, err + } + c.readRemaining = int64(binary.BigEndian.Uint16(b[:2])) + case 127: + if err := c.readFull(b[:8]); err != nil { + return noFrame, err + } + c.readRemaining = int64(binary.BigEndian.Uint64(b[:8])) + } + + // 4. Handle frame masking. + + if mask != c.isServer { + return noFrame, c.handleProtocolError("incorrect mask flag") + } + + if mask { + c.readMaskPos = 0 + if err := c.readFull(c.readMaskKey[:]); err != nil { + return noFrame, err + } + } + + // 5. For text and binary messages, enforce read limit and return. + + if frameType == continuationFrame || frameType == TextMessage || frameType == BinaryMessage { + + c.readLength += c.readRemaining + if c.readLimit > 0 && c.readLength > c.readLimit { + c.WriteControl(CloseMessage, FormatCloseMessage(CloseMessageTooBig, ""), time.Now().Add(writeWait)) + return noFrame, ErrReadLimit + } + + return frameType, nil + } + + // 6. Read control frame payload. + + var payload []byte + if c.readRemaining > 0 { + payload = make([]byte, c.readRemaining) + c.readRemaining = 0 + if err := c.readFull(payload); err != nil { + return noFrame, err + } + if c.isServer { + maskBytes(c.readMaskKey, 0, payload) + } + } + + // 7. Process control frame payload. + + switch frameType { + case PongMessage: + if err := c.handlePong(string(payload)); err != nil { + return noFrame, err + } + case PingMessage: + if err := c.handlePing(string(payload)); err != nil { + return noFrame, err + } + case CloseMessage: + echoMessage := []byte{} + closeCode := CloseNoStatusReceived + closeText := "" + if len(payload) >= 2 { + echoMessage = payload[:2] + closeCode = int(binary.BigEndian.Uint16(payload)) + closeText = string(payload[2:]) + } + c.WriteControl(CloseMessage, echoMessage, time.Now().Add(writeWait)) + return noFrame, &CloseError{Code: closeCode, Text: closeText} + } + + return frameType, nil +} + +func (c *Conn) handleProtocolError(message string) error { + c.WriteControl(CloseMessage, FormatCloseMessage(CloseProtocolError, message), time.Now().Add(writeWait)) + return errors.New("websocket: " + message) +} + +// NextReader returns the next data message received from the peer. The +// returned messageType is either TextMessage or BinaryMessage. +// +// There can be at most one open reader on a connection. NextReader discards +// the previous message if the application has not already consumed it. +// +// Applications must break out of the application's read loop when this method +// returns a non-nil error value. Errors returned from this method are +// permanent. Once this method returns a non-nil error, all subsequent calls to +// this method return the same error. +func (c *Conn) NextReader() (messageType int, r io.Reader, err error) { + + c.readSeq++ + c.readLength = 0 + + for c.readErr == nil { + frameType, err := c.advanceFrame() + if err != nil { + c.readErr = hideTempErr(err) + break + } + if frameType == TextMessage || frameType == BinaryMessage { + return frameType, messageReader{c, c.readSeq}, nil + } + } + + // Applications that do handle the error returned from this method spin in + // tight loop on connection failure. To help application developers detect + // this error, panic on repeated reads to the failed connection. + c.readErrCount++ + if c.readErrCount >= 1000 { + panic("repeated read on failed websocket connection") + } + + return noFrame, nil, c.readErr +} + +type messageReader struct { + c *Conn + seq int +} + +func (r messageReader) Read(b []byte) (int, error) { + + if r.seq != r.c.readSeq { + return 0, io.EOF + } + + for r.c.readErr == nil { + + if r.c.readRemaining > 0 { + if int64(len(b)) > r.c.readRemaining { + b = b[:r.c.readRemaining] + } + n, err := r.c.br.Read(b) + r.c.readErr = hideTempErr(err) + if r.c.isServer { + r.c.readMaskPos = maskBytes(r.c.readMaskKey, r.c.readMaskPos, b[:n]) + } + r.c.readRemaining -= int64(n) + return n, r.c.readErr + } + + if r.c.readFinal { + r.c.readSeq++ + return 0, io.EOF + } + + frameType, err := r.c.advanceFrame() + switch { + case err != nil: + r.c.readErr = hideTempErr(err) + case frameType == TextMessage || frameType == BinaryMessage: + r.c.readErr = errors.New("websocket: internal error, unexpected text or binary in Reader") + } + } + + err := r.c.readErr + if err == io.EOF && r.seq == r.c.readSeq { + err = errUnexpectedEOF + } + return 0, err +} + +// ReadMessage is a helper method for getting a reader using NextReader and +// reading from that reader to a buffer. +func (c *Conn) ReadMessage() (messageType int, p []byte, err error) { + var r io.Reader + messageType, r, err = c.NextReader() + if err != nil { + return messageType, nil, err + } + p, err = ioutil.ReadAll(r) + return messageType, p, err +} + +// SetReadDeadline sets the read deadline on the underlying network connection. +// After a read has timed out, the websocket connection state is corrupt and +// all future reads will return an error. A zero value for t means reads will +// not time out. +func (c *Conn) SetReadDeadline(t time.Time) error { + return c.conn.SetReadDeadline(t) +} + +// SetReadLimit sets the maximum size for a message read from the peer. If a +// message exceeds the limit, the connection sends a close frame to the peer +// and returns ErrReadLimit to the application. +func (c *Conn) SetReadLimit(limit int64) { + c.readLimit = limit +} + +// SetPingHandler sets the handler for ping messages received from the peer. +// The appData argument to h is the PING frame application data. The default +// ping handler sends a pong to the peer. +func (c *Conn) SetPingHandler(h func(appData string) error) { + if h == nil { + h = func(message string) error { + err := c.WriteControl(PongMessage, []byte(message), time.Now().Add(writeWait)) + if err == ErrCloseSent { + return nil + } else if e, ok := err.(net.Error); ok && e.Temporary() { + return nil + } + return err + } + } + c.handlePing = h +} + +// SetPongHandler sets the handler for pong messages received from the peer. +// The appData argument to h is the PONG frame application data. The default +// pong handler does nothing. +func (c *Conn) SetPongHandler(h func(appData string) error) { + if h == nil { + h = func(string) error { return nil } + } + c.handlePong = h +} + +// UnderlyingConn returns the internal net.Conn. This can be used to further +// modifications to connection specific flags. +func (c *Conn) UnderlyingConn() net.Conn { + return c.conn +} + +// FormatCloseMessage formats closeCode and text as a WebSocket close message. +func FormatCloseMessage(closeCode int, text string) []byte { + buf := make([]byte, 2+len(text)) + binary.BigEndian.PutUint16(buf, uint16(closeCode)) + copy(buf[2:], text) + return buf +} diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/doc.go b/Godeps/_workspace/src/github.com/gorilla/websocket/doc.go new file mode 100644 index 00000000000..499b03dbd9a --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/doc.go @@ -0,0 +1,148 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package websocket implements the WebSocket protocol defined in RFC 6455. +// +// Overview +// +// The Conn type represents a WebSocket connection. A server application uses +// the Upgrade function from an Upgrader object with a HTTP request handler +// to get a pointer to a Conn: +// +// var upgrader = websocket.Upgrader{ +// ReadBufferSize: 1024, +// WriteBufferSize: 1024, +// } +// +// func handler(w http.ResponseWriter, r *http.Request) { +// conn, err := upgrader.Upgrade(w, r, nil) +// if err != nil { +// log.Println(err) +// return +// } +// ... Use conn to send and receive messages. +// } +// +// Call the connection's WriteMessage and ReadMessage methods to send and +// receive messages as a slice of bytes. This snippet of code shows how to echo +// messages using these methods: +// +// for { +// messageType, p, err := conn.ReadMessage() +// if err != nil { +// return +// } +// if err = conn.WriteMessage(messageType, p); err != nil { +// return err +// } +// } +// +// In above snippet of code, p is a []byte and messageType is an int with value +// websocket.BinaryMessage or websocket.TextMessage. +// +// An application can also send and receive messages using the io.WriteCloser +// and io.Reader interfaces. To send a message, call the connection NextWriter +// method to get an io.WriteCloser, write the message to the writer and close +// the writer when done. To receive a message, call the connection NextReader +// method to get an io.Reader and read until io.EOF is returned. This snippet +// shows how to echo messages using the NextWriter and NextReader methods: +// +// for { +// messageType, r, err := conn.NextReader() +// if err != nil { +// return +// } +// w, err := conn.NextWriter(messageType) +// if err != nil { +// return err +// } +// if _, err := io.Copy(w, r); err != nil { +// return err +// } +// if err := w.Close(); err != nil { +// return err +// } +// } +// +// Data Messages +// +// The WebSocket protocol distinguishes between text and binary data messages. +// Text messages are interpreted as UTF-8 encoded text. The interpretation of +// binary messages is left to the application. +// +// This package uses the TextMessage and BinaryMessage integer constants to +// identify the two data message types. The ReadMessage and NextReader methods +// return the type of the received message. The messageType argument to the +// WriteMessage and NextWriter methods specifies the type of a sent message. +// +// It is the application's responsibility to ensure that text messages are +// valid UTF-8 encoded text. +// +// Control Messages +// +// The WebSocket protocol defines three types of control messages: close, ping +// and pong. Call the connection WriteControl, WriteMessage or NextWriter +// methods to send a control message to the peer. +// +// Connections handle received ping and pong messages by invoking callback +// functions set with SetPingHandler and SetPongHandler methods. The default +// ping handler sends a pong to the client. The callback functions can be +// invoked from the NextReader, ReadMessage or the message Read method. +// +// Connections handle received close messages by sending a close message to the +// peer and returning a *CloseError from the the NextReader, ReadMessage or the +// message Read method. +// +// The application must read the connection to process ping and close messages +// sent from the peer. If the application is not otherwise interested in +// messages from the peer, then the application should start a goroutine to +// read and discard messages from the peer. A simple example is: +// +// func readLoop(c *websocket.Conn) { +// for { +// if _, _, err := c.NextReader(); err != nil { +// c.Close() +// break +// } +// } +// } +// +// Concurrency +// +// Connections support one concurrent reader and one concurrent writer. +// +// Applications are responsible for ensuring that no more than one goroutine +// calls the write methods (NextWriter, SetWriteDeadline, WriteMessage, +// WriteJSON) concurrently and that no more than one goroutine calls the read +// methods (NextReader, SetReadDeadline, ReadMessage, ReadJSON, SetPongHandler, +// SetPingHandler) concurrently. +// +// The Close and WriteControl methods can be called concurrently with all other +// methods. +// +// Origin Considerations +// +// Web browsers allow Javascript applications to open a WebSocket connection to +// any host. It's up to the server to enforce an origin policy using the Origin +// request header sent by the browser. +// +// The Upgrader calls the function specified in the CheckOrigin field to check +// the origin. If the CheckOrigin function returns false, then the Upgrade +// method fails the WebSocket handshake with HTTP status 403. +// +// If the CheckOrigin field is nil, then the Upgrader uses a safe default: fail +// the handshake if the Origin request header is present and not equal to the +// Host request header. +// +// An application can allow connections from any origin by specifying a +// function that always returns true: +// +// var upgrader = websocket.Upgrader{ +// CheckOrigin: func(r *http.Request) bool { return true }, +// } +// +// The deprecated Upgrade function does not enforce an origin policy. It's the +// application's responsibility to check the Origin header before calling +// Upgrade. +package websocket diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/json.go b/Godeps/_workspace/src/github.com/gorilla/websocket/json.go new file mode 100644 index 00000000000..4f0e36875a5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/json.go @@ -0,0 +1,55 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "encoding/json" + "io" +) + +// WriteJSON is deprecated, use c.WriteJSON instead. +func WriteJSON(c *Conn, v interface{}) error { + return c.WriteJSON(v) +} + +// WriteJSON writes the JSON encoding of v to the connection. +// +// See the documentation for encoding/json Marshal for details about the +// conversion of Go values to JSON. +func (c *Conn) WriteJSON(v interface{}) error { + w, err := c.NextWriter(TextMessage) + if err != nil { + return err + } + err1 := json.NewEncoder(w).Encode(v) + err2 := w.Close() + if err1 != nil { + return err1 + } + return err2 +} + +// ReadJSON is deprecated, use c.ReadJSON instead. +func ReadJSON(c *Conn, v interface{}) error { + return c.ReadJSON(v) +} + +// ReadJSON reads the next JSON-encoded message from the connection and stores +// it in the value pointed to by v. +// +// See the documentation for the encoding/json Unmarshal function for details +// about the conversion of JSON to a Go value. +func (c *Conn) ReadJSON(v interface{}) error { + _, r, err := c.NextReader() + if err != nil { + return err + } + err = json.NewDecoder(r).Decode(v) + if err == io.EOF { + // One value is expected in the message. + err = io.ErrUnexpectedEOF + } + return err +} diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/server.go b/Godeps/_workspace/src/github.com/gorilla/websocket/server.go new file mode 100644 index 00000000000..85616c79743 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/server.go @@ -0,0 +1,253 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bufio" + "errors" + "net" + "net/http" + "net/url" + "strings" + "time" +) + +// HandshakeError describes an error with the handshake from the peer. +type HandshakeError struct { + message string +} + +func (e HandshakeError) Error() string { return e.message } + +// Upgrader specifies parameters for upgrading an HTTP connection to a +// WebSocket connection. +type Upgrader struct { + // HandshakeTimeout specifies the duration for the handshake to complete. + HandshakeTimeout time.Duration + + // ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer + // size is zero, then a default value of 4096 is used. The I/O buffer sizes + // do not limit the size of the messages that can be sent or received. + ReadBufferSize, WriteBufferSize int + + // Subprotocols specifies the server's supported protocols in order of + // preference. If this field is set, then the Upgrade method negotiates a + // subprotocol by selecting the first match in this list with a protocol + // requested by the client. + Subprotocols []string + + // Error specifies the function for generating HTTP error responses. If Error + // is nil, then http.Error is used to generate the HTTP response. + Error func(w http.ResponseWriter, r *http.Request, status int, reason error) + + // CheckOrigin returns true if the request Origin header is acceptable. If + // CheckOrigin is nil, the host in the Origin header must not be set or + // must match the host of the request. + CheckOrigin func(r *http.Request) bool +} + +func (u *Upgrader) returnError(w http.ResponseWriter, r *http.Request, status int, reason string) (*Conn, error) { + err := HandshakeError{reason} + if u.Error != nil { + u.Error(w, r, status, err) + } else { + http.Error(w, http.StatusText(status), status) + } + return nil, err +} + +// checkSameOrigin returns true if the origin is not set or is equal to the request host. +func checkSameOrigin(r *http.Request) bool { + origin := r.Header["Origin"] + if len(origin) == 0 { + return true + } + u, err := url.Parse(origin[0]) + if err != nil { + return false + } + return u.Host == r.Host +} + +func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header) string { + if u.Subprotocols != nil { + clientProtocols := Subprotocols(r) + for _, serverProtocol := range u.Subprotocols { + for _, clientProtocol := range clientProtocols { + if clientProtocol == serverProtocol { + return clientProtocol + } + } + } + } else if responseHeader != nil { + return responseHeader.Get("Sec-Websocket-Protocol") + } + return "" +} + +// Upgrade upgrades the HTTP server connection to the WebSocket protocol. +// +// The responseHeader is included in the response to the client's upgrade +// request. Use the responseHeader to specify cookies (Set-Cookie) and the +// application negotiated subprotocol (Sec-Websocket-Protocol). +// +// If the upgrade fails, then Upgrade replies to the client with an HTTP error +// response. +func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) { + if r.Method != "GET" { + return u.returnError(w, r, http.StatusMethodNotAllowed, "websocket: method not GET") + } + if values := r.Header["Sec-Websocket-Version"]; len(values) == 0 || values[0] != "13" { + return u.returnError(w, r, http.StatusBadRequest, "websocket: version != 13") + } + + if !tokenListContainsValue(r.Header, "Connection", "upgrade") { + return u.returnError(w, r, http.StatusBadRequest, "websocket: could not find connection header with token 'upgrade'") + } + + if !tokenListContainsValue(r.Header, "Upgrade", "websocket") { + return u.returnError(w, r, http.StatusBadRequest, "websocket: could not find upgrade header with token 'websocket'") + } + + checkOrigin := u.CheckOrigin + if checkOrigin == nil { + checkOrigin = checkSameOrigin + } + if !checkOrigin(r) { + return u.returnError(w, r, http.StatusForbidden, "websocket: origin not allowed") + } + + challengeKey := r.Header.Get("Sec-Websocket-Key") + if challengeKey == "" { + return u.returnError(w, r, http.StatusBadRequest, "websocket: key missing or blank") + } + + subprotocol := u.selectSubprotocol(r, responseHeader) + + var ( + netConn net.Conn + br *bufio.Reader + err error + ) + + h, ok := w.(http.Hijacker) + if !ok { + return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker") + } + var rw *bufio.ReadWriter + netConn, rw, err = h.Hijack() + if err != nil { + return u.returnError(w, r, http.StatusInternalServerError, err.Error()) + } + br = rw.Reader + + if br.Buffered() > 0 { + netConn.Close() + return nil, errors.New("websocket: client sent data before handshake is complete") + } + + c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize) + c.subprotocol = subprotocol + + p := c.writeBuf[:0] + p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...) + p = append(p, computeAcceptKey(challengeKey)...) + p = append(p, "\r\n"...) + if c.subprotocol != "" { + p = append(p, "Sec-Websocket-Protocol: "...) + p = append(p, c.subprotocol...) + p = append(p, "\r\n"...) + } + for k, vs := range responseHeader { + if k == "Sec-Websocket-Protocol" { + continue + } + for _, v := range vs { + p = append(p, k...) + p = append(p, ": "...) + for i := 0; i < len(v); i++ { + b := v[i] + if b <= 31 { + // prevent response splitting. + b = ' ' + } + p = append(p, b) + } + p = append(p, "\r\n"...) + } + } + p = append(p, "\r\n"...) + + // Clear deadlines set by HTTP server. + netConn.SetDeadline(time.Time{}) + + if u.HandshakeTimeout > 0 { + netConn.SetWriteDeadline(time.Now().Add(u.HandshakeTimeout)) + } + if _, err = netConn.Write(p); err != nil { + netConn.Close() + return nil, err + } + if u.HandshakeTimeout > 0 { + netConn.SetWriteDeadline(time.Time{}) + } + + return c, nil +} + +// Upgrade upgrades the HTTP server connection to the WebSocket protocol. +// +// This function is deprecated, use websocket.Upgrader instead. +// +// The application is responsible for checking the request origin before +// calling Upgrade. An example implementation of the same origin policy is: +// +// if req.Header.Get("Origin") != "http://"+req.Host { +// http.Error(w, "Origin not allowed", 403) +// return +// } +// +// If the endpoint supports subprotocols, then the application is responsible +// for negotiating the protocol used on the connection. Use the Subprotocols() +// function to get the subprotocols requested by the client. Use the +// Sec-Websocket-Protocol response header to specify the subprotocol selected +// by the application. +// +// The responseHeader is included in the response to the client's upgrade +// request. Use the responseHeader to specify cookies (Set-Cookie) and the +// negotiated subprotocol (Sec-Websocket-Protocol). +// +// The connection buffers IO to the underlying network connection. The +// readBufSize and writeBufSize parameters specify the size of the buffers to +// use. Messages can be larger than the buffers. +// +// If the request is not a valid WebSocket handshake, then Upgrade returns an +// error of type HandshakeError. Applications should handle this error by +// replying to the client with an HTTP error response. +func Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header, readBufSize, writeBufSize int) (*Conn, error) { + u := Upgrader{ReadBufferSize: readBufSize, WriteBufferSize: writeBufSize} + u.Error = func(w http.ResponseWriter, r *http.Request, status int, reason error) { + // don't return errors to maintain backwards compatibility + } + u.CheckOrigin = func(r *http.Request) bool { + // allow all connections by default + return true + } + return u.Upgrade(w, r, responseHeader) +} + +// Subprotocols returns the subprotocols requested by the client in the +// Sec-Websocket-Protocol header. +func Subprotocols(r *http.Request) []string { + h := strings.TrimSpace(r.Header.Get("Sec-Websocket-Protocol")) + if h == "" { + return nil + } + protocols := strings.Split(h, ",") + for i := range protocols { + protocols[i] = strings.TrimSpace(protocols[i]) + } + return protocols +} diff --git a/Godeps/_workspace/src/github.com/gorilla/websocket/util.go b/Godeps/_workspace/src/github.com/gorilla/websocket/util.go new file mode 100644 index 00000000000..ffdc265ed78 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gorilla/websocket/util.go @@ -0,0 +1,44 @@ +// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "crypto/rand" + "crypto/sha1" + "encoding/base64" + "io" + "net/http" + "strings" +) + +// tokenListContainsValue returns true if the 1#token header with the given +// name contains token. +func tokenListContainsValue(header http.Header, name string, value string) bool { + for _, v := range header[name] { + for _, s := range strings.Split(v, ",") { + if strings.EqualFold(value, strings.TrimSpace(s)) { + return true + } + } + } + return false +} + +var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11") + +func computeAcceptKey(challengeKey string) string { + h := sha1.New() + h.Write([]byte(challengeKey)) + h.Write(keyGUID) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} + +func generateChallengeKey() (string, error) { + p := make([]byte, 16) + if _, err := io.ReadFull(rand.Reader, p); err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(p), nil +} diff --git a/Gruntfile.js b/Gruntfile.js index 70defdeaf6d..9a0c69b96f9 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -18,6 +18,7 @@ module.exports = function (grunt) { } config.pkg.version = grunt.option('pkgVer') || config.pkg.version; + console.log('Version', config.pkg.version); // load plugins require('load-grunt-tasks')(grunt); diff --git a/README.md b/README.md index a3336e0373b..931055349cf 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ -[Grafana](http://grafana.org) [![Circle CI](https://circleci.com/gh/grafana/grafana.svg?style=svg)](https://circleci.com/gh/grafana/grafana) [![Coverage Status](https://coveralls.io/repos/grafana/grafana/badge.png)](https://coveralls.io/r/grafana/grafana) [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/grafana/grafana?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[Grafana](http://grafana.org) [![Circle CI](https://circleci.com/gh/grafana/grafana.svg?style=svg)](https://circleci.com/gh/grafana/grafana) [![Coverage Status](https://coveralls.io/repos/grafana/grafana/badge.png)](https://coveralls.io/r/grafana/grafana) ================ [Website](http://grafana.org) | [Twitter](https://twitter.com/grafana) | [IRC](https://webchat.freenode.net/?channels=grafana) | +![](https://brandfolder.com/api/favicon/icon?size=16&domain=www.slack.com) +[Slack](http://slack.raintank.io) | [Email](mailto:contact@grafana.org) Grafana is an open source, feature rich metrics dashboard and graph editor for @@ -77,6 +79,7 @@ the latest master builds [here](http://grafana.org/download/builds) - Go 1.5 - NodeJS +- [Godep](https://github.com/tools/godep) ### Get Code diff --git a/appveyor.yml b/appveyor.yml index 83506312912..7d84bafc148 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,7 +5,7 @@ os: Windows Server 2012 R2 clone_folder: c:\gopath\src\github.com\grafana\grafana environment: - nodejs_version: "4" + nodejs_version: "5" GOPATH: c:\gopath install: diff --git a/bower.json b/bower.json index 9e0b307c80c..557ee9c9f8f 100644 --- a/bower.json +++ b/bower.json @@ -14,10 +14,10 @@ ], "dependencies": { "jquery": "~2.1.4", - "angular": "~1.5.1", - "angular-route": "~1.5.1", - "angular-mocks": "~1.5.1", - "angular-sanitize": "~1.5.1", + "angular": "~1.5.3", + "angular-route": "~1.5.3", + "angular-mocks": "~1.5.3", + "angular-sanitize": "~1.5.3", "angular-bindonce": "~0.3.3" } } diff --git a/build.go b/build.go index 9a945a2b07d..93b9e6db087 100644 --- a/build.go +++ b/build.go @@ -73,8 +73,7 @@ func main() { grunt("test") case "package": - //verifyGitRepoIsClean() - grunt("release") + grunt("release", fmt.Sprintf("--pkgVer=%v-%v", linuxPackageVersion, linuxPackageIteration)) createLinuxPackages() case "pkg-rpm": @@ -100,12 +99,12 @@ func main() { func makeLatestDistCopies() { rpmIteration := "-1" if linuxPackageIteration != "" { - rpmIteration = "-" + linuxPackageIteration + rpmIteration = linuxPackageIteration } - runError("cp", "dist/grafana_"+version+"_amd64.deb", "dist/grafana_latest_amd64.deb") - runError("cp", "dist/grafana-"+linuxPackageVersion+rpmIteration+".x86_64.rpm", "dist/grafana-latest-1.x86_64.rpm") - runError("cp", "dist/grafana-"+version+".linux-x64.tar.gz", "dist/grafana-latest.linux-x64.tar.gz") + runError("cp", fmt.Sprintf("dist/grafana_%v-%v_amd64.deb", linuxPackageVersion, linuxPackageIteration), "dist/grafana_latest_amd64.deb") + runError("cp", fmt.Sprintf("dist/grafana-%v-%v.x86_64.rpm", linuxPackageVersion, rpmIteration), "dist/grafana-latest-1.x86_64.rpm") + runError("cp", fmt.Sprintf("dist/grafana-%v-%v.linux-x64.tar.gz", linuxPackageVersion, linuxPackageIteration), "dist/grafana-latest.linux-x64.tar.gz") } func readVersionFromPackageJson() { @@ -133,6 +132,11 @@ func readVersionFromPackageJson() { if len(parts) > 1 { linuxPackageVersion = parts[0] linuxPackageIteration = parts[1] + if linuxPackageIteration != "" { + // add timestamp to iteration + linuxPackageIteration = fmt.Sprintf("%s%v", linuxPackageIteration, time.Now().Unix()) + } + log.Println(fmt.Sprintf("teration %v", linuxPackageIteration)) } } diff --git a/conf/defaults.ini b/conf/defaults.ini index 851c88efc7c..6f63891d1ee 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -111,6 +111,13 @@ gc_interval_time = 86400 # Change this option to false to disable reporting. reporting_enabled = true +# Set to false to disable all checks to https://grafana.net +# for new vesions (grafana itself and plugins), check is used +# in some UI views to notify that grafana or plugin update exists +# This option does not cause any auto updates, nor send any information +# only a GET request to http://grafana.net to get latest versions +check_for_updates = true + # Google Analytics universal tracking code, only enabled if you specify an id here google_analytics_ua_id = diff --git a/conf/ldap.toml b/conf/ldap.toml index aa8a9679d68..395179e219f 100644 --- a/conf/ldap.toml +++ b/conf/ldap.toml @@ -28,8 +28,31 @@ search_base_dns = ["dc=grafana,dc=org"] # This is done by enabling group_search_filter below. You must also set member_of= "cn" # in [servers.attributes] below. +# Users with nested/recursive group membership and an LDAP server that supports LDAP_MATCHING_RULE_IN_CHAIN +# can set group_search_filter, group_search_filter_user_attribute, group_search_base_dns and member_of +# below in such a way that the user's recursive group membership is considered. +# +# Nested Groups + Active Directory (AD) Example: +# +# AD groups store the Distinguished Names (DNs) of members, so your filter must +# recursively search your groups for the authenticating user's DN. For example: +# +# group_search_filter = "(member:1.2.840.113556.1.4.1941:=%s)" +# group_search_filter_user_attribute = "distinguishedName" +# group_search_base_dns = ["ou=groups,dc=grafana,dc=org"] +# +# [servers.attributes] +# ... +# member_of = "distinguishedName" + ## Group search filter, to retrieve the groups of which the user is a member (only set if memberOf attribute is not available) # group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))" +## Group search filter user attribute defines what user attribute gets substituted for %s in group_search_filter. +## Defaults to the value of username in [server.attributes] +## Valid options are any of your values in [servers.attributes] +## If you are using nested groups you probably want to set this and member_of in +## [servers.attributes] to "distinguishedName" +# group_search_filter_user_attribute = "distinguishedName" ## An array of the base DNs to search through for groups. Typically uses ou=groups # group_search_base_dns = ["ou=groups,dc=grafana,dc=org"] diff --git a/conf/sample.ini b/conf/sample.ini index b875cbda093..4ab923c1376 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -100,6 +100,13 @@ # Change this option to false to disable reporting. ;reporting_enabled = true +# Set to false to disable all checks to https://grafana.net +# for new vesions (grafana itself and plugins), check is used +# in some UI views to notify that grafana or plugin update exists +# This option does not cause any auto updates, nor send any information +# only a GET request to http://grafana.net to get latest versions +check_for_updates = true + # Google Analytics universal tracking code, only enabled if you specify an id here ;google_analytics_ua_id = diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index b527fa8f046..46b0b964b59 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -86,12 +86,12 @@ pages: - ['http_api/snapshot.md', 'API', 'Snapshot API'] - ['http_api/other.md', 'API', 'Other API'] -- ['plugins/overview.md', 'Plugins', 'Overview'] +- ['plugins/index.md', 'Plugins', 'Overview'] - ['plugins/installation.md', 'Plugins', 'Installation'] -- ['plugins/datasources.md', 'Plugins', 'Datasource plugins'] -- ['plugins/panels.md', 'Plugins', 'Panel plugins'] -- ['plugins/development.md', 'Plugins', 'Plugin development'] -- ['plugins/plugin.json.md', 'Plugins', 'Plugin json'] +- ['plugins/development.md', 'Plugins', 'Development'] +- ['plugins/apps.md', 'Plugins', 'Apps'] +- ['plugins/datasources.md', 'Plugins', 'Datasources'] +- ['plugins/panels.md', 'Plugins', 'Panels'] - ['tutorials/index.md', 'Tutorials', 'Tutorials'] - ['tutorials/hubot_howto.md', 'Tutorials', 'How To integrate Hubot and Grafana'] diff --git a/docs/sources/datasources/cloudwatch.md b/docs/sources/datasources/cloudwatch.md index 351b08eb2aa..c69d3579784 100644 --- a/docs/sources/datasources/cloudwatch.md +++ b/docs/sources/datasources/cloudwatch.md @@ -69,8 +69,22 @@ Name | Description For details about the metrics CloudWatch provides, please refer to the [CloudWatch documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/CW_Support_For_AWS.html). -The `ec2_instance_attribute` query take `filters` in JSON format. -You can specify [pre-defined filters of ec2:DescribeInstances](http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html). +## Example templated Queries + +Example dimension queries which will return list of resources for individual AWS Services: + +Service | Query +------- | ----- +EBS | `dimension_values(us-east-1,AWS/ELB,RequestCount,LoadBalancerName)` +ElastiCache | `dimension_values(us-east-1,AWS/ElastiCache,CPUUtilization,CacheClusterId)` +RedShift | `dimension_values(us-east-1,AWS/Redshift,CPUUtilization,ClusterIdentifier)` +RDS | `dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier)` +S3 | `dimension_values(us-east-1,AWS/S3,BucketSizeBytes,BucketName)` + +## ec2_instance_attribute JSON filters + +The `ec2_instance_attribute` query take `filters` in JSON format. +You can specify [pre-defined filters of ec2:DescribeInstances](http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html). Specify like `{ filter_name1: [ filter_value1 ], filter_name2: [ filter_value2 ] }` Example `ec2_instance_attribute()` query diff --git a/docs/sources/http_api/data_source.md b/docs/sources/http_api/data_source.md index 2a3c20e9af8..2a8f79f71f5 100644 --- a/docs/sources/http_api/data_source.md +++ b/docs/sources/http_api/data_source.md @@ -207,35 +207,6 @@ page_keywords: grafana, admin, http, api, documentation, datasource {"message":"Data source deleted"} -## Available data source types - -`GET /api/datasources/plugins` - -**Example Request**: - - GET /api/datasources/plugins HTTP/1.1 - Accept: application/json - Content-Type: application/json - Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - -**Example Response**: - - HTTP/1.1 200 - Content-Type: application/json - - { - "grafana":{ - "metrics":true,"module":"plugins/datasource/grafana/datasource", - "name":"Grafana (for testing)", - "partials":{ - "query":"app/plugins/datasource/grafana/partials/query.editor.html" - }, - "pluginType":"datasource", - "serviceName":"GrafanaDatasource", - "type":"grafana" - } - } - ## Data source proxy calls `GET /api/datasources/proxy/:datasourceId/*` diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index b5c72f3182c..231b4a601ac 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -10,14 +10,21 @@ page_keywords: grafana, installation, debian, ubuntu, guide Description | Download ------------ | ------------- -.deb for Debian-based Linux | [grafana_2.6.0_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_2.6.0_amd64.deb) +Stable .deb for Debian-based Linux | [grafana_2.6.0_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_2.6.0_amd64.deb) +Beta .deb for Debian-based Linux | [grafana_3.0.0-beta51460725904_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.0-beta51460725904_amd64.deb) -## Install +## Install Stable $ wget https://grafanarel.s3.amazonaws.com/builds/grafana_2.6.0_amd64.deb $ sudo apt-get install -y adduser libfontconfig $ sudo dpkg -i grafana_2.6.0_amd64.deb +## Install 3.0 Beta + + $ wget https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.0-beta51460725904_amd64.deb + $ sudo apt-get install -y adduser libfontconfig + $ sudo dpkg -i grafana_3.0.0-beta51460725904_amd64.deb + ## APT Repository Add the following line to your `/etc/apt/sources.list` file. diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index ec4f648e44b..d981a5c4b2c 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -10,9 +10,10 @@ page_keywords: grafana, installation, centos, fedora, opensuse, redhat, guide Description | Download ------------ | ------------- -.RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [grafana-2.6.0-1.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-2.6.0-1.x86_64.rpm) +Stable .RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [grafana-2.6.0-1.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-2.6.0-1.x86_64.rpm) +Beta .RPM for CentOS / Fedor / OpenSuse / Redhat Linux | [grafana-3.0.0-beta51460725904.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.0-beta51460725904§.x86_64.rpm) -## Install from package file +## Install Stable Release from package file You can install Grafana using Yum directly. @@ -29,6 +30,24 @@ Or install manually using `rpm`. $ sudo rpm -i --nodeps grafana-2.6.0-1.x86_64.rpm +## Install Beta Release from package file + +You can install Grafana using Yum directly. + + $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.0-beta51460725904.x86_64.rpm + +Or install manually using `rpm`. + +#### On CentOS / Fedora / Redhat: + + $ sudo yum install initscripts fontconfig + $ sudo rpm -Uvh grafana-3.0.0-beta51460725904.x86_64.rpm + +#### On OpenSuse: + + $ sudo rpm -i --nodeps grafana-3.0.0-beta51460725904.x86_64.rpm + + ## Install via YUM Repository Add the following to a new file at `/etc/yum.repos.d/grafana.repo` diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md index 1bc2b5a2b92..1d6c5fc76cd 100644 --- a/docs/sources/installation/windows.md +++ b/docs/sources/installation/windows.md @@ -10,7 +10,7 @@ page_keywords: grafana, installation, windows guide Description | Download ------------ | ------------- -Zip package for Windows | [grafana.2.5.0.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-2.5.0.windows-x64.zip) +Stable Zip package for Windows | [grafana.2.6.0.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-2.5.0.windows-x64.zip) ## Configure diff --git a/docs/sources/plugins/app.md b/docs/sources/plugins/app.md deleted file mode 100644 index 250af25c57d..00000000000 --- a/docs/sources/plugins/app.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -page_title: App plugin -page_description: App plugin for Grafana -page_keywords: grafana, plugins, documentation ---- - - > Our goal is not to have a very extensive documentation but rather have actual code that people can look at. An example implementation of an app can be found in this [example app repo](https://github.com/grafana/example-app) - -# Apps - -App plugins is a new kind of grafana plugin that can bundle datasource and panel plugins within one package. It also enable the plugin author to create custom pages within grafana. The custom pages enables the plugin author to include things like documentation, sign up forms or controlling other services using HTTP requests. - -Datasource and panel plugins will show up like normal plugins. The app pages will be available in the main menu. - - - -## Enabling app plugins -After installing an app it have to be enabled before it show up as an datasource or panel. You can do that on the app page in the config tab. - -## README.md - -The readme file in the mounted folder will show up in the overview tab on the app page. - -## Module exports -```javascript -export { - ExampleAppConfigCtrl as ConfigCtrl, - StreamPageCtrl, - LogsPageCtrl -}; -``` -The only required export is the ConfigCtrl. Both StreamPageCtrl and LogsPageCtrl are custom pages defined in plugin.json - -## Custom pages -Custom pages are defined in the plugin.json like this. -```json -"pages": [ - { "name": "Live stream", "component": "StreamPageCtrl", "role": "Editor"}, - { "name": "Log view", "component": "LogsPageCtrl", "role": "Viewer"} -] -``` -The component field have to match one of the components exported in the module.js in the root of the plugin. - -## Bundled plugins - -When Grafana starts it will scan all directories within an app plugin and load folders containing a plugin.json as an plugin. diff --git a/docs/sources/plugins/apps.md b/docs/sources/plugins/apps.md new file mode 100644 index 00000000000..74038a9feb9 --- /dev/null +++ b/docs/sources/plugins/apps.md @@ -0,0 +1,24 @@ +--- +page_title: App plugin +page_description: App plugin for Grafana +page_keywords: grafana, plugins, documentation +--- + + +# Apps + +App plugins is a new kind of grafana plugin that can bundle datasource and panel plugins within one package. It also enable the plugin author to create custom pages within grafana. The custom pages enables the plugin author to include things like documentation, sign up forms or controlling other services using HTTP requests. + +Datasource and panel plugins will show up like normal plugins. The app pages will be available in the main menu. + + + +## Enabling app plugins +After installing an app it have to be enabled before it show up as an datasource or panel. You can do that on the app page in the config tab. + +### Develop your own App + +> Our goal is not to have a very extensive documentation but rather have actual +> code that people can look at. An example implementation of an app can be found +> in this [example app repo](https://github.com/grafana/example-app) + diff --git a/docs/sources/plugins/datasources.md b/docs/sources/plugins/datasources.md index c44cd842844..3732948d527 100644 --- a/docs/sources/plugins/datasources.md +++ b/docs/sources/plugins/datasources.md @@ -4,11 +4,18 @@ page_description: Datasource plugins for Grafana page_keywords: grafana, plugins, documentation --- - > Our goal is not to have a very extensive documentation but rather have actual code that people can look at. An example implementation of a datasource can be found in this [example datasource repo](https://github.com/grafana/simple-json-datasource) # Datasources -Datasource plugins enables people to develop plugins for any database that communicates over http. Its up to the plugin to transform the data into time series data so that any grafana panel can then show it. +Datasource plugins enables people to develop plugins for any database that +communicates over http. Its up to the plugin to transform the data into +time series data so that any grafana panel can then show it. + +## Datasource development + +> Our goal is not to have a very extensive documentation but rather have actual +> code that people can look at. An example implementation of a datasource can be +> found in this [example datasource repo](https://github.com/grafana/simple-json-datasource) To interact with the rest of grafana the plugins module file can export 5 different components. @@ -19,11 +26,14 @@ To interact with the rest of grafana the plugins module file can export 5 differ - AnnotationsQueryCtrl ## Plugin json + There are two datasource specific settings for the plugin.json + ```javascript "metrics": true, "annotations": false, ``` + These settings indicates what kind of data the plugin can deliver. At least one of them have to be true ## Datasource diff --git a/docs/sources/plugins/developing_plugins.md b/docs/sources/plugins/developing_plugins.md deleted file mode 100644 index 91d1ff3f84e..00000000000 --- a/docs/sources/plugins/developing_plugins.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -page_title: Plugin development -page_description: Plugin development for Grafana -page_keywords: grafana, plugins, documentation, development ---- - -# Plugin development - -From grafana 3.0 it's very easy to develop your own plugins and share them with other grafana users. - -## What languages? - -Since everything turns into javascript its up to you to choose which language you want. That said its proberbly a good idea to choose es6 or typescript since we use es6 classes in Grafana. - -##Buildscript - -You can use any buildsystem you like that support systemjs. All the built content should endup in a folder named dist and commited to the repository. - -##Loading plugins -The easiset way to try your plugin with grafana is to [setup grafana for development](https://github.com/grafana/grafana/blob/master/DEVELOPMENT.md) and place your plugin in the /data/plugins folder in grafana. When grafana starts it will scan that folder for folders that contains a plugin.json file and mount them as plugins. If your plugin folder contains a folder named dist it will mount that folder instead of the plugin base folder. - -## Examples / boilerplate -We currently have three different examples that you can fork to get started developing your grafana plugin. - - - [simple-json-datasource](https://github.com/grafana/simple-json-datasource) (small datasource plugin for quering json data from backends) - - [panel-boilderplate-es5](https://github.com/grafana/grafana/tree/master/examples/panel-boilerplate-es5) - - [example-app](https://github.com/grafana/example-app) - diff --git a/docs/sources/plugins/development.md b/docs/sources/plugins/development.md new file mode 100644 index 00000000000..29dcd0568e3 --- /dev/null +++ b/docs/sources/plugins/development.md @@ -0,0 +1,52 @@ +--- +page_title: Plugin development guide +page_description: Plugin development for Grafana +page_keywords: grafana, plugins, documentation, development +--- + +# Plugin development + +From grafana 3.0 it's very easy to develop your own plugins and share them with other grafana users. + +## Short version + +1. [Setup grafana](http://docs.grafana.org/project/building_from_source/) +2. Clone an example plugin into ```/var/lib/grafana/plugins``` or `data/plugins` (relative to grafana git repo if your running development version from source dir) +3. Code away! + +## What languages? + +Since everything turns into javascript it's up to you to choose which language you want. That said it's probably a good idea to choose es6 or typescript since we use es6 classes in Grafana. So it's easier to get inspiration from the Grafana repo is you choose one of those languages. + +## Buildscript + +You can use any build system you like that support systemjs. All the built content should end up in a folder named ```dist``` and committed to the repository.By committing the dist folder the person who installs your plugin does not have to run any buildscript. + +All our example plugins have build scripted configured. + +## module.(js|ts) + +This is the entry point for every plugin. This is the place where you should export +your plugin implementation. Depending on what kind of plugin you are developing you +will be expected to export different things. You can find what's expected for [datasource](./datasources.md), [panels](./panels.md) +and [apps](./apps.md) plugins in the documentation. + +## Start developing your plugin +There are two ways that you can start developing a Grafana plugin. + +1. Setup a Grafana development environment. [(described here)](http://docs.grafana.org/project/building_from_source/) and place your plugin in the ```data/plugins``` folder. +2. Install Grafana and place your plugin in the plugins directory which is set in your [config file](../installation/configuration.md). By default this is `/var/lib/grafana/plugins` on Linux systems. +3. Place your plugin directory anywhere you like and specify it grafana.ini. + +We encourage people to setup the full Grafana environment so that you can get inspiration from the rest of grafana code base. + +When Grafana starts it will scan the plugin folders and mount every folder that contains a plugin.json file unless +the folder contains a subfolder named dist. In that case grafana will mount the dist folder instead. +This makes it possible to have both built and src content in the same plugin git repo. + +## Examples +We currently have three different examples that you can fork/download to get started developing your grafana plugin. + + - [simple-json-datasource](https://github.com/grafana/simple-json-datasource) (small datasource plugin for querying json data from backends) + - [piechart-panel](https://github.com/grafana/piechart-panel) + - [example-app](https://github.com/grafana/example-app) diff --git a/docs/sources/plugins/index.md b/docs/sources/plugins/index.md new file mode 100644 index 00000000000..08654bfb946 --- /dev/null +++ b/docs/sources/plugins/index.md @@ -0,0 +1,21 @@ +--- +page_title: Plugin overview +page_description: Plugins for Grafana +page_keywords: grafana, plugins, documentation +--- + +# Plugins + +From Grafana 3.0 not only datasource plugins are supported but also panel plugins and apps. +Having panels as plugins make it easy to create and add any kind of panel, to show your data +or improve your favorite dashboards. Apps is something new in Grafana that enables +bundling of datasources, panels, dashboards and Grafana pages into a cohesive experience. + +Grafana already have a strong community of contributors and plugin developers. +By making it easier to develop and install plugins we hope that the community +can grow even stronger and develop new plugins that we would never think about. + +You can discover available plugins on [Grafana.net](http://grafana.net) + + + diff --git a/docs/sources/plugins/installation.md b/docs/sources/plugins/installation.md index d3a8013ce2b..c83a54bdce3 100644 --- a/docs/sources/plugins/installation.md +++ b/docs/sources/plugins/installation.md @@ -4,43 +4,41 @@ page_description: Plugin installation for Grafana page_keywords: grafana, plugins, documentation --- -# Plugins - -## Installing plugins +# Installing plugins The easiest way to install plugins is by using the CLI tool grafana-cli which is bundled with grafana. Before any modification take place after modifying plugins, grafana-server needs to be restarted. ### Grafana plugin directory -On Linux systems the grafana-cli will assume that the grafana plugin directory is "/var/lib/grafana/plugins". It's possible to override the directory which grafana-cli will operate on by specifing the --path flag. On Windows systems this parameter have to be specified for every call. +On Linux systems the grafana-cli will assume that the grafana plugin directory is `/var/lib/grafana/plugins`. It's possible to override the directory which grafana-cli will operate on by specifying the --path flag. On Windows systems this parameter have to be specified for every call. ### Grafana-cli commands List available plugins ``` -grafana-cli list-remote +grafana-cli plugins list-remote ``` Install a plugin type ``` -grafana-cli install +grafana-cli plugins install ``` List installed plugins ``` -grafana-cli ls +grafana-cli plugins ls ``` -Upgrade all installed plugins +Update all installed plugins ``` -grafana-cli upgrade-all +grafana-cli plugins update-all ``` -Upgrade one plugin +Update one plugin ``` -grafana-cli upgrade +grafana-cli plugins update ``` Remove one plugin ``` -grafana-cli remove +grafana-cli plugins remove ``` diff --git a/docs/sources/plugins/overview.md b/docs/sources/plugins/overview.md deleted file mode 100644 index 6d0864ac8f5..00000000000 --- a/docs/sources/plugins/overview.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -page_title: Plugin overview -page_description: Plugins for Grafana -page_keywords: grafana, plugins, documentation ---- - -# Plugins - -From Grafana 3.0 not only datasource plugins are supported but also panel plugins and apps. Having panels as plugins make it easy to create and add any kind of panel, to show your data or improve your favorite dashboards. Apps is something new in Grafana that enables bundling of datasources, panels that belongs together. - -Grafana already have a strong community of contributors and plugin developers. By making it easier to develop and install plugins we hope that the community can grow even stronger and develop new plugins that we would never think about. - diff --git a/docs/sources/plugins/panels.md b/docs/sources/plugins/panels.md index ad9d5db66d8..179af7451c5 100644 --- a/docs/sources/plugins/panels.md +++ b/docs/sources/plugins/panels.md @@ -4,26 +4,15 @@ page_description: Panel plugins for Grafana page_keywords: grafana, plugins, documentation --- - > Our goal is not to have a very extensive documentation but rather have actual code that people can look at. An example implementation of a datasource can be found in the grafana repo under /examples/panel-boilerplate-es5 # Panels -To interact with the rest of grafana the panel plugin need to export a class in the module.js. -This class have to inherit from sdk.PanelCtrl or sdk.MetricsPanelCtrl and be exported as PanelCtrl. +Panels are the main building blocks of dashboards. -```javascript - return { - PanelCtrl: BoilerPlatePanelCtrl - }; -``` +## Panel development -This class will be instantiated once for every panel of its kind in a dashboard and treated as an AngularJs controller. +Examples -## MetricsPanelCtrl or PanelCtrl - -MetricsPanelCtrl inherits from PanelCtrl and adds some common features for datasource usage. So if your Panel will be working with a datasource you should inherit from MetricsPanelCtrl. If don't need to access any datasource then you should inherit from PanelCtrl instead. - -## Implementing a MetricsPanelCtrl - -If you choose to inherit from MetricsPanelCtrl you should implement a function called refreshData that will take a datasource as in parameter when its time to get new data. Its recommended that the refreshData function calls the issueQueries in the base class but its not mandatory. An examples of such implementation can be found in our [example panel](https://github.com/grafana/grafana/blob/master/examples/panel-boilerplate-es5/module.js#L27-L38) +- [clock-panel](https://github.com/grafana/clock-panel) +- [singlestat-panel](https://github.com/grafana/grafana/blob/master/public/app/plugins/panel/singlestat/module.ts) diff --git a/docs/sources/versions.html_fragment b/docs/sources/versions.html_fragment index d699cc6b6ac..0d62ee1e461 100644 --- a/docs/sources/versions.html_fragment +++ b/docs/sources/versions.html_fragment @@ -1,3 +1,4 @@ +
  • Version v3.0
  • Version v2.6
  • Version v2.5
  • Version v2.1
  • diff --git a/karma.conf.js b/karma.conf.js index 6d6d4583a0f..c803dda5eae 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -8,10 +8,7 @@ module.exports = function(config) { // list of files / patterns to load in the browser files: [ - 'vendor/npm/es5-shim/es5-shim.js', - 'vendor/npm/es5-shim/es5-sham.js', 'vendor/npm/es6-shim/es6-shim.js', - 'vendor/npm/es6-promise/dist/es6-promise.js', 'vendor/npm/systemjs/dist/system.src.js', 'test/test-main.js', @@ -27,9 +24,10 @@ module.exports = function(config) { logLevel: config.LOG_INFO, autoWatch: true, browsers: ['PhantomJS'], - captureTimeout: 2000, + captureTimeout: 20000, singleRun: true, - autoWatchBatchDelay: 1000, + autoWatchBatchDelay: 10000, + browserNoActivityTimeout: 60000, }); diff --git a/latest.json b/latest.json index 79eb42a8527..9354661781a 100644 --- a/latest.json +++ b/latest.json @@ -1,3 +1,4 @@ { - "version": "2.1.1" + "stable": "2.6.0", + "testing": "3.0.0-beta5" } diff --git a/package.json b/package.json index 887c41650d2..f454a42ba7a 100644 --- a/package.json +++ b/package.json @@ -4,16 +4,16 @@ "company": "Coding Instinct AB" }, "name": "grafana", - "version": "3.0.0-pre1", + "version": "3.0.0-beta6", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git" }, "devDependencies": { - "angular2": "2.0.0-beta.0", + "zone.js": "^0.6.6", "autoprefixer": "^6.3.3", "es6-promise": "^3.0.2", - "es6-shim": "^0.33.3", + "es6-shim": "^0.35.0", "expect.js": "~0.2.0", "glob": "~3.2.7", "grunt": "~0.4.0", @@ -31,32 +31,31 @@ "grunt-contrib-watch": "^0.6.1", "grunt-filerev": "^0.2.1", "grunt-git-describe": "~2.3.2", - "grunt-karma": "~0.12.1", + "grunt-karma": "~0.12.2", "grunt-ng-annotate": "^1.0.1", "grunt-notify": "^0.4.3", "grunt-postcss": "^0.8.0", "grunt-sass": "^1.1.0", "grunt-string-replace": "~1.2.1", - "grunt-systemjs-builder": "^0.2.5", + "grunt-systemjs-builder": "^0.2.6", "grunt-tslint": "^3.0.2", "grunt-typescript": "^0.8.0", "grunt-usemin": "3.0.0", "jshint-stylish": "~2.1.0", - "karma": "~0.13.15", + "karma": "0.13.22", "karma-chrome-launcher": "~0.2.2", "karma-coverage": "0.5.3", "karma-coveralls": "1.1.2", "karma-expect": "~1.1.0", "karma-mocha": "~0.2.1", - "karma-phantomjs-launcher": "0.2.1", + "karma-phantomjs-launcher": "1.0.0", "load-grunt-tasks": "3.4.0", "mocha": "2.3.4", - "phantomjs": "~2.1.3", + "phantomjs-prebuilt": "^2.1.3", "reflect-metadata": "0.1.2", - "rxjs": "5.0.0-beta.0", + "rxjs": "5.0.0-beta.4", "sass-lint": "^1.5.0", - "systemjs": "0.19.20", - "zone.js": "0.5.10" + "systemjs": "0.19.24" }, "engines": { "node": "0.4.x", @@ -68,7 +67,7 @@ }, "license": "Apache-2.0", "dependencies": { - "es5-shim": "^4.4.1", + "eventemitter3": "^1.2.0", "grunt-jscs": "~1.5.x", "grunt-sass-lint": "^0.1.0", "grunt-sync": "^0.4.1", @@ -76,7 +75,7 @@ "lodash": "^2.4.1", "remarkable": "^1.6.2", "sinon": "1.16.1", - "systemjs-builder": "^0.15.7", + "systemjs-builder": "^0.15.13", "tether": "^1.2.0", "tether-drop": "^1.4.2", "tslint": "^3.4.0", diff --git a/packaging/publish/publish.sh b/packaging/publish/publish.sh index 1d039ae1028..79303707231 100755 --- a/packaging/publish/publish.sh +++ b/packaging/publish/publish.sh @@ -1,17 +1,22 @@ #! /usr/bin/env bash -version=2.6.0 +deb_ver=3.0.0-beta51460725904 +rpm_ver=3.0.0-beta51460725904 -wget https://grafanarel.s3.amazonaws.com/builds/grafana_${version}_amd64.deb +#rpm_ver=3.0.0-1 -package_cloud push grafana/stable/debian/jessie grafana_${version}_amd64.deb -package_cloud push grafana/stable/debian/wheezy grafana_${version}_amd64.deb -package_cloud push grafana/testing/debian/jessie grafana_${version}_amd64.deb -package_cloud push grafana/testing/debian/wheezy grafana_${version}_amd64.deb +#wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb -wget https://grafanarel.s3.amazonaws.com/builds/grafana-${version}-1.x86_64.rpm +#package_cloud push grafana/stable/debian/jessie grafana_${deb_ver}_amd64.deb +#package_cloud push grafana/stable/debian/wheezy grafana_${deb_ver}_amd64.deb -package_cloud push grafana/testing/el/6 grafana-${version}-1.x86_64.rpm -package_cloud push grafana/testing/el/7 grafana-${version}-1.x86_64.rpm -package_cloud push grafana/stable/el/7 grafana-${version}-1.x86_64.rpm -package_cloud push grafana/stable/el/6 grafana-${version}-1.x86_64.rpm +#package_cloud push grafana/testing/debian/jessie grafana_${deb_ver}_amd64.deb +#package_cloud push grafana/testing/debian/wheezy grafana_${deb_ver}_amd64.deb + +#wget https://grafanarel.s3.amazonaws.com/builds/grafana-${rpm_ver}.x86_64.rpm + +#package_cloud push grafana/testing/el/6 grafana-${rpm_ver}.x86_64.rpm +package_cloud push grafana/testing/el/7 grafana-${rpm_ver}.x86_64.rpm + +# package_cloud push grafana/stable/el/7 grafana-${version}-1.x86_64.rpm +# package_cloud push grafana/stable/el/6 grafana-${version}-1.x86_64.rpm diff --git a/pkg/api/api.go b/pkg/api/api.go index 29f7863ff60..684633e0bcd 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -4,6 +4,7 @@ import ( "github.com/go-macaron/binding" "github.com/grafana/grafana/pkg/api/avatar" "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/api/live" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "gopkg.in/macaron.v1" @@ -28,6 +29,8 @@ func Register(r *macaron.Macaron) { // authed views r.Get("/profile/", reqSignedIn, Index) + r.Get("/profile/password", reqSignedIn, Index) + r.Get("/profile/switch-org/:id", reqSignedIn, ChangeActiveOrgAndRedirectToHome) r.Get("/org/", reqSignedIn, Index) r.Get("/org/new", reqSignedIn, Index) r.Get("/datasources/", reqSignedIn, Index) @@ -35,6 +38,7 @@ func Register(r *macaron.Macaron) { r.Get("/org/users/", reqSignedIn, Index) r.Get("/org/apikeys/", reqSignedIn, Index) r.Get("/dashboard/import/", reqSignedIn, Index) + r.Get("/admin", reqGrafanaAdmin, Index) r.Get("/admin/settings", reqGrafanaAdmin, Index) r.Get("/admin/users", reqGrafanaAdmin, Index) r.Get("/admin/users/create", reqGrafanaAdmin, Index) @@ -43,6 +47,8 @@ func Register(r *macaron.Macaron) { r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, Index) r.Get("/admin/stats", reqGrafanaAdmin, Index) + r.Get("/styleguide", reqSignedIn, Index) + r.Get("/plugins", reqSignedIn, Index) r.Get("/plugins/:id/edit", reqSignedIn, Index) r.Get("/plugins/:id/page/:page", reqSignedIn, Index) @@ -92,10 +98,15 @@ func Register(r *macaron.Macaron) { r.Put("/", bind(m.UpdateUserCommand{}), wrap(UpdateSignedInUser)) r.Post("/using/:id", wrap(UserSetUsingOrg)) r.Get("/orgs", wrap(GetSignedInUserOrgList)) + r.Post("/stars/dashboard/:id", wrap(StarDashboard)) r.Delete("/stars/dashboard/:id", wrap(UnstarDashboard)) + r.Put("/password", bind(m.ChangeUserPasswordCommand{}), wrap(ChangeUserPassword)) r.Get("/quotas", wrap(GetUserQuotas)) + + r.Get("/preferences", wrap(GetUserPreferences)) + r.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateUserPreferences)) }) // users (admin permission required) @@ -126,6 +137,9 @@ func Register(r *macaron.Macaron) { r.Post("/invites", quota("user"), bind(dtos.AddInviteForm{}), wrap(AddOrgInvite)) r.Patch("/invites/:code/revoke", wrap(RevokeInvite)) + // prefs + r.Get("/preferences", wrap(GetOrgPreferences)) + r.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateOrgPreferences)) }, reqOrgAdmin) // create new org @@ -160,6 +174,11 @@ func Register(r *macaron.Macaron) { r.Delete("/:id", wrap(DeleteApiKey)) }, reqOrgAdmin) + // Preferences + r.Group("/preferences", func() { + r.Post("/set-home-dash", bind(m.SavePreferencesCommand{}), wrap(SetHomeDashboard)) + }) + // Data sources r.Group("/datasources", func() { r.Get("/", GetDataSources) @@ -172,12 +191,12 @@ func Register(r *macaron.Macaron) { r.Get("/datasources/id/:name", wrap(GetDataSourceIdByName), reqSignedIn) - r.Group("/plugins", func() { - r.Get("/", wrap(GetPluginList)) + r.Get("/plugins", wrap(GetPluginList)) + r.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingById)) + r.Group("/plugins", func() { r.Get("/:pluginId/readme", wrap(GetPluginReadme)) r.Get("/:pluginId/dashboards/", wrap(GetPluginDashboards)) - r.Get("/:pluginId/settings", wrap(GetPluginSettingById)) r.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), wrap(UpdatePluginSetting)) }, reqOrgAdmin) @@ -234,11 +253,20 @@ func Register(r *macaron.Macaron) { // rendering r.Get("/render/*", reqSignedIn, RenderToPng) + // grafana.net proxy + r.Any("/api/gnet/*", reqSignedIn, ProxyGnetRequest) + // Gravatar service. avt := avatar.CacheServer() r.Get("/avatar/:hash", avt.ServeHTTP) + // Websocket + liveConn := live.New() + r.Any("/ws", liveConn.Serve) + + // streams + r.Post("/api/streams/push", reqSignedIn, bind(dtos.StreamMessage{}), liveConn.PushToStream) + InitAppPluginRoutes(r) - r.NotFound(NotFoundHandler) } diff --git a/pkg/api/avatar/avatar.go b/pkg/api/avatar/avatar.go index 79aebece349..41ce857db4f 100644 --- a/pkg/api/avatar/avatar.go +++ b/pkg/api/avatar/avatar.go @@ -116,6 +116,7 @@ func (this *service) ServeHTTP(w http.ResponseWriter, r *http.Request) { if avatar.Expired() { if err := avatar.Update(); err != nil { log.Trace("avatar update error: %v", err) + avatar = this.notFound } } diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 22f9e1e22a1..b55a1377bd8 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -159,6 +159,24 @@ func canEditDashboard(role m.RoleType) bool { } func GetHomeDashboard(c *middleware.Context) { + prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId} + if err := bus.Dispatch(&prefsQuery); err != nil { + c.JsonApiErr(500, "Failed to get preferences", err) + } + + if prefsQuery.Result.HomeDashboardId != 0 { + slugQuery := m.GetDashboardSlugByIdQuery{Id: prefsQuery.Result.HomeDashboardId} + err := bus.Dispatch(&slugQuery) + if err != nil { + c.JsonApiErr(500, "Failed to get slug from database", err) + return + } + + dashRedirect := dtos.DashboardRedirect{RedirectUri: "db/" + slugQuery.Result} + c.JSON(200, &dashRedirect) + return + } + filePath := path.Join(setting.StaticRootPath, "dashboards/home.json") file, err := os.Open(filePath) if err != nil { diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index 6e6b4394e2d..eb88045cd51 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -61,7 +61,7 @@ func NewReverseProxy(ds *m.DataSource, proxyPath string, targetUrl *url.URL) *ht req.Header.Del("Set-Cookie") } - return &httputil.ReverseProxy{Director: director} + return &httputil.ReverseProxy{Director: director, FlushInterval: time.Millisecond * 200} } func getDatasource(id int64, orgId int64) (*m.DataSource, error) { diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index f685dcc2520..63c2ec57b7a 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -40,7 +40,7 @@ func GetDataSources(c *middleware.Context) { if plugin, exists := plugins.DataSources[ds.Type]; exists { dsItem.TypeLogoUrl = plugin.Info.Logos.Small } else { - dsItem.TypeLogoUrl = "public/img/icn-datasources.svg" + dsItem.TypeLogoUrl = "public/img/icn-datasource.svg" } result = append(result, dsItem) diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 8c1f85c9bd1..8db36be2140 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -33,6 +33,7 @@ type CurrentUser struct { OrgRole m.RoleType `json:"orgRole"` IsGrafanaAdmin bool `json:"isGrafanaAdmin"` GravatarUrl string `json:"gravatarUrl"` + Timezone string `json:"timezone"` } type DashboardMeta struct { @@ -57,6 +58,10 @@ type DashboardFullWithMeta struct { Dashboard *simplejson.Json `json:"dashboard"` } +type DashboardRedirect struct { + RedirectUri string `json:"redirectUri"` +} + type DataSource struct { Id int64 `json:"id"` OrgId int64 `json:"orgId"` diff --git a/pkg/api/dtos/plugins.go b/pkg/api/dtos/plugins.go index 88683155006..fbdf8d4e0ea 100644 --- a/pkg/api/dtos/plugins.go +++ b/pkg/api/dtos/plugins.go @@ -3,27 +3,32 @@ package dtos import "github.com/grafana/grafana/pkg/plugins" type PluginSetting struct { - Name string `json:"name"` - Type string `json:"type"` - Id string `json:"id"` - Enabled bool `json:"enabled"` - Pinned bool `json:"pinned"` - Module string `json:"module"` - BaseUrl string `json:"baseUrl"` - Info *plugins.PluginInfo `json:"info"` - Pages []*plugins.AppPluginPage `json:"pages"` - Includes []*plugins.PluginInclude `json:"includes"` - Dependencies *plugins.PluginDependencies `json:"dependencies"` - JsonData map[string]interface{} `json:"jsonData"` + Name string `json:"name"` + Type string `json:"type"` + Id string `json:"id"` + Enabled bool `json:"enabled"` + Pinned bool `json:"pinned"` + Module string `json:"module"` + BaseUrl string `json:"baseUrl"` + Info *plugins.PluginInfo `json:"info"` + Includes []*plugins.PluginInclude `json:"includes"` + Dependencies *plugins.PluginDependencies `json:"dependencies"` + JsonData map[string]interface{} `json:"jsonData"` + DefaultNavUrl string `json:"defaultNavUrl"` + + LatestVersion string `json:"latestVersion"` + HasUpdate bool `json:"hasUpdate"` } type PluginListItem struct { - Name string `json:"name"` - Type string `json:"type"` - Id string `json:"id"` - Enabled bool `json:"enabled"` - Pinned bool `json:"pinned"` - Info *plugins.PluginInfo `json:"info"` + Name string `json:"name"` + Type string `json:"type"` + Id string `json:"id"` + Enabled bool `json:"enabled"` + Pinned bool `json:"pinned"` + Info *plugins.PluginInfo `json:"info"` + LatestVersion string `json:"latestVersion"` + HasUpdate bool `json:"hasUpdate"` } type PluginList []PluginListItem diff --git a/pkg/api/dtos/prefs.go b/pkg/api/dtos/prefs.go new file mode 100644 index 00000000000..97e20bb4011 --- /dev/null +++ b/pkg/api/dtos/prefs.go @@ -0,0 +1,13 @@ +package dtos + +type Prefs struct { + Theme string `json:"theme"` + HomeDashboardId int64 `json:"homeDashboardId"` + Timezone string `json:"timezone"` +} + +type UpdatePrefsCmd struct { + Theme string `json:"theme"` + HomeDashboardId int64 `json:"homeDashboardId"` + Timezone string `json:"timezone"` +} diff --git a/pkg/api/dtos/stream.go b/pkg/api/dtos/stream.go new file mode 100644 index 00000000000..026ff79c1e8 --- /dev/null +++ b/pkg/api/dtos/stream.go @@ -0,0 +1,13 @@ +package dtos + +import "encoding/json" + +type StreamMessage struct { + Stream string `json:"stream"` + Series []StreamMessageSeries `json:"series"` +} + +type StreamMessageSeries struct { + Name string `json:"name"` + Datapoints [][]json.Number `json:"datapoints"` +} diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index c84d7faccff..dd84f7827eb 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -59,7 +59,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro defaultDatasource = ds.Name } - if len(ds.JsonData.MustMap()) > 0 { + if ds.JsonData != nil { dsMap["jsonData"] = ds.JsonData } @@ -137,9 +137,11 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro "allowOrgCreate": (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin, "authProxyEnabled": setting.AuthProxyEnabled, "buildInfo": map[string]interface{}{ - "version": setting.BuildVersion, - "commit": setting.BuildCommit, - "buildstamp": setting.BuildStamp, + "version": setting.BuildVersion, + "commit": setting.BuildCommit, + "buildstamp": setting.BuildStamp, + "latestVersion": plugins.GrafanaLatestVersion, + "hasUpdate": plugins.GrafanaHasUpdate, }, } diff --git a/pkg/api/gnetproxy.go b/pkg/api/gnetproxy.go new file mode 100644 index 00000000000..6511afd39b7 --- /dev/null +++ b/pkg/api/gnetproxy.go @@ -0,0 +1,46 @@ +package api + +import ( + "crypto/tls" + "net" + "net/http" + "net/http/httputil" + "time" + + "github.com/grafana/grafana/pkg/middleware" + "github.com/grafana/grafana/pkg/util" +) + +var gNetProxyTransport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, +} + +func ReverseProxyGnetReq(proxyPath string) *httputil.ReverseProxy { + director := func(req *http.Request) { + req.URL.Scheme = "https" + req.URL.Host = "grafana.net" + req.Host = "grafana.net" + + req.URL.Path = util.JoinUrlFragments("https://grafana.net/api", proxyPath) + + // clear cookie headers + req.Header.Del("Cookie") + req.Header.Del("Set-Cookie") + } + + return &httputil.ReverseProxy{Director: director} +} + +func ProxyGnetRequest(c *middleware.Context) { + proxyPath := c.Params("*") + proxy := ReverseProxyGnetReq(proxyPath) + proxy.Transport = gNetProxyTransport + proxy.ServeHTTP(c.Resp, c.Req.Request) + c.Resp.Header().Del("Set-Cookie") +} diff --git a/pkg/api/index.go b/pkg/api/index.go index 691c50f04f4..575ea35cfaf 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -2,6 +2,7 @@ package api import ( "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" @@ -14,6 +15,12 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { return nil, err } + prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId} + if err := bus.Dispatch(&prefsQuery); err != nil { + return nil, err + } + prefs := prefsQuery.Result + var data = dtos.IndexViewData{ User: &dtos.CurrentUser{ Id: c.UserId, @@ -21,12 +28,13 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { Login: c.Login, Email: c.Email, Name: c.Name, - LightTheme: c.Theme == "light", OrgId: c.OrgId, OrgName: c.OrgName, OrgRole: c.OrgRole, GravatarUrl: dtos.GetGravatarUrl(c.Email), IsGrafanaAdmin: c.IsGrafanaAdmin, + LightTheme: prefs.Theme == "light", + Timezone: prefs.Timezone, }, Settings: settings, AppUrl: setting.AppUrl, @@ -56,8 +64,8 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR { dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Divider: true}) - dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "New", Url: setting.AppSubUrl + "/dashboard/new"}) - dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "Import", Url: setting.AppSubUrl + "/import/dashboard"}) + dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "New", Icon: "fa fa-plus", Url: setting.AppSubUrl + "/dashboard/new"}) + dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "Import", Icon: "fa fa-download", Url: setting.AppSubUrl + "/import/dashboard"}) } data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ @@ -88,22 +96,35 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { for _, plugin := range enabledPlugins.Apps { if plugin.Pinned { - pageLink := &dtos.NavLink{ + appLink := &dtos.NavLink{ Text: plugin.Name, - Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit", + Url: plugin.DefaultNavUrl, Img: plugin.Info.Logos.Small, } - for _, page := range plugin.Pages { - if !page.SuppressNav { - pageLink.Children = append(pageLink.Children, &dtos.NavLink{ - Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/page/" + page.Slug, - Text: page.Name, - }) + for _, include := range plugin.Includes { + if include.Type == "page" && include.AddToNav { + link := &dtos.NavLink{ + Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/page/" + include.Slug, + Text: include.Name, + } + appLink.Children = append(appLink.Children, link) + } + if include.Type == "dashboard" && include.AddToNav { + link := &dtos.NavLink{ + Url: setting.AppSubUrl + "/dashboard/db/" + include.Slug, + Text: include.Name, + } + appLink.Children = append(appLink.Children, link) } } - data.MainNavLinks = append(data.MainNavLinks, pageLink) + if c.OrgRole == m.ROLE_ADMIN { + appLink.Children = append(appLink.Children, &dtos.NavLink{Divider: true}) + appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "fa fa-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"}) + } + + data.MainNavLinks = append(data.MainNavLinks, appLink) } } @@ -113,10 +134,10 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { Icon: "fa fa-fw fa-cogs", Url: setting.AppSubUrl + "/admin", Children: []*dtos.NavLink{ - {Text: "Global Users", Icon: "fa fa-fw fa-cogs", Url: setting.AppSubUrl + "/admin/users"}, - {Text: "Global Orgs", Icon: "fa fa-fw fa-cogs", Url: setting.AppSubUrl + "/admin/orgs"}, - {Text: "Server Settings", Icon: "fa fa-fw fa-cogs", Url: setting.AppSubUrl + "/admin/settings"}, - {Text: "Server Stats", Icon: "fa-fw fa-cogs", Url: setting.AppSubUrl + "/admin/stats"}, + {Text: "Global Users", Url: setting.AppSubUrl + "/admin/users"}, + {Text: "Global Orgs", Url: setting.AppSubUrl + "/admin/orgs"}, + {Text: "Server Settings", Url: setting.AppSubUrl + "/admin/settings"}, + {Text: "Server Stats", Url: setting.AppSubUrl + "/admin/stats"}, }, }) } diff --git a/pkg/api/live/conn.go b/pkg/api/live/conn.go new file mode 100644 index 00000000000..d474fc48a1f --- /dev/null +++ b/pkg/api/live/conn.go @@ -0,0 +1,118 @@ +package live + +import ( + "net/http" + "time" + + "github.com/gorilla/websocket" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/log" +) + +const ( + // Time allowed to write a message to the peer. + writeWait = 10 * time.Second + + // Time allowed to read the next pong message from the peer. + pongWait = 60 * time.Second + + // Send pings to peer with this period. Must be less than pongWait. + pingPeriod = (pongWait * 9) / 10 + + // Maximum message size allowed from peer. + maxMessageSize = 512 +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +type connection struct { + ws *websocket.Conn + send chan []byte +} + +func newConnection(ws *websocket.Conn) *connection { + return &connection{ + send: make(chan []byte, 256), + ws: ws, + } +} + +func (c *connection) readPump() { + defer func() { + h.unregister <- c + c.ws.Close() + }() + c.ws.SetReadLimit(maxMessageSize) + c.ws.SetReadDeadline(time.Now().Add(pongWait)) + c.ws.SetPongHandler(func(string) error { c.ws.SetReadDeadline(time.Now().Add(pongWait)); return nil }) + for { + _, message, err := c.ws.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) { + log.Info("error: %v", err) + } + break + } + + c.handleMessage(message) + } +} + +func (c *connection) handleMessage(message []byte) { + json, err := simplejson.NewJson(message) + if err != nil { + log.Error(3, "Unreadable message on websocket channel:", err) + } + + msgType := json.Get("action").MustString() + streamName := json.Get("stream").MustString() + + if len(streamName) == 0 { + log.Error(3, "Not allowed to subscribe to empty stream name") + return + } + + switch msgType { + case "subscribe": + h.subChannel <- &streamSubscription{name: streamName, conn: c} + case "unsubscribe": + h.subChannel <- &streamSubscription{name: streamName, conn: c, remove: true} + } + +} + +func (c *connection) write(mt int, payload []byte) error { + c.ws.SetWriteDeadline(time.Now().Add(writeWait)) + return c.ws.WriteMessage(mt, payload) +} + +// writePump pumps messages from the hub to the websocket connection. +func (c *connection) writePump() { + ticker := time.NewTicker(pingPeriod) + defer func() { + ticker.Stop() + c.ws.Close() + }() + for { + select { + case message, ok := <-c.send: + if !ok { + c.write(websocket.CloseMessage, []byte{}) + return + } + if err := c.write(websocket.TextMessage, message); err != nil { + return + } + case <-ticker.C: + if err := c.write(websocket.PingMessage, []byte{}); err != nil { + return + } + } + } +} diff --git a/pkg/api/live/hub.go b/pkg/api/live/hub.go new file mode 100644 index 00000000000..736f848db2c --- /dev/null +++ b/pkg/api/live/hub.go @@ -0,0 +1,94 @@ +package live + +import ( + "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/log" +) + +type hub struct { + connections map[*connection]bool + streams map[string]map[*connection]bool + + register chan *connection + unregister chan *connection + streamChannel chan *dtos.StreamMessage + subChannel chan *streamSubscription +} + +type streamSubscription struct { + conn *connection + name string + remove bool +} + +var h = hub{ + connections: make(map[*connection]bool), + streams: make(map[string]map[*connection]bool), + register: make(chan *connection), + unregister: make(chan *connection), + streamChannel: make(chan *dtos.StreamMessage), + subChannel: make(chan *streamSubscription), +} + +func (h *hub) removeConnection() { + +} + +func (h *hub) run() { + for { + select { + case c := <-h.register: + h.connections[c] = true + log.Info("Live: New connection (Total count: %v)", len(h.connections)) + + case c := <-h.unregister: + if _, ok := h.connections[c]; ok { + log.Info("Live: Closing Connection (Total count: %v)", len(h.connections)) + delete(h.connections, c) + close(c.send) + } + // hand stream subscriptions + case sub := <-h.subChannel: + log.Info("Live: Subscribing to: %v, remove: %v", sub.name, sub.remove) + subscribers, exists := h.streams[sub.name] + + // handle unsubscribe + if exists && sub.remove { + delete(subscribers, sub.conn) + continue + } + + if !exists { + subscribers = make(map[*connection]bool) + h.streams[sub.name] = subscribers + } + subscribers[sub.conn] = true + + // handle stream messages + case message := <-h.streamChannel: + subscribers, exists := h.streams[message.Stream] + if !exists || len(subscribers) == 0 { + log.Info("Live: Message to stream without subscribers: %v", message.Stream) + continue + } + + messageBytes, _ := simplejson.NewFromAny(message).Encode() + for sub := range subscribers { + // check if channel is open + if _, ok := h.connections[sub]; !ok { + delete(subscribers, sub) + continue + } + + select { + case sub.send <- messageBytes: + default: + close(sub.send) + delete(h.connections, sub) + delete(subscribers, sub) + } + } + } + } +} diff --git a/pkg/api/live/live.go b/pkg/api/live/live.go new file mode 100644 index 00000000000..4a309740590 --- /dev/null +++ b/pkg/api/live/live.go @@ -0,0 +1,36 @@ +package live + +import ( + "net/http" + + "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/middleware" +) + +type LiveConn struct { +} + +func New() *LiveConn { + go h.run() + return &LiveConn{} +} + +func (lc *LiveConn) Serve(w http.ResponseWriter, r *http.Request) { + log.Info("Live: Upgrading to WebSocket") + + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Error(3, "Live: Failed to upgrade connection to WebSocket", err) + return + } + c := newConnection(ws) + h.register <- c + go c.writePump() + c.readPump() +} + +func (lc *LiveConn) PushToStream(c *middleware.Context, message dtos.StreamMessage) { + h.streamChannel <- &message + c.JsonOK("Message recevived") +} diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 56d0d99296c..6446f7fee33 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -14,6 +14,7 @@ func GetPluginList(c *middleware.Context) Response { typeFilter := c.Query("type") enabledFilter := c.Query("enabled") embeddedFilter := c.Query("embedded") + coreFilter := c.Query("core") pluginSettingsMap, err := plugins.GetPluginSettings(c.OrgId) @@ -28,16 +29,23 @@ func GetPluginList(c *middleware.Context) Response { continue } + // filter out core plugins + if coreFilter == "0" && pluginDef.IsCorePlugin { + continue + } + // filter on type if typeFilter != "" && typeFilter != pluginDef.Type { continue } listItem := dtos.PluginListItem{ - Id: pluginDef.Id, - Name: pluginDef.Name, - Type: pluginDef.Type, - Info: &pluginDef.Info, + Id: pluginDef.Id, + Name: pluginDef.Name, + Type: pluginDef.Type, + Info: &pluginDef.Info, + LatestVersion: pluginDef.GrafanaNetVersion, + HasUpdate: pluginDef.GrafanaNetHasUpdate, } if pluginSetting, exists := pluginSettingsMap[pluginDef.Id]; exists { @@ -72,18 +80,17 @@ func GetPluginSettingById(c *middleware.Context) Response { } else { dto := &dtos.PluginSetting{ - Type: def.Type, - Id: def.Id, - Name: def.Name, - Info: &def.Info, - Dependencies: &def.Dependencies, - Includes: def.Includes, - BaseUrl: def.BaseUrl, - Module: def.Module, - } - - if app, exists := plugins.Apps[pluginId]; exists { - dto.Pages = app.Pages + Type: def.Type, + Id: def.Id, + Name: def.Name, + Info: &def.Info, + Dependencies: &def.Dependencies, + Includes: def.Includes, + BaseUrl: def.BaseUrl, + Module: def.Module, + DefaultNavUrl: def.DefaultNavUrl, + LatestVersion: def.GrafanaNetVersion, + HasUpdate: def.GrafanaNetHasUpdate, } query := m.GetPluginSettingByIdQuery{PluginId: pluginId, OrgId: c.OrgId} diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go new file mode 100644 index 00000000000..795b8994470 --- /dev/null +++ b/pkg/api/preferences.go @@ -0,0 +1,73 @@ +package api + +import ( + "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/middleware" + m "github.com/grafana/grafana/pkg/models" +) + +// POST /api/preferences/set-home-dash +func SetHomeDashboard(c *middleware.Context, cmd m.SavePreferencesCommand) Response { + + cmd.UserId = c.UserId + cmd.OrgId = c.OrgId + + if err := bus.Dispatch(&cmd); err != nil { + return ApiError(500, "Failed to set home dashboard", err) + } + + return ApiSuccess("Home dashboard set") +} + +// GET /api/user/preferences +func GetUserPreferences(c *middleware.Context) Response { + return getPreferencesFor(c.OrgId, c.UserId) +} + +func getPreferencesFor(orgId int64, userId int64) Response { + prefsQuery := m.GetPreferencesQuery{UserId: userId, OrgId: orgId} + + if err := bus.Dispatch(&prefsQuery); err != nil { + return ApiError(500, "Failed to get preferences", err) + } + + dto := dtos.Prefs{ + Theme: prefsQuery.Result.Theme, + HomeDashboardId: prefsQuery.Result.HomeDashboardId, + Timezone: prefsQuery.Result.Timezone, + } + + return Json(200, &dto) +} + +// PUT /api/user/preferences +func UpdateUserPreferences(c *middleware.Context, dtoCmd dtos.UpdatePrefsCmd) Response { + return updatePreferencesFor(c.OrgId, c.UserId, &dtoCmd) +} + +func updatePreferencesFor(orgId int64, userId int64, dtoCmd *dtos.UpdatePrefsCmd) Response { + saveCmd := m.SavePreferencesCommand{ + UserId: userId, + OrgId: orgId, + Theme: dtoCmd.Theme, + Timezone: dtoCmd.Timezone, + HomeDashboardId: dtoCmd.HomeDashboardId, + } + + if err := bus.Dispatch(&saveCmd); err != nil { + return ApiError(500, "Failed to save preferences", err) + } + + return ApiSuccess("Preferences updated") +} + +// GET /api/org/preferences +func GetOrgPreferences(c *middleware.Context) Response { + return getPreferencesFor(c.OrgId, 0) +} + +// PUT /api/org/preferences +func UpdateOrgPreferences(c *middleware.Context, dtoCmd dtos.UpdatePrefsCmd) Response { + return updatePreferencesFor(c.OrgId, 0, &dtoCmd) +} diff --git a/pkg/api/search.go b/pkg/api/search.go index 4b253e32cb0..5ec95971033 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -1,10 +1,11 @@ package api import ( + "strconv" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/services/search" - "strconv" ) func Search(c *middleware.Context) { diff --git a/pkg/api/user.go b/pkg/api/user.go index 5af243eeb22..8f54feaf6a0 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -4,6 +4,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -109,6 +110,23 @@ func UserSetUsingOrg(c *middleware.Context) Response { return ApiSuccess("Active organization changed") } +// GET /profile/switch-org/:id +func ChangeActiveOrgAndRedirectToHome(c *middleware.Context) { + orgId := c.ParamsInt64(":id") + + if !validateUsingOrg(c.UserId, orgId) { + NotFoundHandler(c) + } + + cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgId} + + if err := bus.Dispatch(&cmd); err != nil { + NotFoundHandler(c) + } + + c.Redirect(setting.AppSubUrl + "/") +} + func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand) Response { userQuery := m.GetUserByIdQuery{Id: c.UserId} diff --git a/pkg/cmd/grafana-cli/commands/commands.go b/pkg/cmd/grafana-cli/commands/commands.go index f1b36c90ef2..b3821a47844 100644 --- a/pkg/cmd/grafana-cli/commands/commands.go +++ b/pkg/cmd/grafana-cli/commands/commands.go @@ -1,9 +1,10 @@ package commands import ( + "os" + "github.com/codegangsta/cli" "github.com/grafana/grafana/pkg/cmd/grafana-cli/log" - "os" ) func runCommand(command func(commandLine CommandLine) error) func(context *cli.Context) { @@ -22,30 +23,44 @@ func runCommand(command func(commandLine CommandLine) error) func(context *cli.C } } -var Commands = []cli.Command{ +var pluginCommands = []cli.Command{ { Name: "install", - Usage: "install ", + Usage: "install ", Action: runCommand(installCommand), }, { Name: "list-remote", Usage: "list remote available plugins", Action: runCommand(listremoteCommand), }, { - Name: "upgrade", - Usage: "upgrade ", - Action: runCommand(upgradeCommand), + Name: "update", + Usage: "update ", + Aliases: []string{"upgrade"}, + Action: runCommand(upgradeCommand), }, { - Name: "upgrade-all", - Usage: "upgrades all your installed plugins", - Action: runCommand(upgradeAllCommand), + Name: "update-all", + Aliases: []string{"upgrade-all"}, + Usage: "update all your installed plugins", + Action: runCommand(upgradeAllCommand), }, { Name: "ls", Usage: "list all installed plugins", Action: runCommand(lsCommand), + }, { + Name: "uninstall", + Usage: "uninstall ", + Action: runCommand(removeCommand), }, { Name: "remove", - Usage: "remove ", + Usage: "remove ", Action: runCommand(removeCommand), }, } + +var Commands = []cli.Command{ + { + Name: "plugins", + Usage: "Manage plugins for grafana", + Subcommands: pluginCommands, + }, +} diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index 57c0c324320..81e7a15233b 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -5,10 +5,6 @@ import ( "bytes" "errors" "fmt" - "github.com/fatih/color" - "github.com/grafana/grafana/pkg/cmd/grafana-cli/log" - m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" - s "github.com/grafana/grafana/pkg/cmd/grafana-cli/services" "io" "io/ioutil" "net/http" @@ -16,6 +12,11 @@ import ( "path" "regexp" "strings" + + "github.com/fatih/color" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/log" + m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" + s "github.com/grafana/grafana/pkg/cmd/grafana-cli/services" ) func validateInput(c CommandLine, pluginFolder string) error { @@ -24,17 +25,16 @@ func validateInput(c CommandLine, pluginFolder string) error { return errors.New("please specify plugin to install") } - pluginDir := c.GlobalString("path") - if pluginDir == "" { - return errors.New("missing path flag") + pluginsDir := c.GlobalString("pluginsDir") + if pluginsDir == "" { + return errors.New("missing pluginsDir flag") } - fileInfo, err := os.Stat(pluginDir) + fileInfo, err := os.Stat(pluginsDir) if err != nil { - if err = os.MkdirAll(pluginDir, os.ModePerm); err != nil { - return errors.New("path is not a directory") + if err = os.MkdirAll(pluginsDir, os.ModePerm); err != nil { + return errors.New(fmt.Sprintf("pluginsDir (%s) is not a directory", pluginsDir)) } - return nil } @@ -46,7 +46,7 @@ func validateInput(c CommandLine, pluginFolder string) error { } func installCommand(c CommandLine) error { - pluginFolder := c.GlobalString("path") + pluginFolder := c.GlobalString("pluginsDir") if err := validateInput(c, pluginFolder); err != nil { return err } @@ -59,7 +59,7 @@ func installCommand(c CommandLine) error { func InstallPlugin(pluginName, version string, c CommandLine) error { plugin, err := s.GetPlugin(pluginName, c.GlobalString("repo")) - pluginFolder := c.GlobalString("path") + pluginFolder := c.GlobalString("pluginsDir") if err != nil { return err } @@ -120,6 +120,7 @@ func RemoveGitBuildFromName(pluginName, filename string) string { } var retryCount = 0 +var permissionsDeniedMessage = "Could not create %s. Permission denied. Make sure you have write access to plugindir" func downloadFile(pluginName, filePath, url string) (err error) { defer func() { @@ -153,16 +154,16 @@ func downloadFile(pluginName, filePath, url string) (err error) { newFile := path.Join(filePath, RemoveGitBuildFromName(pluginName, zf.Name)) if zf.FileInfo().IsDir() { - os.Mkdir(newFile, 0777) + err := os.Mkdir(newFile, 0777) + if PermissionsError(err) { + return fmt.Errorf(permissionsDeniedMessage, newFile) + } } else { dst, err := os.Create(newFile) - if err != nil { - if strings.Contains(err.Error(), "permission denied") { - return fmt.Errorf( - "Could not create file %s. permission deined. Make sure you have write access to plugindir", - newFile) - } + if PermissionsError(err) { + return fmt.Errorf(permissionsDeniedMessage, newFile) } + defer dst.Close() src, err := zf.Open() if err != nil { @@ -176,3 +177,7 @@ func downloadFile(pluginName, filePath, url string) (err error) { return nil } + +func PermissionsError(err error) bool { + return err != nil && strings.Contains(err.Error(), "permission denied") +} diff --git a/pkg/cmd/grafana-cli/commands/ls_command.go b/pkg/cmd/grafana-cli/commands/ls_command.go index a1481157d61..796f6e500d1 100644 --- a/pkg/cmd/grafana-cli/commands/ls_command.go +++ b/pkg/cmd/grafana-cli/commands/ls_command.go @@ -3,6 +3,7 @@ package commands import ( "errors" "fmt" + "github.com/fatih/color" "github.com/grafana/grafana/pkg/cmd/grafana-cli/log" m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" @@ -31,7 +32,7 @@ var validateLsCommand = func(pluginDir string) error { } func lsCommand(c CommandLine) error { - pluginDir := c.GlobalString("path") + pluginDir := c.GlobalString("pluginsDir") if err := validateLsCommand(pluginDir); err != nil { return err } diff --git a/pkg/cmd/grafana-cli/commands/remove_command.go b/pkg/cmd/grafana-cli/commands/remove_command.go index 1235d257ab6..e0ecbb2b788 100644 --- a/pkg/cmd/grafana-cli/commands/remove_command.go +++ b/pkg/cmd/grafana-cli/commands/remove_command.go @@ -2,6 +2,7 @@ package commands import ( "errors" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/log" m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" services "github.com/grafana/grafana/pkg/cmd/grafana-cli/services" @@ -11,7 +12,7 @@ var getPluginss func(path string) []m.InstalledPlugin = services.GetLocalPlugins var removePlugin func(pluginPath, id string) error = services.RemoveInstalledPlugin func removeCommand(c CommandLine) error { - pluginPath := c.GlobalString("path") + pluginPath := c.GlobalString("pluginsDir") localPlugins := getPluginss(pluginPath) log.Info("remove!\n") diff --git a/pkg/cmd/grafana-cli/commands/upgrade_all_command.go b/pkg/cmd/grafana-cli/commands/upgrade_all_command.go index d8594182a99..7f088be3e14 100644 --- a/pkg/cmd/grafana-cli/commands/upgrade_all_command.go +++ b/pkg/cmd/grafana-cli/commands/upgrade_all_command.go @@ -28,9 +28,9 @@ func ShouldUpgrade(installed string, remote m.Plugin) bool { } func upgradeAllCommand(c CommandLine) error { - pluginDir := c.GlobalString("path") + pluginsDir := c.GlobalString("pluginsDir") - localPlugins := s.GetLocalPlugins(pluginDir) + localPlugins := s.GetLocalPlugins(pluginsDir) remotePlugins, err := s.ListAllPlugins(c.GlobalString("repo")) @@ -51,9 +51,9 @@ func upgradeAllCommand(c CommandLine) error { } for _, p := range pluginsToUpgrade { - log.Infof("Upgrading %v \n", p.Id) + log.Infof("Updating %v \n", p.Id) - s.RemoveInstalledPlugin(pluginDir, p.Id) + s.RemoveInstalledPlugin(pluginsDir, p.Id) InstallPlugin(p.Id, "", c) } diff --git a/pkg/cmd/grafana-cli/commands/upgrade_command.go b/pkg/cmd/grafana-cli/commands/upgrade_command.go index e4072e5ced9..b9ca834be6d 100644 --- a/pkg/cmd/grafana-cli/commands/upgrade_command.go +++ b/pkg/cmd/grafana-cli/commands/upgrade_command.go @@ -5,10 +5,10 @@ import ( ) func upgradeCommand(c CommandLine) error { - pluginDir := c.GlobalString("path") + pluginsDir := c.GlobalString("pluginsDir") pluginName := c.Args().First() - localPlugin, err := s.ReadPlugin(pluginDir, pluginName) + localPlugin, err := s.ReadPlugin(pluginsDir, pluginName) if err != nil { return err @@ -23,7 +23,7 @@ func upgradeCommand(c CommandLine) error { for _, v := range remotePlugins.Plugins { if localPlugin.Id == v.Id { if ShouldUpgrade(localPlugin.Info.Version, v) { - s.RemoveInstalledPlugin(pluginDir, pluginName) + s.RemoveInstalledPlugin(pluginsDir, pluginName) return InstallPlugin(localPlugin.Id, "", c) } } diff --git a/pkg/cmd/grafana-cli/main.go b/pkg/cmd/grafana-cli/main.go index b277714fe9b..5cc3fb6f306 100644 --- a/pkg/cmd/grafana-cli/main.go +++ b/pkg/cmd/grafana-cli/main.go @@ -2,26 +2,43 @@ package main import ( "fmt" + "os" + "runtime" + "github.com/codegangsta/cli" "github.com/grafana/grafana/pkg/cmd/grafana-cli/commands" "github.com/grafana/grafana/pkg/cmd/grafana-cli/log" - "os" - "runtime" + "strings" ) var version = "master" -func getGrafanaPluginPath() string { - if os.Getenv("GF_PLUGIN_DIR") != "" { - return os.Getenv("GF_PLUGIN_DIR") +func getGrafanaPluginDir() string { + currentOS := runtime.GOOS + defaultNix := "/var/lib/grafana/plugins" + + if currentOS == "windows" { + return "C:\\opt\\grafana\\plugins" } - os := runtime.GOOS - if os == "windows" { - return "C:\\opt\\grafana\\plugins" - } else { - return "/var/lib/grafana/plugins" + pwd, err := os.Getwd() + + if err != nil { + log.Error("Could not get current path. using default") + return defaultNix } + + if isDevenvironment(pwd) { + return "../../../data/plugins" + } + + return defaultNix +} + +func isDevenvironment(pwd string) bool { + // if grafana-cli is executed from the cmd folder we can assume + // that its in development environment. + return strings.HasSuffix(pwd, "/pkg/cmd/grafana-cli") } func main() { @@ -29,19 +46,22 @@ func main() { app := cli.NewApp() app.Name = "Grafana cli" - app.Author = "raintank" + app.Usage = "" + app.Author = "Grafana Project" app.Email = "https://github.com/grafana/grafana" app.Version = version app.Flags = []cli.Flag{ cli.StringFlag{ - Name: "path", - Usage: "path to the grafana installation", - Value: getGrafanaPluginPath(), + Name: "pluginsDir", + Usage: "path to the grafana plugin directory", + Value: getGrafanaPluginDir(), + EnvVar: "GF_PLUGIN_DIR", }, cli.StringFlag{ - Name: "repo", - Usage: "url to the plugin repository", - Value: "https://grafana-net.raintank.io/api/plugins", + Name: "repo", + Usage: "url to the plugin repository", + Value: "https://grafana.net/api/plugins", + EnvVar: "GF_PLUGIN_REPO", }, cli.BoolFlag{ Name: "debug, d", diff --git a/pkg/cmd/grafana-cli/services/services.go b/pkg/cmd/grafana-cli/services/services.go index cd03f755075..f0ad460842d 100644 --- a/pkg/cmd/grafana-cli/services/services.go +++ b/pkg/cmd/grafana-cli/services/services.go @@ -4,24 +4,27 @@ import ( "encoding/json" "errors" "fmt" + "path" + "github.com/franela/goreq" "github.com/grafana/grafana/pkg/cmd/grafana-cli/log" m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" - "path" ) var IoHelper m.IoUtil = IoUtilImp{} func ListAllPlugins(repoUrl string) (m.PluginRepo, error) { fullUrl := repoUrl + "/repo" - res, _ := goreq.Request{Uri: fullUrl, MaxRedirects: 3}.Do() - + res, err := goreq.Request{Uri: fullUrl, MaxRedirects: 3}.Do() + if err != nil { + return m.PluginRepo{}, err + } if res.StatusCode != 200 { return m.PluginRepo{}, fmt.Errorf("Could not access %s statuscode %v", fullUrl, res.StatusCode) } var resp m.PluginRepo - err := res.Body.FromJsonTo(&resp) + err = res.Body.FromJsonTo(&resp) if err != nil { return m.PluginRepo{}, errors.New("Could not load plugin data") } @@ -66,9 +69,7 @@ func RemoveInstalledPlugin(pluginPath, id string) error { } func GetPlugin(pluginId, repoUrl string) (m.Plugin, error) { - resp, err := ListAllPlugins(repoUrl) - if err != nil { - } + resp, _ := ListAllPlugins(repoUrl) for _, i := range resp.Plugins { if i.Id == pluginId { diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index dad3f437390..b2c66ba185e 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -24,7 +24,7 @@ import ( "github.com/grafana/grafana/pkg/social" ) -var version = "3.0.0-pre1" +var version = "3.0.0-beta4" var commit = "NA" var buildstamp string var build_date string diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go index ef611ed358f..48f226ccfa5 100644 --- a/pkg/login/ldap.go +++ b/pkg/login/ldap.go @@ -318,7 +318,12 @@ func (a *ldapAuther) searchForUser(username string) (*ldapUserInfo, error) { // If we are using a POSIX LDAP schema it won't support memberOf, so we manually search the groups var groupSearchResult *ldap.SearchResult for _, groupSearchBase := range a.server.GroupSearchBaseDNs { - filter := strings.Replace(a.server.GroupSearchFilter, "%s", username, -1) + var filter_replace string + filter_replace = getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult) + if a.server.GroupSearchFilterUserAttribute == "" { + filter_replace = getLdapAttr(a.server.Attr.Username, searchResult) + } + filter := strings.Replace(a.server.GroupSearchFilter, "%s", filter_replace, -1) if ldapCfg.VerboseLogging { log.Info("LDAP: Searching for user's groups: %s", filter) diff --git a/pkg/login/settings.go b/pkg/login/settings.go index b181dac3281..a42476157fe 100644 --- a/pkg/login/settings.go +++ b/pkg/login/settings.go @@ -27,8 +27,9 @@ type LdapServerConf struct { SearchFilter string `toml:"search_filter"` SearchBaseDNs []string `toml:"search_base_dns"` - GroupSearchFilter string `toml:"group_search_filter"` - GroupSearchBaseDNs []string `toml:"group_search_base_dns"` + GroupSearchFilter string `toml:"group_search_filter"` + GroupSearchFilterUserAttribute string `toml:"group_search_filter_user_attribute"` + GroupSearchBaseDNs []string `toml:"group_search_base_dns"` LdapGroups []*LdapGroupToOrgRole `toml:"group_mappings"` } diff --git a/pkg/metrics/report_usage.go b/pkg/metrics/report_usage.go index c5c2afed7f7..85a87155f6d 100644 --- a/pkg/metrics/report_usage.go +++ b/pkg/metrics/report_usage.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/setting" ) @@ -56,6 +57,9 @@ func sendUsageStats() { metrics["stats.users.count"] = statsQuery.Result.UserCount metrics["stats.orgs.count"] = statsQuery.Result.OrgCount metrics["stats.playlist.count"] = statsQuery.Result.PlaylistCount + metrics["stats.plugins.apps.count"] = len(plugins.Apps) + metrics["stats.plugins.panels.count"] = len(plugins.Panels) + metrics["stats.plugins.datasources.count"] = len(plugins.DataSources) dsStats := m.GetDataSourceStatsQuery{} if err := bus.Dispatch(&dsStats); err != nil { diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 6243c729624..6b19224f934 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -148,3 +148,8 @@ type GetDashboardsQuery struct { DashboardIds []int64 Result *[]Dashboard } + +type GetDashboardSlugByIdQuery struct { + Id int64 + Result string +} diff --git a/pkg/models/preferences.go b/pkg/models/preferences.go new file mode 100644 index 00000000000..4c77bc96d4d --- /dev/null +++ b/pkg/models/preferences.go @@ -0,0 +1,53 @@ +package models + +import ( + "errors" + "time" +) + +// Typed errors +var ( + ErrPreferencesNotFound = errors.New("Preferences not found") +) + +type Preferences struct { + Id int64 + OrgId int64 + UserId int64 + Version int + HomeDashboardId int64 + Timezone string + Theme string + Created time.Time + Updated time.Time +} + +// --------------------- +// QUERIES + +type GetPreferencesQuery struct { + Id int64 + OrgId int64 + UserId int64 + + Result *Preferences +} + +type GetPreferencesWithDefaultsQuery struct { + Id int64 + OrgId int64 + UserId int64 + + Result *Preferences +} + +// --------------------- +// COMMANDS +type SavePreferencesCommand struct { + UserId int64 + OrgId int64 + + HomeDashboardId int64 `json:"homeDashboardId"` + Timezone string `json:"timezone"` + Theme string `json:"theme"` +} diff --git a/pkg/models/stats.go b/pkg/models/stats.go index 63f946b956b..fa9cfdab6e8 100644 --- a/pkg/models/stats.go +++ b/pkg/models/stats.go @@ -21,15 +21,14 @@ type GetDataSourceStatsQuery struct { } type AdminStats struct { - UserCount int `json:"user_count"` - OrgCount int `json:"org_count"` - DashboardCount int `json:"dashboard_count"` - DbSnapshotCount int `json:"db_snapshot_count"` - DbTagCount int `json:"db_tag_count"` - DataSourceCount int `json:"data_source_count"` - PlaylistCount int `json:"playlist_count"` - StarredDbCount int `json:"starred_db_count"` - GrafanaAdminCount int `json:"grafana_admin_count"` + UserCount int `json:"user_count"` + OrgCount int `json:"org_count"` + DashboardCount int `json:"dashboard_count"` + DbSnapshotCount int `json:"db_snapshot_count"` + DbTagCount int `json:"db_tag_count"` + DataSourceCount int `json:"data_source_count"` + PlaylistCount int `json:"playlist_count"` + StarredDbCount int `json:"starred_db_count"` } type GetAdminStatsQuery struct { diff --git a/pkg/models/user.go b/pkg/models/user.go index 2842bad490d..a231156b7b0 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -136,7 +136,6 @@ type SignedInUser struct { Login string Name string Email string - Theme string ApiKeyId int64 IsGrafanaAdmin bool } diff --git a/pkg/plugins/app_plugin.go b/pkg/plugins/app_plugin.go index 7fc170784f3..f565b0e2b22 100644 --- a/pkg/plugins/app_plugin.go +++ b/pkg/plugins/app_plugin.go @@ -6,16 +6,9 @@ import ( "github.com/gosimple/slug" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" ) -type AppPluginPage struct { - Name string `json:"name"` - Slug string `json:"slug"` - Component string `json:"component"` - Role models.RoleType `json:"role"` - SuppressNav bool `json:"suppressNav"` -} - type AppPluginCss struct { Light string `json:"light"` Dark string `json:"dark"` @@ -23,7 +16,6 @@ type AppPluginCss struct { type AppPlugin struct { FrontendPluginBase - Pages []*AppPluginPage `json:"pages"` Routes []*AppPluginRoute `json:"routes"` FoundChildPlugins []*PluginInclude `json:"-"` @@ -84,10 +76,18 @@ func (app *AppPlugin) initApp() { } } + app.DefaultNavUrl = setting.AppSubUrl + "/plugins/" + app.Id + "/edit" + // slugify pages - for _, page := range app.Pages { - if page.Slug == "" { - page.Slug = slug.Make(page.Name) + for _, include := range app.Includes { + if include.Slug == "" { + include.Slug = slug.Make(include.Name) + } + if include.Type == "page" && include.DefaultNav { + app.DefaultNavUrl = setting.AppSubUrl + "/plugins/" + app.Id + "/page/" + include.Slug + } + if include.Type == "dashboard" && include.DefaultNav { + app.DefaultNavUrl = setting.AppSubUrl + "/dashboard/db/" + include.Slug } } } diff --git a/pkg/plugins/frontend_plugin.go b/pkg/plugins/frontend_plugin.go index 55690777b2a..974559001d1 100644 --- a/pkg/plugins/frontend_plugin.go +++ b/pkg/plugins/frontend_plugin.go @@ -14,7 +14,7 @@ type FrontendPluginBase struct { } func (fp *FrontendPluginBase) initFrontendPlugin() { - if isInternalPlugin(fp.PluginDir) { + if isExternalPlugin(fp.PluginDir) { StaticRoutes = append(StaticRoutes, &PluginStaticRoute{ Directory: fp.PluginDir, PluginId: fp.Id, @@ -48,17 +48,18 @@ func (fp *FrontendPluginBase) setPathsBasedOnApp(app *AppPlugin) { func (fp *FrontendPluginBase) handleModuleDefaults() { - if isInternalPlugin(fp.PluginDir) { + if isExternalPlugin(fp.PluginDir) { fp.Module = path.Join("plugins", fp.Id, "module") fp.BaseUrl = path.Join("public/plugins", fp.Id) return } + fp.IsCorePlugin = true fp.Module = path.Join("app/plugins", fp.Type, fp.Id, "module") fp.BaseUrl = path.Join("public/app/plugins", fp.Type, fp.Id) } -func isInternalPlugin(pluginDir string) bool { +func isExternalPlugin(pluginDir string) bool { return !strings.Contains(pluginDir, setting.StaticRootPath) } diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index 8443e91931d..68268239c51 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" ) @@ -41,6 +42,11 @@ type PluginBase struct { IncludedInAppId string `json:"-"` PluginDir string `json:"-"` + DefaultNavUrl string `json:"-"` + IsCorePlugin bool `json:"-"` + + GrafanaNetVersion string `json:"-"` + GrafanaNetHasUpdate bool `json:"-"` // cache for readme file contents Readme []byte `json:"-"` @@ -74,10 +80,16 @@ type PluginDependencies struct { } type PluginInclude struct { - Name string `json:"name"` - Path string `json:"path"` - Type string `json:"type"` - Id string `json:"id"` + Name string `json:"name"` + Path string `json:"path"` + Type string `json:"type"` + Component string `json:"component"` + Role models.RoleType `json:"role"` + AddToNav bool `json:"addToNav"` + DefaultNav bool `json:"defaultNav"` + Slug string `json:"slug"` + + Id string `json:"-"` } type PluginDependencyItem struct { diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 0d69d5745b9..073685afc79 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -22,6 +22,9 @@ var ( Apps map[string]*AppPlugin Plugins map[string]*PluginBase PluginTypes map[string]interface{} + + GrafanaLatestVersion string + GrafanaHasUpdate bool ) type PluginScanner struct { @@ -70,6 +73,7 @@ func Init() error { app.initApp() } + go StartPluginUpdateChecker() return nil } diff --git a/pkg/plugins/update_checker.go b/pkg/plugins/update_checker.go new file mode 100644 index 00000000000..36169434096 --- /dev/null +++ b/pkg/plugins/update_checker.go @@ -0,0 +1,119 @@ +package plugins + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "strings" + "time" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/setting" +) + +type GrafanaNetPlugin struct { + Slug string `json:"slug"` + Version string `json:"version"` +} + +type GithubLatest struct { + Stable string `json:"stable"` + Testing string `json:"testing"` +} + +func StartPluginUpdateChecker() { + if !setting.CheckForUpdates { + return + } + + // do one check directly + go checkForUpdates() + + ticker := time.NewTicker(time.Minute * 10) + for { + select { + case <-ticker.C: + checkForUpdates() + } + } +} + +func getAllExternalPluginSlugs() string { + str := "" + + for _, plug := range Plugins { + if plug.IsCorePlugin { + continue + } + + str += plug.Id + "," + } + + return str +} + +func checkForUpdates() { + log.Trace("Checking for updates") + + client := http.Client{Timeout: time.Duration(5 * time.Second)} + + pluginSlugs := getAllExternalPluginSlugs() + resp, err := client.Get("https://grafana.net/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + setting.BuildVersion) + + if err != nil { + log.Trace("Failed to get plugins repo from grafana.net, %v", err.Error()) + return + } + + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Trace("Update check failed, reading response from grafana.net, %v", err.Error()) + return + } + + gNetPlugins := []GrafanaNetPlugin{} + err = json.Unmarshal(body, &gNetPlugins) + if err != nil { + log.Trace("Failed to unmarshal plugin repo, reading response from grafana.net, %v", err.Error()) + return + } + + for _, plug := range Plugins { + for _, gplug := range gNetPlugins { + if gplug.Slug == plug.Id { + plug.GrafanaNetVersion = gplug.Version + plug.GrafanaNetHasUpdate = plug.Info.Version != plug.GrafanaNetVersion + } + } + } + + resp2, err := client.Get("https://raw.githubusercontent.com/grafana/grafana/master/latest.json") + if err != nil { + log.Trace("Failed to get lates.json repo from github: %v", err.Error()) + return + } + + defer resp2.Body.Close() + body, err = ioutil.ReadAll(resp2.Body) + if err != nil { + log.Trace("Update check failed, reading response from github.net, %v", err.Error()) + return + } + + var githubLatest GithubLatest + err = json.Unmarshal(body, &githubLatest) + if err != nil { + log.Trace("Failed to unmarshal github latest, reading response from github: %v", err.Error()) + return + } + + if strings.Contains(setting.BuildVersion, "-") { + GrafanaLatestVersion = githubLatest.Testing + GrafanaHasUpdate = !strings.HasPrefix(setting.BuildVersion, githubLatest.Testing) + } else { + GrafanaLatestVersion = githubLatest.Stable + GrafanaHasUpdate = githubLatest.Stable != setting.BuildVersion + } +} diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 396d507cfd2..a64094cb65e 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -18,6 +18,7 @@ func init() { bus.AddHandler("sql", DeleteDashboard) bus.AddHandler("sql", SearchDashboards) bus.AddHandler("sql", GetDashboardTags) + bus.AddHandler("sql", GetDashboardSlugById) } func SaveDashboard(cmd *m.SaveDashboardCommand) error { @@ -255,3 +256,23 @@ func GetDashboards(query *m.GetDashboardsQuery) error { return nil } + +type DashboardSlugDTO struct { + Slug string +} + +func GetDashboardSlugById(query *m.GetDashboardSlugByIdQuery) error { + var rawSql = `SELECT slug from dashboard WHERE Id=?` + var slug = DashboardSlugDTO{} + + exists, err := x.Sql(rawSql, query.Id).Get(&slug) + + if err != nil { + return err + } else if exists == false { + return m.ErrDashboardNotFound + } + + query.Result = slug.Slug + return nil +} diff --git a/pkg/services/sqlstore/migrations/preferences_mig.go b/pkg/services/sqlstore/migrations/preferences_mig.go index 0ce01857b75..67a8169a7a8 100644 --- a/pkg/services/sqlstore/migrations/preferences_mig.go +++ b/pkg/services/sqlstore/migrations/preferences_mig.go @@ -4,17 +4,29 @@ import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" func addPreferencesMigrations(mg *Migrator) { - preferencesV1 := Table{ + mg.AddMigration("drop preferences table v2", NewDropTableMigration("preferences")) + + preferencesV2 := Table{ Name: "preferences", Columns: []*Column{ {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, - {Name: "pref_id", Type: DB_Int, Nullable: false}, - {Name: "pref_type", Type: DB_NVarchar, Length: 255, Nullable: false}, - {Name: "pref_data", Type: DB_Text, Nullable: false}, + {Name: "org_id", Type: DB_BigInt, Nullable: false}, + {Name: "user_id", Type: DB_BigInt, Nullable: false}, + {Name: "version", Type: DB_Int, Nullable: false}, + {Name: "home_dashboard_id", Type: DB_BigInt, Nullable: false}, + {Name: "timezone", Type: DB_NVarchar, Length: 50, Nullable: false}, + {Name: "theme", Type: DB_NVarchar, Length: 20, Nullable: false}, + {Name: "created", Type: DB_DateTime, Nullable: false}, + {Name: "updated", Type: DB_DateTime, Nullable: false}, + }, + Indices: []*Index{ + {Cols: []string{"org_id"}}, + {Cols: []string{"user_id"}}, }, } - // create table - mg.AddMigration("create preferences table v1", NewAddTableMigration(preferencesV1)) + mg.AddMigration("drop preferences table v3", NewDropTableMigration("preferences")) + // create table + mg.AddMigration("create preferences table v3", NewAddTableMigration(preferencesV2)) } diff --git a/pkg/services/sqlstore/preferences.go b/pkg/services/sqlstore/preferences.go new file mode 100644 index 00000000000..d120c485ed3 --- /dev/null +++ b/pkg/services/sqlstore/preferences.go @@ -0,0 +1,96 @@ +package sqlstore + +import ( + "time" + + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" +) + +func init() { + bus.AddHandler("sql", GetPreferences) + bus.AddHandler("sql", GetPreferencesWithDefaults) + bus.AddHandler("sql", SavePreferences) +} + +func GetPreferencesWithDefaults(query *m.GetPreferencesWithDefaultsQuery) error { + + prefs := make([]*m.Preferences, 0) + filter := "(org_id=? AND user_id=?) OR (org_id=? AND user_id=0)" + err := x.Where(filter, query.OrgId, query.UserId, query.OrgId). + OrderBy("user_id ASC"). + Find(&prefs) + + if err != nil { + return err + } + + res := &m.Preferences{ + Theme: "dark", + Timezone: "browser", + HomeDashboardId: 0, + } + + for _, p := range prefs { + if p.Theme != "" { + res.Theme = p.Theme + } + if p.Timezone != "" { + res.Timezone = p.Timezone + } + if p.HomeDashboardId != 0 { + res.HomeDashboardId = p.HomeDashboardId + } + } + + query.Result = res + return nil +} + +func GetPreferences(query *m.GetPreferencesQuery) error { + + var prefs m.Preferences + exists, err := x.Where("org_id=? AND user_id=?", query.OrgId, query.UserId).Get(&prefs) + + if err != nil { + return err + } + + if exists { + query.Result = &prefs + } else { + query.Result = new(m.Preferences) + } + + return nil +} + +func SavePreferences(cmd *m.SavePreferencesCommand) error { + return inTransaction2(func(sess *session) error { + + var prefs m.Preferences + exists, err := sess.Where("org_id=? AND user_id=?", cmd.OrgId, cmd.UserId).Get(&prefs) + + if !exists { + prefs = m.Preferences{ + UserId: cmd.UserId, + OrgId: cmd.OrgId, + HomeDashboardId: cmd.HomeDashboardId, + Timezone: cmd.Timezone, + Theme: cmd.Theme, + Created: time.Now(), + Updated: time.Now(), + } + _, err = sess.Insert(&prefs) + return err + } else { + prefs.HomeDashboardId = cmd.HomeDashboardId + prefs.Timezone = cmd.Timezone + prefs.Theme = cmd.Theme + prefs.Updated = time.Now() + prefs.Version += 1 + _, err := sess.Id(prefs.Id).AllCols().Update(&prefs) + return err + } + }) +} diff --git a/pkg/services/sqlstore/stats.go b/pkg/services/sqlstore/stats.go index 92efab2015d..7580996ad57 100644 --- a/pkg/services/sqlstore/stats.go +++ b/pkg/services/sqlstore/stats.go @@ -85,12 +85,7 @@ func GetAdminStats(query *m.GetAdminStatsQuery) error { ( SELECT COUNT(DISTINCT ` + dialect.Quote("dashboard_id") + ` ) FROM ` + dialect.Quote("star") + ` - ) AS starred_db_count, - ( - SELECT COUNT(*) - FROM ` + dialect.Quote("user") + ` - WHERE ` + dialect.Quote("is_admin") + ` = 1 - ) AS grafana_admin_count + ) AS starred_db_count ` var stats m.AdminStats diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 96b8c24b8fc..623e85ec472 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -273,7 +273,6 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error { u.email as email, u.login as login, u.name as name, - u.theme as theme, org.name as org_name, org_user.role as org_role, org.id as org_id diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index b8e8d7f7fcf..2d1bad945eb 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -124,6 +124,7 @@ var ( appliedEnvOverrides []string ReportingEnabled bool + CheckForUpdates bool GoogleAnalyticsId string GoogleTagManagerId string @@ -475,6 +476,7 @@ func NewConfigContext(args *CommandLineArgs) error { analytics := Cfg.Section("analytics") ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true) + CheckForUpdates = analytics.Key("check_for_updates").MustBool(true) GoogleAnalyticsId = analytics.Key("google_analytics_ua_id").String() GoogleTagManagerId = analytics.Key("google_tag_manager_id").String() diff --git a/public/app/core/app_events.ts b/public/app/core/app_events.ts new file mode 100644 index 00000000000..4507246d2be --- /dev/null +++ b/public/app/core/app_events.ts @@ -0,0 +1,6 @@ +/// + +import {Emitter} from './utils/emitter'; + +var appEvents = new Emitter(); +export default appEvents; diff --git a/public/app/core/components/colorpicker.ts b/public/app/core/components/colorpicker.ts index 7c35ac7e4ac..1834a1eb8a1 100644 --- a/public/app/core/components/colorpicker.ts +++ b/public/app/core/components/colorpicker.ts @@ -7,10 +7,6 @@ import coreModule from 'app/core/core_module'; var template = `
    - - - -
    `; @@ -51,21 +46,17 @@ export class ColorPickerCtrl { toggleAxis(yaxis) { this.$scope.toggleAxis(); - if (!this.$scope.autoClose) { + if (this.$scope.autoClose) { this.$scope.dismiss(); } } colorSelected(color) { this.$scope.colorSelected(color); - if (!this.$scope.autoClose) { + if (this.$scope.autoClose) { this.$scope.dismiss(); } } - - close() { - this.$scope.dismiss(); - } } export function colorPicker() { diff --git a/public/app/core/components/dashboard_selector.ts b/public/app/core/components/dashboard_selector.ts new file mode 100644 index 00000000000..4209ff3f8dc --- /dev/null +++ b/public/app/core/components/dashboard_selector.ts @@ -0,0 +1,47 @@ +/// + +import config from 'app/core/config'; +import _ from 'lodash'; +import $ from 'jquery'; +import coreModule from 'app/core/core_module'; + +var template = ` + + + Not finding dashboard you want? Star it first, then it should appear in this select box. + +`; + +export class DashboardSelectorCtrl { + model: any; + options: any; + + /** @ngInject */ + constructor(private backendSrv) { + } + + $onInit() { + this.options = [{value: 0, text: 'Default'}]; + + return this.backendSrv.search({starred: true}).then(res => { + res.forEach(dash => { + this.options.push({value: dash.id, text: dash.title}); + }); + }); + } +} + +export function dashboardSelector() { + return { + restrict: 'E', + controller: DashboardSelectorCtrl, + bindToController: true, + controllerAs: 'ctrl', + template: template, + scope: { + model: '=' + } + }; +} + +coreModule.directive('dashboardSelector', dashboardSelector); diff --git a/public/app/core/components/info_popover.ts b/public/app/core/components/info_popover.ts index 9b5b2877d54..b221c8577fa 100644 --- a/public/app/core/components/info_popover.ts +++ b/public/app/core/components/info_popover.ts @@ -8,21 +8,25 @@ import Drop from 'tether-drop'; export function infoPopover() { return { restrict: 'E', + template: '', transclude: true, link: function(scope, elem, attrs, ctrl, transclude) { - var inputElem = elem.prev(); - if (inputElem.length === 0) { - console.log('Failed to find input element for popover'); - return; - } var offset = attrs.offset || '0 -10px'; var position = attrs.position || 'right middle'; - var classes = 'drop-help'; + var classes = 'drop-help drop-hide-out-of-bounds'; + var openOn = 'hover'; + + elem.addClass('gf-form-help-icon'); + if (attrs.wide) { classes += ' drop-wide'; } + if (attrs.mode) { + elem.addClass('gf-form-help-icon--' + attrs.mode); + } + transclude(function(clone, newScope) { var content = document.createElement("div"); _.each(clone, (node) => { @@ -30,24 +34,16 @@ export function infoPopover() { }); var drop = new Drop({ - target: inputElem[0], + target: elem[0], content: content, position: position, classes: classes, - openOn: 'click', + openOn: openOn, tetherOptions: { offset: offset } }); - // inputElem.on('focus.popover', function() { - // drop.open(); - // }); - // - // inputElem.on('blur.popover', function() { - // close(); - // }); - scope.$on('$destroy', function() { drop.destroy(); }); diff --git a/public/app/core/components/navbar/navbar.html b/public/app/core/components/navbar/navbar.html index cbc0472c373..e2faea68eaa 100644 --- a/public/app/core/components/navbar/navbar.html +++ b/public/app/core/components/navbar/navbar.html @@ -8,9 +8,10 @@ - - - {{::ctrl.title}} + + + + {{ctrl.title}}
    diff --git a/public/app/core/components/navbar/navbar.ts b/public/app/core/components/navbar/navbar.ts index baca7721fe8..e815adf84f6 100644 --- a/public/app/core/components/navbar/navbar.ts +++ b/public/app/core/components/navbar/navbar.ts @@ -22,6 +22,7 @@ export function navbarDirective() { scope: { title: "@", titleUrl: "@", + iconUrl: "@", }, link: function(scope, elem, attrs, ctrl) { ctrl.icon = attrs.icon; diff --git a/public/app/core/components/sidemenu/sidemenu.html b/public/app/core/components/sidemenu/sidemenu.html index 7fa5cc56647..25aa38c45b8 100644 --- a/public/app/core/components/sidemenu/sidemenu.html +++ b/public/app/core/components/sidemenu/sidemenu.html @@ -21,10 +21,6 @@ {{::menuItem.text}} - - - {{::menuItem.text}} - @@ -40,7 +36,10 @@ diff --git a/public/app/core/components/sidemenu/sidemenu.ts b/public/app/core/components/sidemenu/sidemenu.ts index 4c0e100f85c..30230586e78 100644 --- a/public/app/core/components/sidemenu/sidemenu.ts +++ b/public/app/core/components/sidemenu/sidemenu.ts @@ -72,9 +72,8 @@ export class SideMenuCtrl { this.orgMenu.push({ text: "Switch to " + org.name, icon: "fa fa-fw fa-random", - click: () => { - this.switchOrg(org.orgId); - } + url: this.getUrl('/profile/switch-org/' + org.orgId), + target: '_self' }); }); @@ -83,12 +82,6 @@ export class SideMenuCtrl { } }); } - - switchOrg(orgId) { - this.backendSrv.post('/api/user/using/' + orgId).then(() => { - window.location.href = window.location.href; - }); - }; } export function sideMenuDirective() { diff --git a/public/app/core/components/switch.ts b/public/app/core/components/switch.ts new file mode 100644 index 00000000000..2a64ec487f7 --- /dev/null +++ b/public/app/core/components/switch.ts @@ -0,0 +1,60 @@ +/// + +import config from 'app/core/config'; +import _ from 'lodash'; +import $ from 'jquery'; +import coreModule from 'app/core/core_module'; +import Drop from 'tether-drop'; + +var template = ` + +
    + + +
    +`; + +export class SwitchCtrl { + onChange: any; + checked: any; + show: any; + id: any; + + /** @ngInject */ + constructor($scope, private $timeout) { + this.show = true; + this.id = $scope.$id; + } + + internalOnChange() { + return this.$timeout(() => { + return this.onChange(); + }); + } + +} + +export function switchDirective() { + return { + restrict: 'E', + controller: SwitchCtrl, + controllerAs: 'ctrl', + bindToController: true, + scope: { + checked: "=", + label: "@", + labelClass: "@", + tooltip: "@", + switchClass: "@", + onChange: "&", + }, + template: template, + }; +} + +coreModule.directive('gfFormSwitch', switchDirective); diff --git a/public/app/core/controllers/login_ctrl.js b/public/app/core/controllers/login_ctrl.js index 89757c3d141..4249d55a44f 100644 --- a/public/app/core/controllers/login_ctrl.js +++ b/public/app/core/controllers/login_ctrl.js @@ -39,7 +39,9 @@ function (angular, coreModule, config) { $scope.buildInfo = { version: config.buildInfo.version, commit: config.buildInfo.commit, - buildstamp: new Date(config.buildInfo.buildstamp * 1000) + buildstamp: new Date(config.buildInfo.buildstamp * 1000), + latestVersion: config.buildInfo.latestVersion, + hasUpdate: config.buildInfo.hasUpdate, }; $scope.submit = function() { diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 2ab7ea01410..abebb5ce560 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -28,12 +28,18 @@ import {infoPopover} from './components/info_popover'; import {colorPicker} from './components/colorpicker'; import {navbarDirective} from './components/navbar/navbar'; import {arrayJoin} from './directives/array_join'; +import {liveSrv} from './live/live_srv'; +import {Emitter} from './utils/emitter'; import {layoutSelector} from './components/layout_selector/layout_selector'; +import {switchDirective} from './components/switch'; +import {dashboardSelector} from './components/dashboard_selector'; import 'app/core/controllers/all'; import 'app/core/services/all'; import 'app/core/routes/routes'; import './filters/filters'; import coreModule from './core_module'; +import appEvents from './app_events'; + export { arrayJoin, @@ -43,6 +49,11 @@ export { navbarDirective, searchDirective, colorPicker, + liveSrv, layoutSelector, - infoPopover + switchDirective, + infoPopover, + Emitter, + appEvents, + dashboardSelector, }; diff --git a/public/app/core/directives/dropdown_typeahead.js b/public/app/core/directives/dropdown_typeahead.js index 3401ca4c38e..b44e953785e 100644 --- a/public/app/core/directives/dropdown_typeahead.js +++ b/public/app/core/directives/dropdown_typeahead.js @@ -119,4 +119,118 @@ function (_, $, coreModule) { } }; }); + + coreModule.default.directive('dropdownTypeahead2', function($compile) { + + var inputTemplate = ''; + + var buttonTemplate = ''; + + return { + scope: { + menuItems: "=dropdownTypeahead2", + dropdownTypeaheadOnSelect: "&dropdownTypeaheadOnSelect", + model: '=ngModel' + }, + link: function($scope, elem, attrs) { + var $input = $(inputTemplate); + var $button = $(buttonTemplate); + $input.appendTo(elem); + $button.appendTo(elem); + + if (attrs.linkText) { + $button.html(attrs.linkText); + } + + if (attrs.ngModel) { + $scope.$watch('model', function(newValue) { + _.each($scope.menuItems, function(item) { + _.each(item.submenu, function(subItem) { + if (subItem.value === newValue) { + $button.html(subItem.text); + } + }); + }); + }); + } + + var typeaheadValues = _.reduce($scope.menuItems, function(memo, value, index) { + if (!value.submenu) { + value.click = 'menuItemSelected(' + index + ')'; + memo.push(value.text); + } else { + _.each(value.submenu, function(item, subIndex) { + item.click = 'menuItemSelected(' + index + ',' + subIndex + ')'; + memo.push(value.text + ' ' + item.text); + }); + } + return memo; + }, []); + + $scope.menuItemSelected = function(index, subIndex) { + var menuItem = $scope.menuItems[index]; + var payload = {$item: menuItem}; + if (menuItem.submenu && subIndex !== void 0) { + payload.$subItem = menuItem.submenu[subIndex]; + } + $scope.dropdownTypeaheadOnSelect(payload); + }; + + $input.attr('data-provide', 'typeahead'); + $input.typeahead({ + source: typeaheadValues, + minLength: 1, + items: 10, + updater: function (value) { + var result = {}; + _.each($scope.menuItems, function(menuItem) { + _.each(menuItem.submenu, function(submenuItem) { + if (value === (menuItem.text + ' ' + submenuItem.text)) { + result.$subItem = submenuItem; + result.$item = menuItem; + } + }); + }); + + if (result.$item) { + $scope.$apply(function() { + $scope.dropdownTypeaheadOnSelect(result); + }); + } + + $input.trigger('blur'); + return ''; + } + }); + + $button.click(function() { + $button.hide(); + $input.show(); + $input.focus(); + }); + + $input.keyup(function() { + elem.toggleClass('open', $input.val() === ''); + }); + + $input.blur(function() { + $input.hide(); + $input.val(''); + $button.show(); + $button.focus(); + // clicking the function dropdown menu wont + // work if you remove class at once + setTimeout(function() { + elem.removeClass('open'); + }, 200); + }); + + $compile(elem.contents())($scope); + } + }; + }); }); diff --git a/public/app/core/directives/plugin_component.ts b/public/app/core/directives/plugin_component.ts index 69535327cc8..3e6383cc5c6 100644 --- a/public/app/core/directives/plugin_component.ts +++ b/public/app/core/directives/plugin_component.ts @@ -146,10 +146,14 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ }; }); } - // ConfigCtrl + // Datasource ConfigCtrl case 'datasource-config-ctrl': { var dsMeta = scope.ctrl.datasourceMeta; - return System.import(dsMeta.module).then(function(dsModule) { + return System.import(dsMeta.module).then(function(dsModule): any { + if (!dsModule.ConfigCtrl) { + return {notFound: true}; + } + return { baseUrl: dsMeta.baseUrl, name: 'ds-config-' + dsMeta.id, @@ -165,7 +169,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ return System.import(model.module).then(function(appModule) { return { baseUrl: model.baseUrl, - name: 'app-config-' + model.pluginId, + name: 'app-config-' + model.id, bindings: {appModel: "=", appEditCtrl: "="}, attrs: {"app-model": "ctrl.model", "app-edit-ctrl": "ctrl"}, Component: appModule.ConfigCtrl, diff --git a/public/app/core/live/live_srv.ts b/public/app/core/live/live_srv.ts new file mode 100644 index 00000000000..12ea8ff4266 --- /dev/null +++ b/public/app/core/live/live_srv.ts @@ -0,0 +1,136 @@ +/// + +import _ from 'lodash'; +import config from 'app/core/config'; +import coreModule from 'app/core/core_module'; + +import {Observable} from 'vendor/npm/rxjs/Observable'; + +export class LiveSrv { + conn: any; + observers: any; + initPromise: any; + + constructor() { + this.observers = {}; + } + + getWebSocketUrl() { + var l = window.location; + return ((l.protocol === "https:") ? "wss://" : "ws://") + l.host + config.appSubUrl + '/ws'; + } + + getConnection() { + if (this.initPromise) { + return this.initPromise; + } + + if (this.conn && this.conn.readyState === 1) { + return Promise.resolve(this.conn); + } + + this.initPromise = new Promise((resolve, reject) => { + console.log('Live: connecting...'); + this.conn = new WebSocket(this.getWebSocketUrl()); + + this.conn.onclose = (evt) => { + console.log("Live: websocket onclose", evt); + reject({message: 'Connection closed'}); + + this.initPromise = null; + setTimeout(this.reconnect.bind(this), 2000); + }; + + this.conn.onmessage = (evt) => { + this.handleMessage(evt.data); + }; + + this.conn.onerror = (evt) => { + this.initPromise = null; + reject({message: 'Connection error'}); + console.log("Live: websocket error", evt); + }; + + this.conn.onopen = (evt) => { + console.log('opened'); + this.initPromise = null; + resolve(this.conn); + }; + }); + + return this.initPromise; + } + + handleMessage(message) { + message = JSON.parse(message); + + if (!message.stream) { + console.log("Error: stream message without stream!", message); + return; + } + + var observer = this.observers[message.stream]; + if (!observer) { + this.removeObserver(message.stream, null); + return; + } + + observer.next(message); + } + + reconnect() { + // no need to reconnect if no one cares + if (_.keys(this.observers).length === 0) { + return; + } + + console.log('LiveSrv: Reconnecting'); + + this.getConnection().then(conn => { + _.each(this.observers, (value, key) => { + this.send({action: 'subscribe', stream: key}); + }); + }); + } + + send(data) { + this.conn.send(JSON.stringify(data)); + } + + addObserver(stream, observer) { + this.observers[stream] = observer; + + this.getConnection().then(conn => { + this.send({action: 'subscribe', stream: stream}); + }); + } + + removeObserver(stream, observer) { + console.log('unsubscribe', stream); + delete this.observers[stream]; + + this.getConnection().then(conn => { + this.send({action: 'unsubscribe', stream: stream}); + }); + } + + subscribe(streamName) { + console.log('LiveSrv.subscribe: ' + streamName); + + return Observable.create(observer => { + this.addObserver(streamName, observer); + + return () => { + this.removeObserver(streamName, observer); + }; + }); + + // return this.init().then(() => { + // this.send({action: 'subscribe', stream: name}); + // }); + } + +} + +var instance = new LiveSrv(); +export {instance as liveSrv}; diff --git a/public/app/core/routes/dashboard_loaders.js b/public/app/core/routes/dashboard_loaders.js index 8e2157ad84b..61cdf32c128 100644 --- a/public/app/core/routes/dashboard_loaders.js +++ b/public/app/core/routes/dashboard_loaders.js @@ -4,13 +4,17 @@ define([ function (coreModule) { "use strict"; - coreModule.default.controller('LoadDashboardCtrl', function($scope, $routeParams, dashboardLoaderSrv, backendSrv) { + coreModule.default.controller('LoadDashboardCtrl', function($scope, $routeParams, dashboardLoaderSrv, backendSrv, $location) { if (!$routeParams.slug) { - backendSrv.get('/api/dashboards/home').then(function(result) { - var meta = result.meta; - meta.canSave = meta.canShare = meta.canStar = false; - $scope.initDashboard(result, $scope); + backendSrv.get('/api/dashboards/home').then(function(homeDash) { + if (homeDash.redirectUri) { + $location.path('dashboard/' + homeDash.redirectUri); + } else { + var meta = homeDash.meta; + meta.canSave = meta.canShare = meta.canStar = false; + $scope.initDashboard(homeDash, $scope); + } }); return; } diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index 92cf32448a1..1608a772e87 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -88,16 +88,20 @@ function setupAngularRoutes($routeProvider, $locationProvider) { resolve: loadOrgBundle, }) .when('/profile', { - templateUrl: 'public/app/features/profile/partials/profile.html', + templateUrl: 'public/app/features/org/partials/profile.html', controller : 'ProfileCtrl', + controllerAs: 'ctrl', + resolve: loadOrgBundle, }) .when('/profile/password', { - templateUrl: 'public/app/features/profile/partials/password.html', + templateUrl: 'public/app/features/org/partials/change_password.html', controller : 'ChangePasswordCtrl', + resolve: loadOrgBundle, }) .when('/profile/select-org', { - templateUrl: 'public/app/features/profile/partials/select_org.html', + templateUrl: 'public/app/features/org/partials/select_org.html', controller : 'SelectOrgCtrl', + resolve: loadOrgBundle, }) // ADMIN .when('/admin', { diff --git a/public/app/core/services/alert_srv.js b/public/app/core/services/alert_srv.js deleted file mode 100644 index 70965d2947f..00000000000 --- a/public/app/core/services/alert_srv.js +++ /dev/null @@ -1,91 +0,0 @@ -define([ - 'angular', - 'lodash', - '../core_module', -], -function (angular, _, coreModule) { - 'use strict'; - - coreModule.default.service('alertSrv', function($timeout, $sce, $rootScope, $modal, $q) { - var self = this; - - this.init = function() { - $rootScope.onAppEvent('alert-error', function(e, alert) { - self.set(alert[0], alert[1], 'error'); - }, $rootScope); - $rootScope.onAppEvent('alert-warning', function(e, alert) { - self.set(alert[0], alert[1], 'warning', 5000); - }, $rootScope); - $rootScope.onAppEvent('alert-success', function(e, alert) { - self.set(alert[0], alert[1], 'success', 3000); - }, $rootScope); - $rootScope.onAppEvent('confirm-modal', this.showConfirmModal, $rootScope); - }; - - // List of all alert objects - this.list = []; - - this.set = function(title,text,severity,timeout) { - var newAlert = { - title: title || '', - text: text || '', - severity: severity || 'info', - }; - - var newAlertJson = angular.toJson(newAlert); - - // remove same alert if it already exists - _.remove(self.list, function(value) { - return angular.toJson(value) === newAlertJson; - }); - - self.list.push(newAlert); - if (timeout > 0) { - $timeout(function() { - self.list = _.without(self.list,newAlert); - }, timeout); - } - - if (!$rootScope.$$phase) { - $rootScope.$digest(); - } - - return(newAlert); - }; - - this.clear = function(alert) { - self.list = _.without(self.list,alert); - }; - - this.clearAll = function() { - self.list = []; - }; - - this.showConfirmModal = function(e, payload) { - var scope = $rootScope.$new(); - - scope.title = payload.title; - scope.text = payload.text; - scope.text2 = payload.text2; - scope.onConfirm = payload.onConfirm; - scope.icon = payload.icon || "fa-check"; - scope.yesText = payload.yesText || "Yes"; - scope.noText = payload.noText || "Cancel"; - - var confirmModal = $modal({ - template: 'public/app/partials/confirm_modal.html', - persist: false, - modalClass: 'confirm-modal', - show: false, - scope: scope, - keyboard: false - }); - - $q.when(confirmModal).then(function(modalEl) { - modalEl.modal('show'); - }); - - }; - - }); -}); diff --git a/public/app/core/services/alert_srv.ts b/public/app/core/services/alert_srv.ts new file mode 100644 index 00000000000..971743b7285 --- /dev/null +++ b/public/app/core/services/alert_srv.ts @@ -0,0 +1,99 @@ +/// + +import angular from 'angular'; +import _ from 'lodash'; +import $ from 'jquery'; +import coreModule from 'app/core/core_module'; +import appEvents from 'app/core/app_events'; + +export class AlertSrv { + list: any[]; + + /** @ngInject */ + constructor(private $timeout, private $sce, private $rootScope, private $modal) { + this.list = []; + } + + init() { + this.$rootScope.onAppEvent('alert-error', (e, alert) => { + this.set(alert[0], alert[1], 'error', 0); + }, this.$rootScope); + + this.$rootScope.onAppEvent('alert-warning', (e, alert) => { + this.set(alert[0], alert[1], 'warning', 5000); + }, this.$rootScope); + + this.$rootScope.onAppEvent('alert-success', (e, alert) => { + this.set(alert[0], alert[1], 'success', 3000); + }, this.$rootScope); + + appEvents.on('confirm-modal', this.showConfirmModal.bind(this)); + + this.$rootScope.onAppEvent('confirm-modal', (e, data) => { + this.showConfirmModal(data); + }, this.$rootScope); + } + + set(title, text, severity, timeout) { + var newAlert = { + title: title || '', + text: text || '', + severity: severity || 'info', + }; + + var newAlertJson = angular.toJson(newAlert); + + // remove same alert if it already exists + _.remove(this.list, function(value) { + return angular.toJson(value) === newAlertJson; + }); + + this.list.push(newAlert); + if (timeout > 0) { + this.$timeout(() => { + this.list = _.without(this.list, newAlert); + }, timeout); + } + + if (!this.$rootScope.$$phase) { + this.$rootScope.$digest(); + } + + return(newAlert); + } + + clear(alert) { + this.list = _.without(this.list, alert); + } + + clearAll() { + this.list = []; + } + + showConfirmModal(payload) { + var scope = this.$rootScope.$new(); + + scope.title = payload.title; + scope.text = payload.text; + scope.text2 = payload.text2; + scope.onConfirm = payload.onConfirm; + scope.icon = payload.icon || "fa-check"; + scope.yesText = payload.yesText || "Yes"; + scope.noText = payload.noText || "Cancel"; + + var confirmModal = this.$modal({ + template: 'public/app/partials/confirm_modal.html', + persist: false, + modalClass: 'confirm-modal', + show: false, + scope: scope, + keyboard: false + }); + + confirmModal.then(function(modalEl) { + modalEl.modal('show'); + }); + } +} + +coreModule.service('alertSrv', AlertSrv); diff --git a/public/app/core/services/backend_srv.js b/public/app/core/services/backend_srv.js index 6d0d112ba26..ff3784ab45e 100644 --- a/public/app/core/services/backend_srv.js +++ b/public/app/core/services/backend_srv.js @@ -105,6 +105,13 @@ function (angular, _, coreModule, config) { }); } + //populate error obj on Internal Error + if (_.isString(err.data) && err.status === 500) { + err.data = { + error: err.statusText + }; + } + // for Prometheus if (!err.data.message && _.isString(err.data.error)) { err.data.message = err.data.error; diff --git a/public/app/core/services/context_srv.js b/public/app/core/services/context_srv.js deleted file mode 100644 index b4a133afea3..00000000000 --- a/public/app/core/services/context_srv.js +++ /dev/null @@ -1,47 +0,0 @@ -define([ - 'angular', - 'lodash', - '../core_module', - 'app/core/store', - 'app/core/config', -], -function (angular, _, coreModule, store, config) { - 'use strict'; - - coreModule.default.service('contextSrv', function() { - - function User() { - if (config.bootData.user) { - _.extend(this, config.bootData.user); - } - } - - this.hasRole = function(role) { - return this.user.orgRole === role; - }; - - this.setPinnedState = function(val) { - this.pinned = val; - store.set('grafana.sidemenu.pinned', val); - }; - - this.toggleSideMenu = function() { - this.sidemenu = !this.sidemenu; - if (!this.sidemenu) { - this.setPinnedState(false); - } - }; - - this.pinned = store.getBool('grafana.sidemenu.pinned', false); - if (this.pinned) { - this.sidemenu = true; - } - - this.version = config.buildInfo.version; - this.lightTheme = false; - this.user = new User(); - this.isSignedIn = this.user.isSignedIn; - this.isGrafanaAdmin = this.user.isGrafanaAdmin; - this.isEditor = this.hasRole('Editor') || this.hasRole('Admin'); - }); -}); diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts new file mode 100644 index 00000000000..ad670f68a32 --- /dev/null +++ b/public/app/core/services/context_srv.ts @@ -0,0 +1,73 @@ +/// + +import config from 'app/core/config'; +import _ from 'lodash'; +import coreModule from 'app/core/core_module'; +import store from 'app/core/store'; + +export class User { + isGrafanaAdmin: any; + isSignedIn: any; + orgRole: any; + + constructor() { + if (config.bootData.user) { + _.extend(this, config.bootData.user); + } + } +} + +export class ContextSrv { + pinned: any; + version: any; + user: User; + isSignedIn: any; + isGrafanaAdmin: any; + isEditor: any; + sidemenu: any; + lightTheme: any; + + constructor() { + this.pinned = store.getBool('grafana.sidemenu.pinned', false); + if (this.pinned) { + this.sidemenu = true; + } + + if (!config.buildInfo) { + config.buildInfo = {}; + } + if (!config.bootData) { + config.bootData = {user: {}, settings: {}}; + } + + this.version = config.buildInfo.version; + this.lightTheme = false; + this.user = new User(); + this.isSignedIn = this.user.isSignedIn; + this.isGrafanaAdmin = this.user.isGrafanaAdmin; + this.isEditor = this.hasRole('Editor') || this.hasRole('Admin'); + } + + hasRole(role) { + return this.user.orgRole === role; + } + + setPinnedState(val) { + this.pinned = val; + store.set('grafana.sidemenu.pinned', val); + } + + toggleSideMenu() { + this.sidemenu = !this.sidemenu; + if (!this.sidemenu) { + this.setPinnedState(false); + } + } +} + +var contextSrv = new ContextSrv(); +export {contextSrv}; + +coreModule.factory('contextSrv', function() { + return contextSrv; +}); diff --git a/public/app/core/services/popover_srv.ts b/public/app/core/services/popover_srv.ts index 4711dc1b23c..73249a67b5b 100644 --- a/public/app/core/services/popover_srv.ts +++ b/public/app/core/services/popover_srv.ts @@ -46,9 +46,12 @@ function popoverSrv($compile, $rootScope) { drop.on('close', () => { popoverScope.dismiss({fromDropClose: true}); destroyDrop(); + if (options.onClose) { + options.onClose(); + } }); - drop.open(); + setTimeout(() => { drop.open(); }, 10); }; } diff --git a/public/app/core/time_series2.ts b/public/app/core/time_series2.ts index f5fcf5bd50c..cbceff7afd8 100644 --- a/public/app/core/time_series2.ts +++ b/public/app/core/time_series2.ts @@ -42,6 +42,7 @@ export default class TimeSeries { fillBelowTo: any; transform: any; flotpairs: any; + unit: any; constructor(opts) { this.datapoints = opts.datapoints; @@ -52,6 +53,7 @@ export default class TimeSeries { this.valueFormater = kbn.valueFormats.none; this.stats = {}; this.legend = true; + this.unit = opts.unit; } applySeriesOverrides(overrides) { @@ -170,7 +172,7 @@ export default class TimeSeries { } isMsResolutionNeeded() { - for (var i = 0; i + +import EventEmitter from 'eventemitter3'; + +var hasOwnProp = {}.hasOwnProperty; + +function createName(name) { + return '$' + name; +} + +export class Emitter { + emitter: any; + + constructor() { + this.emitter = new EventEmitter(); + } + + emit(name, data?) { + this.emitter.emit(name, data); + } + + on(name, handler, scope?) { + this.emitter.on(name, handler); + + if (scope) { + scope.$on('$destroy', function() { + this.emitter.off(name, handler); + }); + } + } + + off(name, handler) { + this.emitter.off(name, handler); + } +} diff --git a/public/app/features/admin/admin.ts b/public/app/features/admin/admin.ts index 47757520afe..b93fd07a059 100644 --- a/public/app/features/admin/admin.ts +++ b/public/app/features/admin/admin.ts @@ -19,7 +19,8 @@ class AdminSettingsCtrl { class AdminHomeCtrl { /** @ngInject **/ - constructor() {} + constructor() { + } } export class AdminStatsCtrl { diff --git a/public/app/features/admin/partials/stats.html b/public/app/features/admin/partials/stats.html index 1ad114f50e2..bcc246b9e82 100644 --- a/public/app/features/admin/partials/stats.html +++ b/public/app/features/admin/partials/stats.html @@ -22,10 +22,6 @@ Total users {{ctrl.stats.user_count}} - - Total grafana admins - {{ctrl.stats.grafana_admin_count}} - Total organizations {{ctrl.stats.org_count}} diff --git a/public/app/features/all.js b/public/app/features/all.js index c110bcff7cd..43d9f33f190 100644 --- a/public/app/features/all.js +++ b/public/app/features/all.js @@ -7,8 +7,5 @@ define([ './playlist/all', './snapshot/all', './panel/all', - './profile/profileCtrl', - './profile/changePasswordCtrl', - './profile/selectOrgCtrl', './styleguide/styleguide', ], function () {}); diff --git a/public/app/features/annotations/editor_ctrl.js b/public/app/features/annotations/editor_ctrl.js index 8d3887ee757..ca754b094a8 100644 --- a/public/app/features/annotations/editor_ctrl.js +++ b/public/app/features/annotations/editor_ctrl.js @@ -45,6 +45,7 @@ function (angular, _, $) { $scope.reset = function() { $scope.currentAnnotation = angular.copy(annotationDefaults); + $scope.currentAnnotation.datasource = $scope.datasources[0].name; $scope.currentIsNew = true; $scope.datasourceChanged(); }; diff --git a/public/app/features/dashboard/dashboardCtrl.js b/public/app/features/dashboard/dashboardCtrl.js index ea0d91019b8..b6702631155 100644 --- a/public/app/features/dashboard/dashboardCtrl.js +++ b/public/app/features/dashboard/dashboardCtrl.js @@ -134,6 +134,10 @@ function (angular, $, config, moment) { }); }; + $scope.timezoneChanged = function() { + $rootScope.$broadcast("refresh"); + }; + $scope.formatDate = function(date) { return moment(date).format('MMM Do YYYY, h:mm:ss a'); }; diff --git a/public/app/features/dashboard/dashboardSrv.js b/public/app/features/dashboard/dashboardSrv.js index fbd950e60b6..89c09ac551a 100644 --- a/public/app/features/dashboard/dashboardSrv.js +++ b/public/app/features/dashboard/dashboardSrv.js @@ -9,7 +9,7 @@ function (angular, $, _, moment) { var module = angular.module('grafana.services'); - module.factory('dashboardSrv', function() { + module.factory('dashboardSrv', function(contextSrv) { function DashboardModel (data, meta) { if (!data) { @@ -25,7 +25,7 @@ function (angular, $, _, moment) { this.originalTitle = this.title; this.tags = data.tags || []; this.style = data.style || "dark"; - this.timezone = data.timezone || 'browser'; + this.timezone = data.timezone || ''; this.editable = data.editable !== false; this.hideControls = data.hideControls || false; this.sharedCrosshair = data.sharedCrosshair || false; @@ -208,11 +208,19 @@ function (angular, $, _, moment) { }); }; + p.isTimezoneUtc = function() { + return this.getTimezone() === 'utc'; + }; + + p.getTimezone = function() { + return this.timezone ? this.timezone : contextSrv.user.timezone; + }; + p._updateSchema = function(old) { var i, j, k; var oldVersion = this.schemaVersion; var panelUpgrades = []; - this.schemaVersion = 11; + this.schemaVersion = 12; if (oldVersion === this.schemaVersion) { return; @@ -401,11 +409,63 @@ function (angular, $, _, moment) { }); } - if (oldVersion < 11) { + if (oldVersion < 12) { // update template variables _.each(this.templating.list, function(templateVariable) { if (templateVariable.refresh) { templateVariable.refresh = 1; } if (!templateVariable.refresh) { templateVariable.refresh = 0; } + if (templateVariable.hideVariable) { + templateVariable.hide = 2; + } else if (templateVariable.hideLabel) { + templateVariable.hide = 1; + } else { + templateVariable.hide = 0; + } + }); + } + + if (oldVersion < 12) { + // update graph yaxes changes + panelUpgrades.push(function(panel) { + if (panel.type !== 'graph') { return; } + if (!panel.grid) { return; } + + if (!panel.yaxes) { + panel.yaxes = [ + { + show: panel['y-axis'], + min: panel.grid.leftMin, + max: panel.grid.leftMax, + logBase: panel.grid.leftLogBase, + format: panel.y_formats[0], + label: panel.leftYAxisLabel, + }, + { + show: panel['y-axis'], + min: panel.grid.rightMin, + max: panel.grid.rightMax, + logBase: panel.grid.rightLogBase, + format: panel.y_formats[1], + label: panel.rightYAxisLabel, + } + ]; + + panel.xaxis = { + show: panel['x-axis'], + }; + + delete panel.grid.leftMin; + delete panel.grid.leftMax; + delete panel.grid.leftLogBase; + delete panel.grid.rightMin; + delete panel.grid.rightMax; + delete panel.grid.rightLogBase; + delete panel.y_formats; + delete panel.leftYAxisLabel; + delete panel.rightYAxisLabel; + delete panel['y-axis']; + delete panel['x-axis']; + } }); } diff --git a/public/app/features/dashboard/dashnav/dashnav.html b/public/app/features/dashboard/dashnav/dashnav.html index 19112f77a43..9afd152d8aa 100644 --- a/public/app/features/dashboard/dashnav/dashnav.html +++ b/public/app/features/dashboard/dashnav/dashnav.html @@ -36,7 +36,7 @@
  • - +
  • - + +
    -
      -
    • +
      + +
      + +
      +
    • -
    • + +
    • -
    -
    - - -
    -
    -
    -
      -
    • - Unit -
    • -
    • - -
    • -
    -
    -
    - -
    -
      -
    • - Window -
    • -
    • - -
    • -
    -
    -
    -
    -
      -
    • - Model -
    • -
    • - -
    • -
    -
    -
    -
    -
      -
    • - Percentiles -
    • -
    • - -
    • -
    -
    -
    -
    -
    -
      -
    • - {{stat.text}} -
    • -
    • - -
    • -
    -
    -
    -
    -
    -
      -
    • - Sigma -
    • -
    • - -
    • -
    -
    -
    - -
    -
      -
    • - Script -
    • -
    • - -
    • -
    -
    -
    - -
    -
      -
    • - Missing - The missing parameter defines how documents that are missing a value should be treated. By default they will be ignored but it is also possible to treat them as if they had a value -
    • -
    • - -
    • -
    -
    -
    - + +
    +
    + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + + +
    +
    + +
    + + +
    + +
    + +
    diff --git a/public/app/plugins/datasource/elasticsearch/partials/query.editor.html b/public/app/plugins/datasource/elasticsearch/partials/query.editor.html index 017f5cf1a42..377c026382a 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/query.editor.html +++ b/public/app/plugins/datasource/elasticsearch/partials/query.editor.html @@ -1,32 +1,31 @@ - -
  • - Query -
  • -
  • - -
  • -
  • - Alias -
  • -
  • - -
  • + + +
    +
    + + +
    +
    + + +
    +
    + +
    + + +
    + +
    + + +
    +
    - -
    - - -
    - -
    - - -
    - diff --git a/public/app/plugins/datasource/elasticsearch/query_ctrl.ts b/public/app/plugins/datasource/elasticsearch/query_ctrl.ts index 21bb578f40e..48dfd5f4756 100644 --- a/public/app/plugins/datasource/elasticsearch/query_ctrl.ts +++ b/public/app/plugins/datasource/elasticsearch/query_ctrl.ts @@ -38,6 +38,11 @@ export class ElasticQueryCtrl extends QueryCtrl { this.$rootScope.appEvent('elastic-query-updated'); } + getCollapsedText() { + var text = 'Count(), Avg(@value), Group by(@timestamp, 1min) Query'; + return text; + } + handleQueryError(err) { this.error = err.message || 'Failed to issue metric query'; return []; diff --git a/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts b/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts index bc1da316810..4d630ff9c40 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/datasource_specs.ts @@ -12,11 +12,13 @@ describe('ElasticDatasource', function() { beforeEach(angularMocks.module('grafana.core')); beforeEach(angularMocks.module('grafana.services')); beforeEach(ctx.providePhase(['templateSrv', 'backendSrv'])); + beforeEach(angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { ctx.$q = $q; ctx.$httpBackend = $httpBackend; ctx.$rootScope = $rootScope; ctx.$injector = $injector; + $httpBackend.when('GET', /\.html$/).respond(''); })); function createDatasource(instanceSettings) { diff --git a/public/app/plugins/datasource/grafana-live/_plugin.json b/public/app/plugins/datasource/grafana-live/_plugin.json new file mode 100644 index 00000000000..1f2ec204949 --- /dev/null +++ b/public/app/plugins/datasource/grafana-live/_plugin.json @@ -0,0 +1,7 @@ +{ + "type": "datasource", + "name": "Grafana Live", + "id": "grafana-live", + + "metrics": true +} diff --git a/public/app/plugins/datasource/grafana-live/datasource.ts b/public/app/plugins/datasource/grafana-live/datasource.ts new file mode 100644 index 00000000000..36605e5b6bc --- /dev/null +++ b/public/app/plugins/datasource/grafana-live/datasource.ts @@ -0,0 +1,40 @@ +/// + +import {liveSrv} from 'app/core/core'; + +import {Observable} from 'vendor/npm/rxjs/Observable'; + +class DataObservable { + target: any; + + constructor(target) { + this.target = target; + } + + subscribe(options) { + var observable = liveSrv.subscribe(this.target.stream); + return observable.subscribe(data => { + console.log("grafana stream ds data!", data); + }); + } +} + +export class GrafanaStreamDS { + subscription: any; + + /** @ngInject */ + constructor() { + } + + query(options): any { + if (options.targets.length === 0) { + return Promise.resolve({data: []}); + } + + var target = options.targets[0]; + var observable = new DataObservable(target); + + return Promise.resolve(observable); + } +} + diff --git a/public/app/plugins/datasource/grafana-live/module.ts b/public/app/plugins/datasource/grafana-live/module.ts new file mode 100644 index 00000000000..b17abd02feb --- /dev/null +++ b/public/app/plugins/datasource/grafana-live/module.ts @@ -0,0 +1,15 @@ +/// + +import angular from 'angular'; +import {GrafanaStreamDS} from './datasource'; +import {QueryCtrl} from 'app/plugins/sdk'; + +class GrafanaQueryCtrl extends QueryCtrl { + static templateUrl = 'partials/query.editor.html'; +} + +export { + GrafanaStreamDS as Datasource, + GrafanaQueryCtrl as QueryCtrl, +}; + diff --git a/public/app/plugins/datasource/grafana-live/partials/query.editor.html b/public/app/plugins/datasource/grafana-live/partials/query.editor.html new file mode 100644 index 00000000000..912b28a6247 --- /dev/null +++ b/public/app/plugins/datasource/grafana-live/partials/query.editor.html @@ -0,0 +1,8 @@ + +
  • + Stream +
  • +
  • + +
  • +
    diff --git a/public/app/plugins/datasource/grafana/README.md b/public/app/plugins/datasource/grafana/README.md new file mode 100644 index 00000000000..a8319c8dec5 --- /dev/null +++ b/public/app/plugins/datasource/grafana/README.md @@ -0,0 +1,3 @@ +# Grafana Fake Data Datasource - Native Plugin + +This is the built in Fake Data Datasource that is used before any datasources are set up in your Grafana installation. It means you can create a graph without any data and still get an idea of what it would look like. diff --git a/public/app/plugins/datasource/graphite/README.md b/public/app/plugins/datasource/graphite/README.md new file mode 100644 index 00000000000..c27c5789bca --- /dev/null +++ b/public/app/plugins/datasource/graphite/README.md @@ -0,0 +1,9 @@ +# Graphite Datasource - Native Plugin + +Grafana ships with **built in** support for Graphite (of course!). + +Grafana has an advanced Graphite query editor that lets you quickly navigate the metric space, add functions, change function parameters and much more. The editor can handle all types of graphite queries. It can even handle complex nested queries through the use of query references. + +Read more about it here: + +[http://docs.grafana.org/datasources/graphite/](http://docs.grafana.org/datasources/graphite/) \ No newline at end of file diff --git a/public/app/plugins/datasource/graphite/add_graphite_func.js b/public/app/plugins/datasource/graphite/add_graphite_func.js index 4547526d08f..bb4558b154b 100644 --- a/public/app/plugins/datasource/graphite/add_graphite_func.js +++ b/public/app/plugins/datasource/graphite/add_graphite_func.js @@ -11,10 +11,10 @@ function (angular, _, $, gfunc) { .module('grafana.directives') .directive('graphiteAddFunc', function($compile) { var inputTemplate = ''; - var buttonTemplate = '' + ''; diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index c5074b275fd..515cd490937 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -16,38 +16,34 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv this.render_method = instanceSettings.render_method || 'POST'; this.query = function(options) { - try { - var graphOptions = { - from: this.translateTime(options.rangeRaw.from, false), - until: this.translateTime(options.rangeRaw.to, true), - targets: options.targets, - format: options.format, - cacheTimeout: options.cacheTimeout || this.cacheTimeout, - maxDataPoints: options.maxDataPoints, - }; + var graphOptions = { + from: this.translateTime(options.rangeRaw.from, false), + until: this.translateTime(options.rangeRaw.to, true), + targets: options.targets, + format: options.format, + cacheTimeout: options.cacheTimeout || this.cacheTimeout, + maxDataPoints: options.maxDataPoints, + }; - var params = this.buildGraphiteParams(graphOptions, options.scopedVars); - if (params.length === 0) { - return $q.when([]); - } - - if (options.format === 'png') { - return $q.when(this.url + '/render' + '?' + params.join('&')); - } - - var httpOptions: any = {method: this.render_method, url: '/render'}; - - if (httpOptions.method === 'GET') { - httpOptions.url = httpOptions.url + '?' + params.join('&'); - } else { - httpOptions.data = params.join('&'); - httpOptions.headers = { 'Content-Type': 'application/x-www-form-urlencoded' }; - } - - return this.doGraphiteRequest(httpOptions).then(this.convertDataPointsToMs); - } catch (err) { - return $q.reject(err); + var params = this.buildGraphiteParams(graphOptions, options.scopedVars); + if (params.length === 0) { + return $q.when({data: []}); } + + if (options.format === 'png') { + return $q.when(this.url + '/render' + '?' + params.join('&')); + } + + var httpOptions: any = {method: this.render_method, url: '/render'}; + + if (httpOptions.method === 'GET') { + httpOptions.url = httpOptions.url + '?' + params.join('&'); + } else { + httpOptions.data = params.join('&'); + httpOptions.headers = { 'Content-Type': 'application/x-www-form-urlencoded' }; + } + + return this.doGraphiteRequest(httpOptions).then(this.convertDataPointsToMs); }; this.convertDataPointsToMs = function(result) { diff --git a/public/app/plugins/datasource/graphite/partials/query.editor.html b/public/app/plugins/datasource/graphite/partials/query.editor.html index 90dfe0794c0..3c2075d34bb 100755 --- a/public/app/plugins/datasource/graphite/partials/query.editor.html +++ b/public/app/plugins/datasource/graphite/partials/query.editor.html @@ -1,21 +1,27 @@ - + -
  • - -
  • +
    + +
    -
  • +
    +
    + -
  • - -
  • -
  • - - -
  • - +
    + +
    -
  • + + +
    +
    +
    +
    +
    diff --git a/public/app/plugins/datasource/graphite/partials/query.options.html b/public/app/plugins/datasource/graphite/partials/query.options.html index b00028d0e05..05aecf4b44a 100644 --- a/public/app/plugins/datasource/graphite/partials/query.options.html +++ b/public/app/plugins/datasource/graphite/partials/query.options.html @@ -1,10 +1,9 @@
    -
    - - +
    + + Cache timeout - Cache timeout
    -
    - Max data points +
    + Max data points - shorter legend names + Shorter legend names - series as parameters + Series as parameters - stacking + Stacking - templating + Templating diff --git a/public/app/plugins/datasource/graphite/specs/datasource_specs.ts b/public/app/plugins/datasource/graphite/specs/datasource_specs.ts index 2125922b350..237c37cfcc7 100644 --- a/public/app/plugins/datasource/graphite/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/graphite/specs/datasource_specs.ts @@ -15,6 +15,7 @@ describe('graphiteDatasource', function() { ctx.$httpBackend = $httpBackend; ctx.$rootScope = $rootScope; ctx.$injector = $injector; + $httpBackend.when('GET', /\.html$/).respond(''); })); beforeEach(function() { diff --git a/public/app/plugins/datasource/influxdb/README.md b/public/app/plugins/datasource/influxdb/README.md new file mode 100644 index 00000000000..45eaa51eb0f --- /dev/null +++ b/public/app/plugins/datasource/influxdb/README.md @@ -0,0 +1,13 @@ +# InfluxDB Datasource - Native Plugin + +Grafana ships with **built in** support for InfluxDB 0.9. + +There are currently two separate datasources for InfluxDB in Grafana: InfluxDB 0.8.x and InfluxDB 0.9.x. The API and capabilities of InfluxDB 0.9.x are completely different from InfluxDB 0.8.x which is why Grafana handles them as different data sources. + +This is the plugin for InfluxDB 0.9. It is rapidly evolving and we continue to track its API. + +InfluxDB 0.8 is no longer maintained by InfluxDB Inc, but we provide support as a convenience to existing users. You can find it [here](https://www.grafana.net/plugins/grafana-influxdb-08-datasource). + +Read more about InfluxDB here: + +[http://docs.grafana.org/datasources/influxdb/](http://docs.grafana.org/datasources/influxdb/) \ No newline at end of file diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 1136c9709bd..4037e963bcb 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -6,48 +6,64 @@ import _ from 'lodash'; import * as dateMath from 'app/core/utils/datemath'; import InfluxSeries from './influx_series'; import InfluxQuery from './influx_query'; +import ResponseParser from './response_parser'; -/** @ngInject */ -export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) { - this.type = 'influxdb'; - this.urls = _.map(instanceSettings.url.split(','), function(url) { - return url.trim(); - }); +export default class InfluxDatasource { + type: string; + urls: any; + username: string; + password: string; + name: string; + database: any; + basicAuth: any; + interval: any; + supportAnnotations: boolean; + supportMetrics: boolean; + responseParser: any; - this.username = instanceSettings.username; - this.password = instanceSettings.password; - this.name = instanceSettings.name; - this.database = instanceSettings.database; - this.basicAuth = instanceSettings.basicAuth; - this.interval = (instanceSettings.jsonData || {}).timeInterval; - this.supportAnnotations = true; - this.supportMetrics = true; + /** @ngInject */ + constructor(instanceSettings, private $q, private backendSrv, private templateSrv) { + this.type = 'influxdb'; + this.urls = _.map(instanceSettings.url.split(','), function(url) { + return url.trim(); + }); - this.query = function(options) { - var timeFilter = getTimeFilter(options); + this.username = instanceSettings.username; + this.password = instanceSettings.password; + this.name = instanceSettings.name; + this.database = instanceSettings.database; + this.basicAuth = instanceSettings.basicAuth; + this.interval = (instanceSettings.jsonData || {}).timeInterval; + this.supportAnnotations = true; + this.supportMetrics = true; + this.responseParser = new ResponseParser(); + } + + query(options) { + var timeFilter = this.getTimeFilter(options); var queryTargets = []; var i, y; - var allQueries = _.map(options.targets, function(target) { + var allQueries = _.map(options.targets, (target) => { if (target.hide) { return []; } queryTargets.push(target); // build query - var queryModel = new InfluxQuery(target, templateSrv, options.scopedVars); + var queryModel = new InfluxQuery(target, this.templateSrv, options.scopedVars); var query = queryModel.render(true); query = query.replace(/\$interval/g, (target.interval || options.interval)); return query; - }).join("\n"); + }).join(";"); // replace grafana variables allQueries = allQueries.replace(/\$timeFilter/g, timeFilter); // replace templated variables - allQueries = templateSrv.replace(allQueries, options.scopedVars); + allQueries = this.templateSrv.replace(allQueries, options.scopedVars); - return this._seriesQuery(allQueries).then(function(data): any { + return this._seriesQuery(allQueries).then((data): any => { if (!data || !data.results) { return []; } @@ -60,7 +76,7 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) var target = queryTargets[i]; var alias = target.alias; if (alias) { - alias = templateSrv.replace(target.alias, options.scopedVars); + alias = this.templateSrv.replace(target.alias, options.scopedVars); } var influxSeries = new InfluxSeries({ series: data.results[i].series, alias: alias }); @@ -84,16 +100,16 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) }); }; - this.annotationQuery = function(options) { + annotationQuery(options) { if (!options.annotation.query) { - return $q.reject({message: 'Query missing in annotation definition'}); + return this.$q.reject({message: 'Query missing in annotation definition'}); } - var timeFilter = getTimeFilter({rangeRaw: options.rangeRaw}); + var timeFilter = this.getTimeFilter({rangeRaw: options.rangeRaw}); var query = options.annotation.query.replace('$timeFilter', timeFilter); - query = templateSrv.replace(query); + query = this.templateSrv.replace(query, null, 'regex'); - return this._seriesQuery(query).then(function(data) { + return this._seriesQuery(query).then(data => { if (!data || !data.results || !data.results[0]) { throw { message: 'No results in response from InfluxDB' }; } @@ -101,44 +117,40 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) }); }; - this.metricFindQuery = function (query) { + metricFindQuery(query) { var interpolated; try { - interpolated = templateSrv.replace(query, null, 'regex'); + interpolated = this.templateSrv.replace(query, null, 'regex'); } catch (err) { - return $q.reject(err); + return this.$q.reject(err); } - return this._seriesQuery(interpolated).then(function (results) { - if (!results || results.results.length === 0) { return []; } - - var influxResults = results.results[0]; - if (!influxResults.series) { - return []; - } - - var series = influxResults.series[0]; - return _.map(series.values, function(value) { - if (_.isArray(value)) { - return { text: value[0] }; - } else { - return { text: value }; - } - }); - }); + return this._seriesQuery(interpolated) + .then(_.curry(this.responseParser.parse)(query)); }; - this._seriesQuery = function(query) { + _seriesQuery(query) { return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'}); - }; + } - this.testDatasource = function() { - return this.metricFindQuery('SHOW MEASUREMENTS LIMIT 1').then(function () { + + serializeParams(params) { + if (!params) { return '';} + + return _.reduce(params, (memo, value, key) => { + if (value === null || value === undefined) { return memo; } + memo.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); + return memo; + }, []).join("&"); + } + + testDatasource() { + return this.metricFindQuery('SHOW MEASUREMENTS LIMIT 1').then(() => { return { status: "success", message: "Data source is working", title: "Success" }; }); - }; + } - this._influxRequest = function(method, url, data) { + _influxRequest(method, url, data) { var self = this; var currentUrl = self.urls.shift(); @@ -165,6 +177,7 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) data: data, precision: "ms", inspect: { type: 'influxdb' }, + paramSerializer: this.serializeParams, }; options.headers = options.headers || {}; @@ -172,7 +185,7 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) options.headers.Authorization = self.basicAuth; } - return backendSrv.datasourceRequest(options).then(function(result) { + return this.backendSrv.datasourceRequest(options).then(result => { return result.data; }, function(err) { if (err.status !== 0 || err.status >= 300) { @@ -185,9 +198,9 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) }); }; - function getTimeFilter(options) { - var from = getInfluxTime(options.rangeRaw.from, false); - var until = getInfluxTime(options.rangeRaw.to, true); + getTimeFilter(options) { + var from = this.getInfluxTime(options.rangeRaw.from, false); + var until = this.getInfluxTime(options.rangeRaw.to, true); var fromIsAbsolute = from[from.length-1] === 's'; if (until === 'now()' && !fromIsAbsolute) { @@ -197,7 +210,7 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) return 'time > ' + from + ' and time < ' + until; } - function getInfluxTime(date, roundUp) { + getInfluxTime(date, roundUp) { if (_.isString(date)) { if (date === 'now') { return 'now()'; diff --git a/public/app/plugins/datasource/influxdb/influx_query.ts b/public/app/plugins/datasource/influxdb/influx_query.ts index 72d08909ac4..85457c7a8b3 100644 --- a/public/app/plugins/datasource/influxdb/influx_query.ts +++ b/public/app/plugins/datasource/influxdb/influx_query.ts @@ -11,6 +11,7 @@ export default class InfluxQuery { templateSrv: any; scopedVars: any; + /** @ngInject */ constructor(target, templateSrv?, scopedVars?) { this.target = target; this.templateSrv = templateSrv; diff --git a/public/app/plugins/datasource/influxdb/module.ts b/public/app/plugins/datasource/influxdb/module.ts index f2a21cc9022..26b067227a0 100644 --- a/public/app/plugins/datasource/influxdb/module.ts +++ b/public/app/plugins/datasource/influxdb/module.ts @@ -1,4 +1,4 @@ -import {InfluxDatasource} from './datasource'; +import InfluxDatasource from './datasource'; import {InfluxQueryCtrl} from './query_ctrl'; class InfluxConfigCtrl { diff --git a/public/app/plugins/datasource/influxdb/partials/query.editor.html b/public/app/plugins/datasource/influxdb/partials/query.editor.html index 525599981d5..6175279e778 100644 --- a/public/app/plugins/datasource/influxdb/partials/query.editor.html +++ b/public/app/plugins/datasource/influxdb/partials/query.editor.html @@ -1,24 +1,63 @@ - -
    - + - - +
    + +
    - +
    -
    +
    +
    + + + + +
    + +
    + +
    + +
    + +
    +
    +
    + +
    + +
    +
    + +
    + +
    + + +
    + +
    + +
    + +
    +
    +
    -
    - -
    - - +<<<<<<< HEAD
    @@ -54,17 +93,111 @@
    +||||||| merged common ancestors +
    +
    +
    + + + + + +
    +
    + +
    +
    +
      +
    • + SELECT +
    • +
    • + +
    • + +
    +
    +
    + +
    +
      +
    • + GROUP BY +
    • +
    • + +
    • +
    • + +
    • +
    +
    +
    +
    +======= +
    +
    + + + + + + +>>>>>>> 4515e6678346e711c34d379bba45504eae6a5dc8 + +<<<<<<< HEAD
    +||||||| merged common ancestors +
    +
    + + +======= +
    + +
    +
    +
    +
    +>>>>>>> 4515e6678346e711c34d379bba45504eae6a5dc8
    -
    - -
    - + +
    +
    + + +
    +
    + +
    + +
    +
    +
    +
    -
    + diff --git a/public/app/plugins/datasource/influxdb/partials/query.options.html b/public/app/plugins/datasource/influxdb/partials/query.options.html index 1e3a08eb556..e2b5e522541 100644 --- a/public/app/plugins/datasource/influxdb/partials/query.options.html +++ b/public/app/plugins/datasource/influxdb/partials/query.options.html @@ -38,7 +38,7 @@
    -
    +
    Alias patterns
    diff --git a/public/app/plugins/datasource/influxdb/partials/query_part.html b/public/app/plugins/datasource/influxdb/partials/query_part.html index 0eb0146ec13..478edfe5c29 100644 --- a/public/app/plugins/datasource/influxdb/partials/query_part.html +++ b/public/app/plugins/datasource/influxdb/partials/query_part.html @@ -2,4 +2,4 @@
    -{{part.def.type}}() +{{part.def.type}}() diff --git a/public/app/plugins/datasource/influxdb/query_ctrl.ts b/public/app/plugins/datasource/influxdb/query_ctrl.ts index d59bbbbccd5..0e90e7477f0 100644 --- a/public/app/plugins/datasource/influxdb/query_ctrl.ts +++ b/public/app/plugins/datasource/influxdb/query_ctrl.ts @@ -23,6 +23,7 @@ export class InfluxQueryCtrl extends QueryCtrl { measurementSegment: any; removeTagFilterSegment: any; + /** @ngInject **/ constructor($scope, $injector, private templateSrv, private $q, private uiSegmentSrv) { super($scope, $injector); @@ -193,7 +194,7 @@ export class InfluxQueryCtrl extends QueryCtrl { if (addTemplateVars) { for (let variable of this.templateSrv.variables) { - segments.unshift(this.uiSegmentSrv.newSegment({ type: 'template', value: '/$' + variable.name + '$/', expandable: true })); + segments.unshift(this.uiSegmentSrv.newSegment({ type: 'template', value: '/^$' + variable.name + '$/', expandable: true })); } } @@ -316,5 +317,9 @@ export class InfluxQueryCtrl extends QueryCtrl { return '='; } } + + getCollapsedText() { + return this.queryModel.render(false); + } } diff --git a/public/app/plugins/datasource/influxdb/response_parser.ts b/public/app/plugins/datasource/influxdb/response_parser.ts new file mode 100644 index 00000000000..2e33b398a88 --- /dev/null +++ b/public/app/plugins/datasource/influxdb/response_parser.ts @@ -0,0 +1,28 @@ +/// + +import _ from 'lodash'; + +export default class ResponseParser { + + parse(query, results) { + if (!results || results.results.length === 0) { return []; } + + var influxResults = results.results[0]; + if (!influxResults.series) { + return []; + } + + var series = influxResults.series[0]; + return _.map(series.values, (value) => { + if (_.isArray(value)) { + if (query.toLowerCase().indexOf('show tag values') >= 0) { + return { text: (value[1] || value[0]) }; + } else { + return { text: value[0] }; + } + } else { + return { text: value }; + } + }); + } +} diff --git a/public/app/plugins/datasource/influxdb/specs/response_parser_specs.ts b/public/app/plugins/datasource/influxdb/specs/response_parser_specs.ts new file mode 100644 index 00000000000..e58fe32dd1b --- /dev/null +++ b/public/app/plugins/datasource/influxdb/specs/response_parser_specs.ts @@ -0,0 +1,133 @@ +import _ from 'lodash'; +import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'; +import ResponseParser from '../response_parser'; + +describe("influxdb response parser", () => { + this.parser = new ResponseParser(); + describe("SHOW TAG response", () => { + var query = 'SHOW TAG KEYS FROM "cpu"'; + var response = { + "results": [ + { + "series": [ + { + "name": "cpu", + "columns": ["tagKey"], + "values": [ ["datacenter"], ["hostname"], ["source"] ] + } + ] + } + ] + }; + + var result = this.parser.parse(query, response); + + it("expects three results", () => { + expect(_.size(result)).to.be(3); + }); + }); + + describe("SHOW TAG VALUES response", () => { + var query = 'SHOW TAG VALUES FROM "cpu" WITH KEY = "hostname"'; + + describe("response from 0.10.0", () => { + var response = { + "results": [ + { + "series": [ + { + "name": "hostnameTagValues", + "columns": ["hostname"], + "values": [ ["server1"], ["server2"] ] + } + ] + } + ] + }; + + var result = this.parser.parse(query, response); + + it("should get two responses", () => { + expect(_.size(result)).to.be(2); + expect(result[0].text).to.be("server1"); + expect(result[1].text).to.be("server2"); + }); + }); + + describe("response from 0.11.0", () => { + var response = { + "results": [ + { + "series": [ + { + "name": "cpu", + "columns": [ "key", "value"], + "values": [ [ "source", "site" ], [ "source", "api" ] ] + } + ] + } + ] + }; + + var result = this.parser.parse(query, response); + + it("should get two responses", () => { + expect(_.size(result)).to.be(2); + expect(result[0].text).to.be('site'); + expect(result[1].text).to.be('api'); + }); + }); + + + + + }); + + describe("SHOW FIELD response", () => { + var query = 'SHOW FIELD KEYS FROM "cpu"'; + describe("response from 0.10.0", () => { + var response = { + "results": [ + { + "series": [ + { + "name": "measurements", + "columns": ["name"], + "values": [ + ["cpu"], ["derivative"], ["logins.count"], ["logs"], ["payment.ended"], ["payment.started"] + ] + } + ] + } + ] + }; + + var result = this.parser.parse(query, response); + it("should get two responses", () => { + expect(_.size(result)).to.be(6); + }); + }); + + describe("response from 0.11.0", () => { + var response = { + "results": [ + { + "series": [ + { + "name": "cpu", + "columns": ["fieldKey"], + "values": [ [ "value"] ] + } + ] + } + ] + }; + + var result = this.parser.parse(query, response); + + it("should get two responses", () => { + expect(_.size(result)).to.be(1); + }); + }); + }); +}); diff --git a/public/app/plugins/datasource/mixed/README.md b/public/app/plugins/datasource/mixed/README.md new file mode 100644 index 00000000000..b69bb626c15 --- /dev/null +++ b/public/app/plugins/datasource/mixed/README.md @@ -0,0 +1,3 @@ +# Mixed Datasource - Native Plugin + +This is a **built in** datasource that allows you to mix different datasource on the same graph! You can enable this by selecting the built in -- Mixed -- data source. When selected this will allow you to specify data source on a per query basis. This will, for example, allow you to plot metrics from different Graphite servers on the same Graph or plot data from Elasticsearch alongside data from Prometheus. Mixing different data sources on the same graph works for any data source, even custom ones. \ No newline at end of file diff --git a/public/app/plugins/datasource/opentsdb/README.md b/public/app/plugins/datasource/opentsdb/README.md new file mode 100644 index 00000000000..697ecd5c4dc --- /dev/null +++ b/public/app/plugins/datasource/opentsdb/README.md @@ -0,0 +1,7 @@ +# OpenTSDB Datasource - Native Plugin + +Grafana ships with **built in** support for OpenTSDB, a scalable, distributed time series database. + +Read more about it here: + +[http://docs.grafana.org/datasources/opentsdb/](http://docs.grafana.org/datasources/opentsdb/) \ No newline at end of file diff --git a/public/app/plugins/datasource/opentsdb/config_ctrl.ts b/public/app/plugins/datasource/opentsdb/config_ctrl.ts index e4d69c40b4d..c7c90e9c17f 100644 --- a/public/app/plugins/datasource/opentsdb/config_ctrl.ts +++ b/public/app/plugins/datasource/opentsdb/config_ctrl.ts @@ -16,7 +16,7 @@ export class OpenTsConfigCtrl { tsdbVersions = [ {name: '<=2.1', value: 1}, - {name: '2.2', value: 2}, + {name: '>=2.2', value: 2}, ]; tsdbResolutions = [ diff --git a/public/app/plugins/datasource/opentsdb/specs/datasource-specs.ts b/public/app/plugins/datasource/opentsdb/specs/datasource-specs.ts index 1da4268e433..a1dc9baada9 100644 --- a/public/app/plugins/datasource/opentsdb/specs/datasource-specs.ts +++ b/public/app/plugins/datasource/opentsdb/specs/datasource-specs.ts @@ -12,9 +12,10 @@ describe('opentsdb', function() { beforeEach(angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { ctx.$q = $q; - ctx.$httpBackend = $httpBackend; + ctx.$httpBackend = $httpBackend; ctx.$rootScope = $rootScope; ctx.ds = $injector.instantiate(OpenTsDatasource, {instanceSettings: instanceSettings}); + $httpBackend.when('GET', /\.html$/).respond(''); })); describe('When performing metricFindQuery', function() { diff --git a/public/app/plugins/datasource/prometheus/README.md b/public/app/plugins/datasource/prometheus/README.md new file mode 100644 index 00000000000..5b188c3393c --- /dev/null +++ b/public/app/plugins/datasource/prometheus/README.md @@ -0,0 +1,7 @@ +# Prometheus Datasource - Native Plugin + +Grafana ships with **built in** support for Prometheus, the open-source service monitoring system and time series database. + +Read more about it here: + +[http://docs.grafana.org/datasources/prometheus/](http://docs.grafana.org/datasources/prometheus/) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index acfc4b876d3..21d8c93aa94 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -157,7 +157,7 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS var interpolated; try { - interpolated = templateSrv.replace(expr); + interpolated = templateSrv.replace(expr, {}, interpolateQueryExpr); } catch (err) { return $q.reject(err); } diff --git a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts b/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts index 1958c6be4cf..5457fc0e16d 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts @@ -14,6 +14,7 @@ describe('PrometheusDatasource', function() { ctx.$httpBackend = $httpBackend; ctx.$rootScope = $rootScope; ctx.ds = $injector.instantiate(PrometheusDatasource, {instanceSettings: instanceSettings}); + $httpBackend.when('GET', /\.html$/).respond(''); })); describe('When querying prometheus with one target using query editor target spec', function() { diff --git a/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts b/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts index 5edf7038cd3..d328f4cb25b 100644 --- a/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts @@ -8,6 +8,7 @@ describe('PrometheusMetricFindQuery', function() { var ctx = new helpers.ServiceTestContext(); var instanceSettings = {url: 'proxied', directUrl: 'direct', user: 'test', password: 'mupp' }; + beforeEach(angularMocks.module('grafana.core')); beforeEach(angularMocks.module('grafana.services')); beforeEach(angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { @@ -15,6 +16,7 @@ describe('PrometheusMetricFindQuery', function() { ctx.$httpBackend = $httpBackend; ctx.$rootScope = $rootScope; ctx.ds = $injector.instantiate(PrometheusDatasource, {instanceSettings: instanceSettings}); + $httpBackend.when('GET', /\.html$/).respond(''); })); describe('When performing metricFindQuery', function() { diff --git a/public/app/plugins/panel/dashlist/README.md b/public/app/plugins/panel/dashlist/README.md new file mode 100644 index 00000000000..55996b5aff4 --- /dev/null +++ b/public/app/plugins/panel/dashlist/README.md @@ -0,0 +1,9 @@ +# Dashlist Panel - Native Plugin + +This Dashlist panel is **included** with Grafana. + +The dashboard list panel allows you to display dynamic links to other dashboards. The list can be configured to use starred dashboards, a search query and/or dashboard tags. + +Read more about it here: + +[http://docs.grafana.org/reference/dashlist/](http://docs.grafana.org/reference/dashlist/) \ No newline at end of file diff --git a/public/app/plugins/panel/dashlist/editor.html b/public/app/plugins/panel/dashlist/editor.html index c0577578598..d2159093476 100644 --- a/public/app/plugins/panel/dashlist/editor.html +++ b/public/app/plugins/panel/dashlist/editor.html @@ -1,40 +1,32 @@ -
    -
    -
    - Mode -
    - -
    -
    -
    - - - -
    -
    +
    +
    +
    Options
    -
    -
    - Search options - Query + + + - + -
    +
    + Max items + +
    +
    -
    - Tags +
    +
    Search
    - - -
    -
    +
    + Query + +
    + +
    + Tags + + +
    +
    -
    -
    - Limit number to - -
    -
    diff --git a/public/app/plugins/panel/dashlist/img/icn-dashlist-panel.svg b/public/app/plugins/panel/dashlist/img/icn-dashlist-panel.svg new file mode 100644 index 00000000000..f584770dff0 --- /dev/null +++ b/public/app/plugins/panel/dashlist/img/icn-dashlist-panel.svg @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/plugins/panel/dashlist/module.html b/public/app/plugins/panel/dashlist/module.html index 79952d0032c..b5c59862e5d 100644 --- a/public/app/plugins/panel/dashlist/module.html +++ b/public/app/plugins/panel/dashlist/module.html @@ -1,12 +1,17 @@ -
    - +
    +
    +
    + {{group.header}} +
    + +
    diff --git a/public/app/plugins/panel/dashlist/module.ts b/public/app/plugins/panel/dashlist/module.ts index 7d3f9d74bd0..77029bd7ceb 100644 --- a/public/app/plugins/panel/dashlist/module.ts +++ b/public/app/plugins/panel/dashlist/module.ts @@ -7,16 +7,19 @@ import {impressions} from 'app/features/dashboard/impression_store'; // Set and populate defaults var panelDefaults = { - mode: 'starred', query: '', limit: 10, - tags: [] + tags: [], + recent: false, + search: false, + starred: true, + headings: true, }; class DashListCtrl extends PanelCtrl { static templateUrl = 'module.html'; - dashList: any[]; + groups: any[]; modes: any[]; /** @ngInject */ @@ -25,49 +28,100 @@ class DashListCtrl extends PanelCtrl { _.defaults(this.panel, panelDefaults); if (this.panel.tag) { - this.panel.tags = [$scope.panel.tag]; + this.panel.tags = [this.panel.tag]; delete this.panel.tag; } + + this.events.on('refresh', this.onRefresh.bind(this)); + this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); + + this.groups = [ + {list: [], show: false, header: "Starred dashboards",}, + {list: [], show: false, header: "Recently viewed dashboards"}, + {list: [], show: false, header: "Search"}, + ]; + + // update capability + if (this.panel.mode) { + if (this.panel.mode === 'starred') { + this.panel.starred = true; + this.panel.headings = false; + } + if (this.panel.mode === 'recently viewed') { + this.panel.recent = true; + this.panel.starred = false; + this.panel.headings = false; + } + if (this.panel.mode === 'search') { + this.panel.search = true; + this.panel.starred = false; + this.panel.headings = false; + } + delete this.panel.mode; + } } - initEditMode() { - super.initEditMode(); + onInitEditMode() { + this.editorTabIndex = 1; this.modes = ['starred', 'search', 'recently viewed']; - this.icon = "fa fa-star"; - this.addEditorTab('Options', () => { - return {templateUrl: 'public/app/plugins/panel/dashlist/editor.html'}; + this.addEditorTab('Options', 'public/app/plugins/panel/dashlist/editor.html'); + } + + onRefresh() { + var promises = []; + + promises.push(this.getRecentDashboards()); + promises.push(this.getStarred()); + promises.push(this.getSearch()); + + return Promise.all(promises) + .then(this.renderingCompleted.bind(this)); + } + + getSearch() { + this.groups[2].show = this.panel.search; + if (!this.panel.search) { + return Promise.resolve(); + } + + var params = { + limit: this.panel.limit, + query: this.panel.query, + tag: this.panel.tags, + }; + + return this.backendSrv.search(params).then(result => { + this.groups[2].list = result; }); } - refresh() { - var params: any = {limit: this.panel.limit}; - - if (this.panel.mode === 'recently viewed') { - var dashIds = _.first(impressions.getDashboardOpened(), this.panel.limit); - - return this.backendSrv.search({dashboardIds: dashIds, limit: this.panel.limit}).then(result => { - this.dashList = dashIds.map(orderId => { - return _.find(result, dashboard => { - return dashboard.id === orderId; - }); - }).filter(el => { - return el !== undefined; - }); - - this.renderingCompleted(); - }); - } - - if (this.panel.mode === 'starred') { - params.starred = "true"; - } else { - params.query = this.panel.query; - params.tag = this.panel.tags; + getStarred() { + this.groups[0].show = this.panel.starred; + if (!this.panel.starred) { + return Promise.resolve(); } + var params = {limit: this.panel.limit, starred: "true"}; return this.backendSrv.search(params).then(result => { - this.dashList = result; - this.renderingCompleted(); + this.groups[0].list = result; + }); + } + + getRecentDashboards() { + this.groups[1].show = this.panel.recent; + if (!this.panel.recent) { + return Promise.resolve(); + } + + var dashIds = _.first(impressions.getDashboardOpened(), this.panel.limit); + return this.backendSrv.search({dashboardIds: dashIds, limit: this.panel.limit}).then(result => { + this.groups[1].list = dashIds.map(orderId => { + return _.find(result, dashboard => { + return dashboard.id === orderId; + }); + }).filter(el => { + return el !== undefined; + }); }); } } diff --git a/public/app/plugins/panel/dashlist/plugin.json b/public/app/plugins/panel/dashlist/plugin.json index 9cac424ac68..d46622e5e27 100644 --- a/public/app/plugins/panel/dashlist/plugin.json +++ b/public/app/plugins/panel/dashlist/plugin.json @@ -7,6 +7,10 @@ "author": { "name": "Grafana Project", "url": "http://grafana.org" +}, + "logos": { + "small": "img/icn-dashlist-panel.svg", + "large": "img/icn-dashlist-panel.svg" } } } diff --git a/public/app/plugins/panel/graph/README.md b/public/app/plugins/panel/graph/README.md new file mode 100644 index 00000000000..2dc8682f0e3 --- /dev/null +++ b/public/app/plugins/panel/graph/README.md @@ -0,0 +1,7 @@ +# Graph Panel - Native Plugin + +The Graph is the main graph panel and is **included** with Grafana. It provides a very rich set of graphing options. + +Read more about it here: + +[http://docs.grafana.org/reference/graph/](http://docs.grafana.org/reference/graph/) \ No newline at end of file diff --git a/public/app/plugins/panel/graph/axisEditor.html b/public/app/plugins/panel/graph/axisEditor.html deleted file mode 100644 index d9b7b4599e0..00000000000 --- a/public/app/plugins/panel/graph/axisEditor.html +++ /dev/null @@ -1,240 +0,0 @@ - -
    -
    -
    -
      -
    • - Left Y -
    • -
    • - Unit -
    • - -
    • - Scale type -
    • -
    • - -
    • -
    • - Label -
    • -
    • - -
    • -
    -
    -
    -
    -
      -
    • - -
    • -
    • - Y-Max -
    • -
    • - -
    • -
    • - Y-Min -
    • -
    • - -
    • -
    -
    -
    -
    -
      -
    • - Right Y -
    • -
    • - Unit -
    • - -
    • - Scale type -
    • -
    • - -
    • -
    • - Label -
    • -
    • - -
    • -
    -
    -
    -
    -
      -
    • - -
    • -
    • - Y-Max -
    • -
    • - -
    • -
    • - Y-Min -
    • -
    • - -
    • -
    -
    -
    - -
    - -
    -
    -
      -
    • - Show Axis -
    • -
    • - X-Axis  - - -
    • -
    • - Y-Axis  - - -
    • -
    -
    -
    -
    -
      -
    • - Thresholds -
    • -
    • - Level 1 -
    • -
    • - -
    • -
    • - -
    • -
    • - Level 2 -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    -
    -
    -
    -
    - -
    -
    -
    -
      -
    • - Legend -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - Side width -
    • -
    • - -
    • -
    -
    -
    -
    -
      -
    • - Hide series -
    • -
    • - -
    • -
    • - -
    • -
    -
    -
    - -
    -
      -
    • - Values -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - Decimals -
    • -
    • - -
    • -
    -
    -
    -
    -
    diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index 098d86a294f..9e6633b5551 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -54,7 +54,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) { }, scope); // Receive render events - scope.$on('render',function(event, renderData) { + ctrl.events.on('render', function(renderData) { data = renderData || data; if (!data) { ctrl.refresh(); @@ -97,10 +97,6 @@ function (angular, $, moment, _, kbn, GraphTooltip) { return true; } - if (ctrl.otherPanelInFullscreenMode()) { - return true; - } - if (!setElementHeight()) { return true; } if(_.isString(data)) { @@ -119,7 +115,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) { for (var i = 0; i < data.length; i++) { var series = data[i]; var axis = yaxis[series.yaxis - 1]; - var formater = kbn.valueFormats[panel.y_formats[series.yaxis - 1]]; + var formater = kbn.valueFormats[panel.yaxes[series.yaxis - 1].format]; // decimal override if (_.isNumber(panel.decimals)) { @@ -136,18 +132,18 @@ function (angular, $, moment, _, kbn, GraphTooltip) { } // add left axis labels - if (panel.leftYAxisLabel) { + if (panel.yaxes[0].label) { var yaxisLabel = $("
    ") - .text(panel.leftYAxisLabel) + .text(panel.yaxes[0].label) .appendTo(elem); yaxisLabel.css("margin-top", yaxisLabel.width() / 2); } // add right axis labels - if (panel.rightYAxisLabel) { + if (panel.yaxes[1].label) { var rightLabel = $("
    ") - .text(panel.rightYAxisLabel) + .text(panel.yaxes[1].label) .appendTo(elem); rightLabel.css("margin-top", rightLabel.width() / 2); @@ -155,8 +151,10 @@ function (angular, $, moment, _, kbn, GraphTooltip) { } function processOffsetHook(plot, gridMargin) { - if (panel.leftYAxisLabel) { gridMargin.left = 20; } - if (panel.rightYAxisLabel) { gridMargin.right = 20; } + var left = panel.yaxes[0]; + var right = panel.yaxes[1]; + if (left.show && left.label) { gridMargin.left = 20; } + if (right.show && right.label) { gridMargin.right = 20; } } // Function for rendering panel @@ -221,7 +219,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) { for (var i = 0; i < data.length; i++) { var series = data[i]; - series.data = series.getFlotPairs(series.nullPointMode || panel.nullPointMode, panel.y_formats); + series.data = series.getFlotPairs(series.nullPointMode || panel.nullPointMode); // if hidden remove points and disable stack if (ctrl.hiddenSeries[series.alias]) { @@ -283,7 +281,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) { var max = _.isUndefined(ctrl.range.to) ? null : ctrl.range.to.valueOf(); options.xaxis = { - timezone: dashboard.timezone, + timezone: dashboard.getTimezone(), show: panel['x-axis'], mode: "time", min: min, @@ -344,11 +342,11 @@ function (angular, $, moment, _, kbn, GraphTooltip) { function configureAxisOptions(data, options) { var defaults = { position: 'left', - show: panel['y-axis'], - min: panel.grid.leftMin, + show: panel.yaxes[0].show, + min: panel.yaxes[0].min, index: 1, - logBase: panel.grid.leftLogBase || 1, - max: panel.percentage && panel.stack ? 100 : panel.grid.leftMax, + logBase: panel.yaxes[0].logBase || 1, + max: panel.percentage && panel.stack ? 100 : panel.yaxes[0].max, }; options.yaxes.push(defaults); @@ -356,18 +354,19 @@ function (angular, $, moment, _, kbn, GraphTooltip) { if (_.findWhere(data, {yaxis: 2})) { var secondY = _.clone(defaults); secondY.index = 2, - secondY.logBase = panel.grid.rightLogBase || 1, + secondY.show = panel.yaxes[1].show; + secondY.logBase = panel.yaxes[1].logBase || 1, secondY.position = 'right'; - secondY.min = panel.grid.rightMin; - secondY.max = panel.percentage && panel.stack ? 100 : panel.grid.rightMax; + secondY.min = panel.yaxes[1].min; + secondY.max = panel.percentage && panel.stack ? 100 : panel.yaxes[1].max; options.yaxes.push(secondY); applyLogScale(options.yaxes[1], data); - configureAxisMode(options.yaxes[1], panel.percentage && panel.stack ? "percent" : panel.y_formats[1]); + configureAxisMode(options.yaxes[1], panel.percentage && panel.stack ? "percent" : panel.yaxes[1].format); } applyLogScale(options.yaxes[0], data); - configureAxisMode(options.yaxes[0], panel.percentage && panel.stack ? "percent" : panel.y_formats[0]); + configureAxisMode(options.yaxes[0], panel.percentage && panel.stack ? "percent" : panel.yaxes[0].format); } function applyLogScale(axis, data) { @@ -460,7 +459,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) { url += panel['x-axis'] ? '' : '&hideAxes=true'; url += panel['y-axis'] ? '' : '&hideYAxis=true'; - switch(panel.y_formats[0]) { + switch(panel.yaxes[0].format) { case 'bytes': url += '&yUnitSystem=binary'; break; diff --git a/public/app/plugins/panel/graph/graph_tooltip.js b/public/app/plugins/panel/graph/graph_tooltip.js index 3e2900be4e9..9ab6369a6b2 100644 --- a/public/app/plugins/panel/graph/graph_tooltip.js +++ b/public/app/plugins/panel/graph/graph_tooltip.js @@ -9,7 +9,7 @@ function ($) { var ctrl = scope.ctrl; var panel = ctrl.panel; - var $tooltip = $('
    '); + var $tooltip = $('
    '); this.findHoverIndexFromDataPoints = function(posX, series, last) { var ps = series.datapoints.pointsize; @@ -33,9 +33,8 @@ function ($) { return j - 1; }; - this.showTooltip = function(absoluteTime, relativeTime, innerHtml, pos) { - var body = '
    '+ absoluteTime + - ' (' + relativeTime + ')
    '; + this.showTooltip = function(absoluteTime, innerHtml, pos) { + var body = '
    '+ absoluteTime + '
    '; body += innerHtml + '
    '; $tooltip.html(body).place_tt(pos.pageX + 20, pos.pageY); }; @@ -109,7 +108,7 @@ function ($) { var plot = elem.data().plot; var plotData = plot.getData(); var seriesList = getSeriesFn(); - var group, value, absoluteTime, relativeTime, hoverInfo, i, series, seriesHtml, tooltipFormat; + var group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat; if (panel.tooltip.msResolution) { tooltipFormat = 'YYYY-MM-DD HH:mm:ss.SSS'; @@ -132,7 +131,6 @@ function ($) { seriesHtml = ''; - relativeTime = dashboard.getRelativeTime(seriesHoverInfo.time); absoluteTime = dashboard.formatDate(seriesHoverInfo.time, tooltipFormat); for (i = 0; i < seriesHoverInfo.length; i++) { @@ -142,17 +140,22 @@ function ($) { continue; } + var highlightClass = ''; + if (item && i === item.seriesIndex) { + highlightClass = 'graph-tooltip-list-item--highlight'; + } + series = seriesList[i]; value = series.formatValue(hoverInfo.value); - seriesHtml += '
    '; + seriesHtml += '
    '; seriesHtml += ' ' + series.label + ':
    '; seriesHtml += '
    ' + value + '
    '; plot.highlight(i, hoverInfo.hoverIndex); } - self.showTooltip(absoluteTime, relativeTime, seriesHtml, pos); + self.showTooltip(absoluteTime, seriesHtml, pos); } // single series tooltip else if (item) { @@ -169,12 +172,11 @@ function ($) { value = series.formatValue(value); - relativeTime = dashboard.getRelativeTime(item.datapoint[0]); absoluteTime = dashboard.formatDate(item.datapoint[0], tooltipFormat); group += '
    ' + value + '
    '; - self.showTooltip(absoluteTime, relativeTime, group, pos); + self.showTooltip(absoluteTime, group, pos); } // no hit else { diff --git a/public/app/plugins/panel/graph/img/icn-graph-panel.svg b/public/app/plugins/panel/graph/img/icn-graph-panel.svg new file mode 100644 index 00000000000..3fd6e2bf02e --- /dev/null +++ b/public/app/plugins/panel/graph/img/icn-graph-panel.svg @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/plugins/panel/graph/legend.js b/public/app/plugins/panel/graph/legend.js index d6d1bdbf2dd..8146617bc22 100644 --- a/public/app/plugins/panel/graph/legend.js +++ b/public/app/plugins/panel/graph/legend.js @@ -22,7 +22,7 @@ function (angular, _, $) { var seriesList; var i; - scope.$on('render', function() { + ctrl.events.on('render', function() { data = ctrl.seriesList; if (data) { render(); @@ -49,7 +49,6 @@ function (angular, _, $) { position: 'bottom center', template: '', model: { - autoClose: true, series: series, toggleAxis: function() { ctrl.toggleAxis(series); @@ -157,7 +156,7 @@ function (angular, _, $) { } var html = '
    '; html += '
    '; @@ -194,9 +193,9 @@ function (angular, _, $) { } var topPadding = 6; - $container.css("height", maxHeight - topPadding); + $container.css("max-height", maxHeight - topPadding); } else { - $container.css("height", ""); + $container.css("max-height", ""); } } } diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 0776558c1db..1fcd44204f7 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -5,6 +5,7 @@ import './legend'; import './series_overrides_ctrl'; import template from './template'; +import angular from 'angular'; import moment from 'moment'; import kbn from 'app/core/utils/kbn'; import _ from 'lodash'; @@ -17,20 +18,28 @@ var panelDefaults = { datasource: null, // sets client side (flot) or native graphite png renderer (png) renderer: 'flot', - // Show/hide the x-axis - 'x-axis' : true, - // Show/hide y-axis - 'y-axis' : true, - // y axis formats, [left axis,right axis] - y_formats : ['short', 'short'], - // grid options + yaxes: [ + { + label: null, + show: true, + logBase: 1, + min: null, + max: null, + format: 'short' + }, + { + label: null, + show: true, + logBase: 1, + min: null, + max: null, + format: 'short' + } + ], + xaxis: { + show: true + }, grid : { - leftLogBase: 1, - leftMax: null, - rightMax: null, - leftMin: null, - rightMin: null, - rightLogBase: 1, threshold1: null, threshold2: null, threshold1Color: 'rgba(216, 200, 27, 0.27)', @@ -100,20 +109,25 @@ class GraphCtrl extends MetricsPanelCtrl { constructor($scope, $injector, private annotationsSrv) { super($scope, $injector); - _.defaults(this.panel, panelDefaults); + _.defaults(this.panel, angular.copy(panelDefaults)); _.defaults(this.panel.tooltip, panelDefaults.tooltip); _.defaults(this.panel.grid, panelDefaults.grid); _.defaults(this.panel.legend, panelDefaults.legend); this.colors = $scope.$root.colors; + + this.events.on('render', this.onRender.bind(this)); + this.events.on('data-received', this.onDataReceived.bind(this)); + this.events.on('data-error', this.onDataError.bind(this)); + this.events.on('data-snapshot-load', this.onDataSnapshotLoad.bind(this)); + this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); + this.events.on('init-panel-actions', this.onInitPanelActions.bind(this)); } - initEditMode() { - super.initEditMode(); - - this.icon = "fa fa-bar-chart"; - this.addEditorTab('Axes & Grid', 'public/app/plugins/panel/graph/axisEditor.html', 2); - this.addEditorTab('Display Styles', 'public/app/plugins/panel/graph/styleEditor.html', 3); + onInitEditMode() { + this.addEditorTab('Axes', 'public/app/plugins/panel/graph/tab_axes.html', 2); + this.addEditorTab('Legend', 'public/app/plugins/panel/graph/tab_legend.html', 3); + this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4); this.logScales = { 'linear': 1, @@ -125,51 +139,47 @@ class GraphCtrl extends MetricsPanelCtrl { this.unitFormats = kbn.getUnitFormats(); } - getExtendedMenu() { - var menu = super.getExtendedMenu(); - menu.push({text: 'Export CSV (series as rows)', click: 'ctrl.exportCsv()'}); - menu.push({text: 'Export CSV (series as columns)', click: 'ctrl.exportCsvColumns()'}); - menu.push({text: 'Toggle legend', click: 'ctrl.toggleLegend()'}); - return menu; + onInitPanelActions(actions) { + actions.push({text: 'Export CSV (series as rows)', click: 'ctrl.exportCsv()'}); + actions.push({text: 'Export CSV (series as columns)', click: 'ctrl.exportCsvColumns()'}); + actions.push({text: 'Toggle legend', click: 'ctrl.toggleLegend()'}); } setUnitFormat(axis, subItem) { - this.panel.y_formats[axis] = subItem.value; + axis.format = subItem.value; this.render(); } - refreshData(datasource) { + issueQueries(datasource) { this.annotationsPromise = this.annotationsSrv.getAnnotations(this.dashboard); - - return this.issueQueries(datasource) - .then(res => this.dataHandler(res)) - .catch(err => { - this.seriesList = []; - this.render([]); - throw err; - }); + return super.issueQueries(datasource); } zoomOut(evt) { this.publishAppEvent('zoom-out', evt); } - loadSnapshot(snapshotData) { + onDataSnapshotLoad(snapshotData) { this.annotationsPromise = this.annotationsSrv.getAnnotations(this.dashboard); - this.dataHandler(snapshotData); + this.onDataReceived(snapshotData); } - dataHandler(results) { + onDataError(err) { + this.seriesList = []; + this.render([]); + } + + onDataReceived(dataList) { // png renderer returns just a url - if (_.isString(results)) { - this.render(results); + if (_.isString(dataList)) { + this.render(dataList); return; } this.datapointsWarning = false; this.datapointsCount = 0; this.datapointsOutside = false; - this.seriesList = _.map(results.data, (series, i) => this.seriesHandler(series, i)); + this.seriesList = dataList.map(this.seriesHandler.bind(this)); this.datapointsWarning = this.datapointsCount === 0 || this.datapointsOutside; this.annotationsPromise.then(annotations => { @@ -180,7 +190,7 @@ class GraphCtrl extends MetricsPanelCtrl { this.loading = false; this.render(this.seriesList); }); - }; + } seriesHandler(seriesData, index) { var datapoints = seriesData.datapoints; @@ -192,6 +202,7 @@ class GraphCtrl extends MetricsPanelCtrl { datapoints: datapoints, alias: alias, color: color, + unit: seriesData.unit, }); if (datapoints && datapoints.length > 0) { @@ -202,16 +213,23 @@ class GraphCtrl extends MetricsPanelCtrl { } this.datapointsCount += datapoints.length; - this.panel.tooltip.msResolution = this.panel.tooltip.msResolution || series.isMsResolutionNeeded(); } - series.applySeriesOverrides(this.panel.seriesOverrides); + return series; } - render(data?: any) { - this.broadcastRender(data); + onRender() { + if (!this.seriesList) { return; } + + for (let series of this.seriesList) { + series.applySeriesOverrides(this.panel.seriesOverrides); + + if (series.unit) { + this.panel.yaxes[series.yaxis-1].format = series.unit; + } + } } changeSeriesColor(series, color) { @@ -230,7 +248,6 @@ class GraphCtrl extends MetricsPanelCtrl { } else { this.toggleSeriesExclusiveMode(serie); } - this.render(); } diff --git a/public/app/plugins/panel/graph/plugin.json b/public/app/plugins/panel/graph/plugin.json index baa9fe12a39..b2976c27e04 100644 --- a/public/app/plugins/panel/graph/plugin.json +++ b/public/app/plugins/panel/graph/plugin.json @@ -7,6 +7,11 @@ "author": { "name": "Grafana Project", "url": "http://grafana.org" +}, + "logos": { + "small": "img/icn-graph-panel.svg", + "large": "img/icn-graph-panel.svg" } } } + diff --git a/public/app/plugins/panel/graph/series_overrides_ctrl.js b/public/app/plugins/panel/graph/series_overrides_ctrl.js index 014c1f6abe7..940da1a9e4b 100644 --- a/public/app/plugins/panel/graph/series_overrides_ctrl.js +++ b/public/app/plugins/panel/graph/series_overrides_ctrl.js @@ -59,7 +59,11 @@ define([ openOn: 'click', template: '', model: { + autoClose: true, colorSelected: $scope.colorSelected, + }, + onClose: function() { + $scope.ctrl.render(); } }); }; diff --git a/public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts b/public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts index e46459e2249..dfce4031772 100644 --- a/public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts +++ b/public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts @@ -14,47 +14,19 @@ describe('GraphCtrl', function() { beforeEach(ctx.providePhase()); beforeEach(ctx.createPanelController(GraphCtrl)); - - describe('get_data with 2 series', function() { - beforeEach(function() { - ctx.annotationsSrv.getAnnotations = sinon.stub().returns(ctx.$q.when([])); - ctx.datasource.query = sinon.stub().returns(ctx.$q.when({ - data: [ - { target: 'test.cpu1', datapoints: [[1, 10]]}, - { target: 'test.cpu2', datapoints: [[1, 10]]} - ] - })); - ctx.ctrl.render = sinon.spy(); - ctx.ctrl.refreshData(ctx.datasource); - ctx.scope.$digest(); - }); - - it('should send time series to render', function() { - var data = ctx.ctrl.render.getCall(0).args[0]; - expect(data.length).to.be(2); - }); - - describe('get_data failure following success', function() { - beforeEach(function() { - ctx.datasource.query = sinon.stub().returns(ctx.$q.reject('Datasource Error')); - ctx.ctrl.refreshData(ctx.datasource); - ctx.scope.$digest(); - }); - - }); + beforeEach(() => { + ctx.ctrl.annotationsPromise = Promise.resolve({}); + ctx.ctrl.updateTimeRange(); }); describe('msResolution with second resolution timestamps', function() { beforeEach(function() { - ctx.datasource.query = sinon.stub().returns(ctx.$q.when({ - data: [ - { target: 'test.cpu1', datapoints: [[1234567890, 45], [1234567899, 60]]}, - { target: 'test.cpu2', datapoints: [[1236547890, 55], [1234456709, 90]]} - ] - })); + var data = [ + { target: 'test.cpu1', datapoints: [[1234567890, 45], [1234567899, 60]]}, + { target: 'test.cpu2', datapoints: [[1236547890, 55], [1234456709, 90]]} + ]; ctx.ctrl.panel.tooltip.msResolution = false; - ctx.ctrl.refreshData(ctx.datasource); - ctx.scope.$digest(); + ctx.ctrl.onDataReceived(data); }); it('should not show millisecond resolution tooltip', function() { @@ -64,15 +36,12 @@ describe('GraphCtrl', function() { describe('msResolution with millisecond resolution timestamps', function() { beforeEach(function() { - ctx.datasource.query = sinon.stub().returns(ctx.$q.when({ - data: [ - { target: 'test.cpu1', datapoints: [[1234567890000, 45], [1234567899000, 60]]}, - { target: 'test.cpu2', datapoints: [[1236547890001, 55], [1234456709000, 90]]} - ] - })); + var data = [ + { target: 'test.cpu1', datapoints: [[1234567890000, 45], [1234567899000, 60]]}, + { target: 'test.cpu2', datapoints: [[1236547890001, 55], [1234456709000, 90]]} + ]; ctx.ctrl.panel.tooltip.msResolution = false; - ctx.ctrl.refreshData(ctx.datasource); - ctx.scope.$digest(); + ctx.ctrl.onDataReceived(data); }); it('should show millisecond resolution tooltip', function() { @@ -82,15 +51,12 @@ describe('GraphCtrl', function() { describe('msResolution with millisecond resolution timestamps but with trailing zeroes', function() { beforeEach(function() { - ctx.datasource.query = sinon.stub().returns(ctx.$q.when({ - data: [ - { target: 'test.cpu1', datapoints: [[1234567890000, 45], [1234567899000, 60]]}, - { target: 'test.cpu2', datapoints: [[1236547890000, 55], [1234456709000, 90]]} - ] - })); + var data = [ + { target: 'test.cpu1', datapoints: [[1234567890000, 45], [1234567899000, 60]]}, + { target: 'test.cpu2', datapoints: [[1236547890000, 55], [1234456709000, 90]]} + ]; ctx.ctrl.panel.tooltip.msResolution = false; - ctx.ctrl.refreshData(ctx.datasource); - ctx.scope.$digest(); + ctx.ctrl.onDataReceived(data); }); it('should not show millisecond resolution tooltip', function() { @@ -100,16 +66,13 @@ describe('GraphCtrl', function() { describe('msResolution with millisecond resolution timestamps in one of the series', function() { beforeEach(function() { - ctx.datasource.query = sinon.stub().returns(ctx.$q.when({ - data: [ - { target: 'test.cpu1', datapoints: [[1234567890000, 45], [1234567899000, 60]]}, - { target: 'test.cpu2', datapoints: [[1236547890010, 55], [1234456709000, 90]]}, - { target: 'test.cpu3', datapoints: [[1236547890000, 65], [1234456709000, 120]]} - ] - })); + var data = [ + { target: 'test.cpu1', datapoints: [[1234567890000, 45], [1234567899000, 60]]}, + { target: 'test.cpu2', datapoints: [[1236547890010, 55], [1234456709000, 90]]}, + { target: 'test.cpu3', datapoints: [[1236547890000, 65], [1234456709000, 120]]} + ]; ctx.ctrl.panel.tooltip.msResolution = false; - ctx.ctrl.refreshData(ctx.datasource); - ctx.scope.$digest(); + ctx.ctrl.onDataReceived(data); }); it('should show millisecond resolution tooltip', function() { diff --git a/public/app/plugins/panel/graph/specs/graph_specs.ts b/public/app/plugins/panel/graph/specs/graph_specs.ts index dae90224ff8..b9c9362e5de 100644 --- a/public/app/plugins/panel/graph/specs/graph_specs.ts +++ b/public/app/plugins/panel/graph/specs/graph_specs.ts @@ -8,6 +8,7 @@ import $ from 'jquery'; import helpers from 'test/specs/helpers'; import TimeSeries from 'app/core/time_series2'; import moment from 'moment'; +import {Emitter} from 'app/core/core'; describe('grafanaGraph', function() { @@ -24,31 +25,49 @@ describe('grafanaGraph', function() { })); beforeEach(angularMocks.inject(function($rootScope, $compile) { - var ctrl: any = {}; + var ctrl: any = { + events: new Emitter(), + height: 200, + panel: { + legend: {}, + grid: { }, + yaxes: [ + { + min: null, + max: null, + format: 'short', + logBase: 1 + }, + { + min: null, + max: null, + format: 'short', + logBase: 1 + } + ], + xaxis: {}, + seriesOverrides: [], + tooltip: { + shared: true + } + }, + renderingCompleted: sinon.spy(), + hiddenSeries: {}, + dashboard: { + getTimezone: sinon.stub().returns('browser') + }, + range: { + from: moment([2015, 1, 1, 10]), + to: moment([2015, 1, 1, 22]), + }, + }; + var scope = $rootScope.$new(); scope.ctrl = ctrl; - var element = angular.element("
    "); - ctrl.height = '200px'; - ctrl.panel = { - legend: {}, - grid: { }, - y_formats: [], - seriesOverrides: [], - tooltip: { - shared: true - } - }; $rootScope.onAppEvent = sinon.spy(); - ctrl.otherPanelInFullscreenMode = sinon.spy(); - ctrl.renderingCompleted = sinon.spy(); - ctrl.hiddenSeries = {}; - ctrl.dashboard = { timezone: 'browser' }; - ctrl.range = { - from: moment([2015, 1, 1, 10]), - to: moment([2015, 1, 1, 22]), - }; + ctx.data = []; ctx.data.push(new TimeSeries({ datapoints: [[1,1],[2,2]], @@ -61,11 +80,12 @@ describe('grafanaGraph', function() { setupFunc(ctrl, ctx.data); + var element = angular.element("
    "); $compile(element)(scope); scope.$digest(); - $.plot = ctx.plotSpy = sinon.spy(); - scope.$emit('render', ctx.data); + $.plot = ctx.plotSpy = sinon.spy(); + ctrl.events.emit('render', ctx.data); ctx.plotData = ctx.plotSpy.getCall(0).args[1]; ctx.plotOptions = ctx.plotSpy.getCall(0).args[2]; })); @@ -147,13 +167,7 @@ describe('grafanaGraph', function() { graphScenario('when logBase is log 10', function(ctx) { ctx.setup(function(ctrl) { - ctrl.panel.grid = { - leftMax: null, - rightMax: null, - leftMin: null, - rightMin: null, - leftLogBase: 10, - }; + ctrl.panel.yaxes[0].logBase = 10; }); it('should apply axis transform and ticks', function() { diff --git a/public/app/plugins/panel/graph/styleEditor.html b/public/app/plugins/panel/graph/styleEditor.html deleted file mode 100644 index 465e8d4894e..00000000000 --- a/public/app/plugins/panel/graph/styleEditor.html +++ /dev/null @@ -1,123 +0,0 @@ -
    -
    -
    Draw Modes
    -
    - - -
    -
    - - -
    -
    - - -
    -
    -
    -
    Mode Options
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - - -
    -
    -
    -
    Misc options
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    -
    Multiple Series
    -
    - - -
    -
    - - -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    Series specific overrides Regex match example: /server[0-3]/i
    -
    -
    -
      -
    • - -
    • - -
    • - alias or regex -
    • - -
    • - -
    • - -
    • - - - Color: - - - {{option.name}}: {{option.value}} - -
    • - - -
    -
    -
    -
    - - -
    -
    diff --git a/public/app/plugins/panel/graph/tab_axes.html b/public/app/plugins/panel/graph/tab_axes.html new file mode 100644 index 00000000000..81ab3deeb31 --- /dev/null +++ b/public/app/plugins/panel/graph/tab_axes.html @@ -0,0 +1,72 @@ +
    +
    + +
    Left Y
    +
    Right Y
    + + + +
    +
    + +
    +
    + +
    + +
    + +
    +
    + +
    +
    + + +
    +
    + + +
    +
    + +
    + + +
    +
    +
    + +
    +
    X-Axis
    + +
    + +
    +
    Thresholds
    +
    +
    + + +
    +
    + +
    + +
    +
    +
    +
    +
    + + +
    +
    + +
    + +
    +
    +
    +
    +
    diff --git a/public/app/plugins/panel/graph/tab_display.html b/public/app/plugins/panel/graph/tab_display.html new file mode 100644 index 00000000000..067651540e2 --- /dev/null +++ b/public/app/plugins/panel/graph/tab_display.html @@ -0,0 +1,123 @@ +
    +
    +
    Draw Modes
    + + + + + + +
    +
    +
    Mode Options
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    + + +
    +
    +
    Misc options
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    Multiple Series
    + + + + +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    Series specific overrides Regex match example: /server[0-3]/i
    +
    +
    +
      +
    • + +
    • + +
    • + alias or regex +
    • + +
    • + +
    • + +
    • + + + Color: + + + {{option.name}}: {{option.value}} + +
    • + + +
    +
    +
    +
    + + +
    +
    diff --git a/public/app/plugins/panel/graph/tab_legend.html b/public/app/plugins/panel/graph/tab_legend.html new file mode 100644 index 00000000000..82e1335dc10 --- /dev/null +++ b/public/app/plugins/panel/graph/tab_legend.html @@ -0,0 +1,73 @@ +
    +
    +
    Options
    + + + + + + +
    + + +
    +
    + +
    +
    Values
    + +
    + + + + + +
    + +
    + + + + + +
    + +
    + + + +
    + + +
    +
    +
    + +
    +
    Hide series
    + + + + +
    +
    diff --git a/public/app/plugins/panel/pluginlist/README.md b/public/app/plugins/panel/pluginlist/README.md new file mode 100644 index 00000000000..463769dad1f --- /dev/null +++ b/public/app/plugins/panel/pluginlist/README.md @@ -0,0 +1,2 @@ +# Plugin List Panel - Native Plugin + diff --git a/public/app/plugins/panel/pluginlist/editor.html b/public/app/plugins/panel/pluginlist/editor.html new file mode 100644 index 00000000000..c0577578598 --- /dev/null +++ b/public/app/plugins/panel/pluginlist/editor.html @@ -0,0 +1,40 @@ +
    +
    +
    + Mode +
    + +
    +
    +
    + + + +
    +
    + +
    +
    + Search options + Query + + + +
    + +
    + Tags + + + +
    +
    + +
    +
    + Limit number to + +
    +
    +
    diff --git a/public/app/plugins/panel/pluginlist/img/icn-dashlist-panel.svg b/public/app/plugins/panel/pluginlist/img/icn-dashlist-panel.svg new file mode 100644 index 00000000000..8bac231bedf --- /dev/null +++ b/public/app/plugins/panel/pluginlist/img/icn-dashlist-panel.svg @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/plugins/panel/pluginlist/module.html b/public/app/plugins/panel/pluginlist/module.html new file mode 100644 index 00000000000..c73da35b391 --- /dev/null +++ b/public/app/plugins/panel/pluginlist/module.html @@ -0,0 +1,30 @@ + diff --git a/public/app/plugins/panel/pluginlist/module.ts b/public/app/plugins/panel/pluginlist/module.ts new file mode 100644 index 00000000000..8ec86efd9c2 --- /dev/null +++ b/public/app/plugins/panel/pluginlist/module.ts @@ -0,0 +1,74 @@ +/// + +import _ from 'lodash'; +import config from 'app/core/config'; +import {PanelCtrl} from '../../../features/panel/panel_ctrl'; + +// Set and populate defaults +var panelDefaults = { +}; + +class PluginListCtrl extends PanelCtrl { + static templateUrl = 'module.html'; + + pluginList: any[]; + viewModel: any; + + /** @ngInject */ + constructor($scope, $injector, private backendSrv, private $location) { + super($scope, $injector); + _.defaults(this.panel, panelDefaults); + + this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); + this.pluginList = []; + this.viewModel = [ + {header: "Installed Apps", list: [], type: 'app'}, + {header: "Installed Panels", list: [], type: 'panel'}, + {header: "Installed Datasources", list: [], type: 'datasource'}, + ]; + + this.update(); + } + + onInitEditMode() { + this.editorTabIndex = 1; + this.addEditorTab('Options', 'public/app/plugins/panel/pluginlist/editor.html'); + } + + gotoPlugin(plugin, evt) { + if (evt) { evt.stopPropagation(); } + this.$location.url(`plugins/${plugin.id}/edit`); + } + + updateAvailable(plugin, $event) { + $event.stopPropagation(); + $event.preventDefault(); + + var modalScope = this.$scope.$new(true); + modalScope.plugin = plugin; + + this.publishAppEvent('show-modal', { + src: 'public/app/features/plugins/partials/update_instructions.html', + scope: modalScope + }); + } + + update() { + this.backendSrv.get('api/plugins', {embedded: 0, core: 0}).then(plugins => { + this.pluginList = plugins; + this.viewModel[0].list = _.filter(plugins, {type: 'app'}); + this.viewModel[1].list = _.filter(plugins, {type: 'panel'}); + this.viewModel[2].list = _.filter(plugins, {type: 'datasource'}); + + for (let plugin of this.pluginList) { + if (plugin.hasUpdate) { + plugin.state = 'has-update'; + } else if (!plugin.enabled) { + plugin.state = 'not-enabled'; + } + } + }); + } +} + +export {PluginListCtrl, PluginListCtrl as PanelCtrl} diff --git a/public/app/plugins/panel/pluginlist/plugin.json b/public/app/plugins/panel/pluginlist/plugin.json new file mode 100644 index 00000000000..be6ae9a5985 --- /dev/null +++ b/public/app/plugins/panel/pluginlist/plugin.json @@ -0,0 +1,16 @@ +{ + "type": "panel", + "name": "Plugin list", + "id": "pluginlist", + + "info": { + "author": { + "name": "Grafana Project", + "url": "http://grafana.org" +}, + "logos": { + "small": "img/icn-dashlist-panel.svg", + "large": "img/icn-dashlist-panel.svg" + } + } +} diff --git a/public/app/plugins/panel/singlestat/README.md b/public/app/plugins/panel/singlestat/README.md new file mode 100644 index 00000000000..42d72825c27 --- /dev/null +++ b/public/app/plugins/panel/singlestat/README.md @@ -0,0 +1,9 @@ +# Singlestat Panel - Native Plugin + +The Singlestat Panel is **included** with Grafana. + +The Singlestat Panel allows you to show the one main summary stat of a SINGLE series. It reduces the series into a single number (by looking at the max, min, average, or sum of values in the series). Singlestat also provides thresholds to color the stat or the Panel background. It can also translate the single number into a text value, and show a sparkline summary of the series. + +Read more about it here: + +[http://docs.grafana.org/reference/singlestat/](http://docs.grafana.org/reference/singlestat/) \ No newline at end of file diff --git a/public/app/plugins/panel/singlestat/img/icn-singlestat-panel.svg b/public/app/plugins/panel/singlestat/img/icn-singlestat-panel.svg new file mode 100644 index 00000000000..a1e15d4d58d --- /dev/null +++ b/public/app/plugins/panel/singlestat/img/icn-singlestat-panel.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index 175f49c9ff5..8af684c9d68 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -45,7 +45,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { static templateUrl = 'module.html'; series: any[]; - data: any[]; + data: any; fontSizes: any[]; unitFormats: any[]; @@ -53,11 +53,14 @@ class SingleStatCtrl extends MetricsPanelCtrl { constructor($scope, $injector, private $location, private linkSrv) { super($scope, $injector); _.defaults(this.panel, panelDefaults); + + this.events.on('data-received', this.onDataReceived.bind(this)); + this.events.on('data-error', this.onDataError.bind(this)); + this.events.on('data-snapshot-load', this.onDataReceived.bind(this)); + this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); } - initEditMode() { - super.initEditMode(); - this.icon = "fa fa-dashboard"; + onInitEditMode() { this.fontSizes = ['20%', '30%','50%','70%','80%','100%', '110%', '120%', '150%', '170%', '200%']; this.addEditorTab('Options', 'public/app/plugins/panel/singlestat/editor.html', 2); this.unitFormats = kbn.getUnitFormats(); @@ -68,23 +71,22 @@ class SingleStatCtrl extends MetricsPanelCtrl { this.render(); } - refreshData(datasource) { - return this.issueQueries(datasource) - .then(this.dataHandler.bind(this)) - .catch(err => { - this.series = []; - this.render(); - throw err; - }); + onDataError(err) { + this.onDataReceived({data: []}); } - loadSnapshot(snapshotData) { - // give element time to get attached and get dimensions - this.$timeout(() => this.dataHandler(snapshotData), 50); - } + onDataReceived(dataList) { + this.series = dataList.map(this.seriesHandler.bind(this)); - dataHandler(results) { - this.series = _.map(results.data, this.seriesHandler.bind(this)); + var data: any = {}; + this.setValues(data); + + data.thresholds = this.panel.thresholds.split(',').map(function(strVale) { + return Number(strVale.trim()); + }); + + data.colorMap = this.panel.colors; + this.data = data; this.render(); } @@ -155,20 +157,6 @@ class SingleStatCtrl extends MetricsPanelCtrl { return result; } - render() { - var data: any = {}; - this.setValues(data); - - data.thresholds = this.panel.thresholds.split(',').map(function(strVale) { - return Number(strVale.trim()); - }); - - data.colorMap = this.panel.colors; - - this.data = data; - this.broadcastRender(); - } - setValues(data) { data.flotpairs = []; @@ -242,14 +230,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { var templateSrv = this.templateSrv; var data, linkInfo; var $panelContainer = elem.find('.panel-container'); - // change elem to singlestat panel elem = elem.find('.singlestat-panel'); - hookupDrilldownLinkTooltip(); - - scope.$on('render', function() { - render(); - ctrl.renderingCompleted(); - }); function setElementHeight() { elem.css('height', ctrl.height + 'px'); @@ -291,8 +272,14 @@ class SingleStatCtrl extends MetricsPanelCtrl { function addSparkline() { var width = elem.width() + 20; - var height = ctrl.height; + if (width < 30) { + // element has not gotten it's width yet + // delay sparkline render + setTimeout(addSparkline, 30); + return; + } + var height = ctrl.height; var plotCanvas = $('
    '); var plotCss: any = {}; plotCss.position = 'absolute'; @@ -418,6 +405,13 @@ class SingleStatCtrl extends MetricsPanelCtrl { drilldownTooltip.place_tt(e.pageX+20, e.pageY-15); }); } + + hookupDrilldownLinkTooltip(); + + this.events.on('render', function() { + render(); + ctrl.renderingCompleted(); + }); } } diff --git a/public/app/plugins/panel/singlestat/plugin.json b/public/app/plugins/panel/singlestat/plugin.json index ea9ae32853a..197cf3ec27d 100644 --- a/public/app/plugins/panel/singlestat/plugin.json +++ b/public/app/plugins/panel/singlestat/plugin.json @@ -7,6 +7,11 @@ "author": { "name": "Grafana Project", "url": "http://grafana.org" +}, + "logos": { + "small": "img/icn-singlestat-panel.svg", + "large": "img/icn-singlestat-panel.svg" } } } + diff --git a/public/app/plugins/panel/singlestat/specs/singlestat-specs.ts b/public/app/plugins/panel/singlestat/specs/singlestat-specs.ts index 90bd5339737..dc85454b64a 100644 --- a/public/app/plugins/panel/singlestat/specs/singlestat-specs.ts +++ b/public/app/plugins/panel/singlestat/specs/singlestat-specs.ts @@ -23,12 +23,11 @@ describe('SingleStatCtrl', function() { beforeEach(function() { setupFunc(); - ctx.datasource.query = sinon.stub().returns(ctx.$q.when({ - data: [{target: 'test.cpu1', datapoints: ctx.datapoints}] - })); + var data = [ + {target: 'test.cpu1', datapoints: ctx.datapoints} + ]; - ctx.ctrl.refreshData(ctx.datasource); - ctx.scope.$digest(); + ctx.ctrl.onDataReceived(data); ctx.data = ctx.ctrl.data; }); }; diff --git a/public/app/plugins/panel/table/README.md b/public/app/plugins/panel/table/README.md new file mode 100644 index 00000000000..48c4fac641b --- /dev/null +++ b/public/app/plugins/panel/table/README.md @@ -0,0 +1,9 @@ +# Table Panel - Native Plugin + +The Table Panel is **included** with Grafana. + +The table panel is very flexible, supporting both multiple modes for time series as well as for table, annotation and raw JSON data. It also provides date formatting and value formatting and coloring options. + +Check out the [Table Panel Showcase in the Grafana Playground](http://play.grafana.org/dashboard/db/table-panel-showcase) or read more about it here: + +[http://docs.grafana.org/reference/table_panel/](http://docs.grafana.org/reference/table_panel/) \ No newline at end of file diff --git a/public/app/plugins/panel/table/img/icn-table-panel.svg b/public/app/plugins/panel/table/img/icn-table-panel.svg new file mode 100644 index 00000000000..83097e259dc --- /dev/null +++ b/public/app/plugins/panel/table/img/icn-table-panel.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts index b821cf3642f..7a2973cc390 100644 --- a/public/app/plugins/panel/table/module.ts +++ b/public/app/plugins/panel/table/module.ts @@ -57,61 +57,45 @@ class TablePanelCtrl extends MetricsPanelCtrl { } _.defaults(this.panel, panelDefaults); + + this.events.on('data-received', this.onDataReceived.bind(this)); + this.events.on('data-error', this.onDataError.bind(this)); + this.events.on('data-snapshot-load', this.onDataReceived.bind(this)); + this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); + this.events.on('init-panel-actions', this.onInitPanelActions.bind(this)); } - initEditMode() { - super.initEditMode(); + onInitEditMode() { this.addEditorTab('Options', tablePanelEditor, 2); } - getExtendedMenu() { - var menu = super.getExtendedMenu(); - menu.push({text: 'Export CSV', click: 'ctrl.exportCsv()'}); - return menu; + onInitPanelActions(actions) { + actions.push({text: 'Export CSV', click: 'ctrl.exportCsv()'}); } - refreshData(datasource) { + issueQueries(datasource) { this.pageIndex = 0; if (this.panel.transform === 'annotations') { + this.setTimeQueryStart(); return this.annotationsSrv.getAnnotations(this.dashboard).then(annotations => { - this.dataRaw = annotations; - this.render(); + return {data: annotations}; }); } - return this.issueQueries(datasource) - .then(this.dataHandler.bind(this)) - .catch(err => { - this.render(); - throw err; - }); + return super.issueQueries(datasource); } - toggleColumnSort(col, colIndex) { - if (this.panel.sort.col === colIndex) { - if (this.panel.sort.desc) { - this.panel.sort.desc = false; - } else { - this.panel.sort.col = null; - } - } else { - this.panel.sort.col = colIndex; - this.panel.sort.desc = true; - } - + onDataError(err) { + this.dataRaw = []; this.render(); } - dataHandler(results) { - this.dataRaw = results.data; + onDataReceived(dataList) { + this.dataRaw = dataList; this.pageIndex = 0; - this.render(); - } - render() { - // automatically correct transform mode - // based on data + // automatically correct transform mode based on data if (this.dataRaw && this.dataRaw.length) { if (this.dataRaw[0].type === 'table') { this.panel.transform = 'table'; @@ -126,9 +110,27 @@ class TablePanelCtrl extends MetricsPanelCtrl { } } + this.render(); + } + + render() { this.table = transformDataToTable(this.dataRaw, this.panel); this.table.sort(this.panel.sort); - this.broadcastRender(this.table); + return super.render(this.table); + } + + toggleColumnSort(col, colIndex) { + if (this.panel.sort.col === colIndex) { + if (this.panel.sort.desc) { + this.panel.sort.desc = false; + } else { + this.panel.sort.col = null; + } + } else { + this.panel.sort.col = colIndex; + this.panel.sort.desc = true; + } + this.render(); } exportCsv() { @@ -142,19 +144,17 @@ class TablePanelCtrl extends MetricsPanelCtrl { var formaters = []; function getTableHeight() { - var panelHeight = ctrl.height || ctrl.panel.height || ctrl.row.height; - if (_.isString(panelHeight)) { - panelHeight = parseInt(panelHeight.replace('px', ''), 10); - } + var panelHeight = ctrl.height; + if (pageCount > 1) { - panelHeight -= 28; + panelHeight -= 26; } - return (panelHeight - 60) + 'px'; + return (panelHeight - 31) + 'px'; } function appendTableRows(tbodyElem) { - var renderer = new TableRenderer(panel, data, ctrl.dashboard.timezone); + var renderer = new TableRenderer(panel, data, ctrl.dashboard.isTimezoneUtc()); tbodyElem.empty(); tbodyElem.html(renderer.render(ctrl.pageIndex)); } @@ -209,11 +209,12 @@ class TablePanelCtrl extends MetricsPanelCtrl { elem.off('click', '.table-panel-page-link'); }); - scope.$on('render', function(event, renderData) { + ctrl.events.on('render', function(renderData) { data = renderData || data; if (data) { renderPanel(); } + ctrl.renderingCompleted(); }); } } diff --git a/public/app/plugins/panel/table/plugin.json b/public/app/plugins/panel/table/plugin.json index e25efdfbe36..84a527db565 100644 --- a/public/app/plugins/panel/table/plugin.json +++ b/public/app/plugins/panel/table/plugin.json @@ -7,6 +7,11 @@ "author": { "name": "Grafana Project", "url": "http://grafana.org" +}, + "logos": { + "small": "img/icn-table-panel.svg", + "large": "img/icn-table-panel.svg" } } } + diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index 0a306b103a4..c00656071be 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/public/app/plugins/panel/table/renderer.ts @@ -8,7 +8,7 @@ export class TableRenderer { formaters: any[]; colorState: any; - constructor(private panel, private table, private timezone) { + constructor(private panel, private table, private isUtc) { this.formaters = []; this.colorState = {}; } @@ -45,7 +45,7 @@ export class TableRenderer { return v => { if (_.isArray(v)) { v = v[0]; } var date = moment(v); - if (this.timezone === 'utc') { + if (this.isUtc) { date = date.utc(); } return date.format(style.dateFormat); diff --git a/public/app/plugins/panel/text/README.md b/public/app/plugins/panel/text/README.md new file mode 100644 index 00000000000..14751842990 --- /dev/null +++ b/public/app/plugins/panel/text/README.md @@ -0,0 +1,5 @@ +# Text Panel - Native Plugin + +The Text Panel is **included** with Grafana. + +The Text Panel is a very simple panel that displays text. The source text is written in the Markdown syntax meaning you can format the text. Read [GitHub's Mastering Markdown](https://guides.github.com/features/mastering-markdown/) to learn more. \ No newline at end of file diff --git a/public/app/plugins/panel/text/img/icn-text-panel.svg b/public/app/plugins/panel/text/img/icn-text-panel.svg new file mode 100644 index 00000000000..4274cd6c35c --- /dev/null +++ b/public/app/plugins/panel/text/img/icn-text-panel.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/plugins/panel/text/module.ts b/public/app/plugins/panel/text/module.ts index 601f5ffc813..42657788c0a 100644 --- a/public/app/plugins/panel/text/module.ts +++ b/public/app/plugins/panel/text/module.ts @@ -20,20 +20,18 @@ export class TextPanelCtrl extends PanelCtrl { super($scope, $injector); _.defaults(this.panel, panelDefaults); + + this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); + this.events.on('refresh', this.onRender.bind(this)); + this.events.on('render', this.onRender.bind(this)); } - initEditMode() { - super.initEditMode(); - this.icon = 'fa fa-text-width'; + onInitEditMode() { this.addEditorTab('Options', 'public/app/plugins/panel/text/editor.html'); this.editorTabIndex = 1; } - refresh() { - this.render(); - } - - render() { + onRender() { if (this.panel.mode === 'markdown') { this.renderMarkdown(this.panel.content); } else if (this.panel.mode === 'html') { diff --git a/public/app/plugins/panel/text/plugin.json b/public/app/plugins/panel/text/plugin.json index 485f42942f2..e63974c6167 100644 --- a/public/app/plugins/panel/text/plugin.json +++ b/public/app/plugins/panel/text/plugin.json @@ -7,6 +7,11 @@ "author": { "name": "Grafana Project", "url": "http://grafana.org" +}, + "logos": { + "small": "img/icn-text-panel.svg", + "large": "img/icn-text-panel.svg" } } } + diff --git a/public/app/plugins/panel/unknown/module.ts b/public/app/plugins/panel/unknown/module.ts index 0a0871d6b69..c4567599a38 100644 --- a/public/app/plugins/panel/unknown/module.ts +++ b/public/app/plugins/panel/unknown/module.ts @@ -9,6 +9,7 @@ export class UnknownPanelCtrl extends PanelCtrl { constructor($scope, $injector) { super($scope, $injector); } + } diff --git a/public/app/system.conf.js b/public/app/system.conf.js index 16fdcd7e3d8..276988e5c34 100644 --- a/public/app/system.conf.js +++ b/public/app/system.conf.js @@ -4,6 +4,7 @@ System.config({ paths: { 'remarkable': 'vendor/npm/remarkable/dist/remarkable.js', 'tether': 'vendor/npm/tether/dist/js/tether.js', + 'eventemitter3': 'vendor/npm/eventemitter3/index.js', 'tether-drop': 'vendor/npm/tether-drop/dist/js/drop.js', 'moment': 'vendor/moment.js', "jquery": "vendor/jquery/dist/jquery.js", @@ -55,5 +56,9 @@ System.config({ deps: ['jquery'], exports: 'angular', }, + 'vendor/npm/eventemitter3/index.js': { + format: 'cjs', + exports: 'EventEmitter' + }, } }); diff --git a/public/dashboards/home.json b/public/dashboards/home.json index 1d57a37edfe..c3ed1017bad 100644 --- a/public/dashboards/home.json +++ b/public/dashboards/home.json @@ -9,55 +9,61 @@ "hideControls": true, "sharedCrosshair": false, "rows": [ - { + { "collapse": false, "editable": true, - "height": "90px", + "height": "25px", "panels": [ { "content": "
    \n Home Dashboard\n
    ", "editable": true, "id": 1, + "links": [], "mode": "html", "span": 12, "style": {}, "title": "", "transparent": true, - "type": "text", - "links": [] + "type": "text" } ], "title": "New row" - }, - { + }, + { "collapse": false, "editable": true, "height": "510px", "panels": [ { - "id": 2, - "limit": 10, - "mode": "starred", + "id": 3, + "limit": 4, + "links": [], "query": "", - "span": 6, + "span": 7, "tags": [], - "title": "Starred dashboards", - "type": "dashlist" + "title": "", + "transparent": false, + "type": "dashlist", + "recent": true, + "search": false, + "starred": true, + "headings": true }, { - "id": 3, - "limit": 10, - "mode": "recently viewed", - "query": "", - "span": 6, - "tags": [], - "title": "Recently viewed dashboards", - "type": "dashlist" + "editable": true, + "error": false, + "id": 4, + "isNew": true, + "links": [], + "span": 5, + "title": "", + "transparent": false, + "type": "pluginlist" } ], "title": "Row" - } - ], + } + ], "time": { "from": "now-6h", "to": "now" @@ -95,7 +101,7 @@ "annotations": { "list": [] }, - "schemaVersion": 9, - "version": 5, + "schemaVersion": 12, + "version": 2, "links": [] } diff --git a/public/fonts/grafana-icons.eot b/public/fonts/grafana-icons.eot old mode 100644 new mode 100755 index 64c6f9654db..4e594eca65d Binary files a/public/fonts/grafana-icons.eot and b/public/fonts/grafana-icons.eot differ diff --git a/public/fonts/grafana-icons.svg b/public/fonts/grafana-icons.svg old mode 100644 new mode 100755 index 850b43adad9..319e1739324 --- a/public/fonts/grafana-icons.svg +++ b/public/fonts/grafana-icons.svg @@ -27,7 +27,7 @@ - + @@ -45,7 +45,7 @@ - + diff --git a/public/fonts/grafana-icons.ttf b/public/fonts/grafana-icons.ttf old mode 100644 new mode 100755 index 4f6f75f8897..2cfefbff196 Binary files a/public/fonts/grafana-icons.ttf and b/public/fonts/grafana-icons.ttf differ diff --git a/public/fonts/grafana-icons.woff b/public/fonts/grafana-icons.woff old mode 100644 new mode 100755 index 206056f78c2..9c1f1bc848c Binary files a/public/fonts/grafana-icons.woff and b/public/fonts/grafana-icons.woff differ diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index 32643962ddc..194d7a5487c 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -17,6 +17,7 @@ @import "base/grid"; @import "base/font_awesome"; @import "base/grafana_icons"; +@import "base/code"; // UTILS @import "utils/utils"; @@ -35,11 +36,13 @@ @import "components/navs"; @import "components/tabs"; @import "components/alerts"; +@import "components/switch"; @import "components/tooltip"; @import "components/tags"; @import "components/panel_graph"; @import "components/submenu"; @import "components/panel_dashlist"; +@import "components/panel_pluginlist"; @import "components/panel_singlestat"; @import "components/panel_table"; @import "components/panel_text"; @@ -67,6 +70,7 @@ @import "components/drop"; @import "components/query_editor"; @import "components/tabbed_view"; +@import "components/query_part"; // PAGES @import "pages/login"; diff --git a/public/sass/_old_responsive.scss b/public/sass/_old_responsive.scss index a7b2917277a..a388f9ae26e 100644 --- a/public/sass/_old_responsive.scss +++ b/public/sass/_old_responsive.scss @@ -7,8 +7,8 @@ // --------------------- @include media-breakpoint-down(sm) { div.panel { - width: 100%; - padding: 0px; + width: 100% !important; + padding: 0px !important; } .panel-margin { margin-right: 0; @@ -24,6 +24,7 @@ max-width: 120px; } .dashnav-zoom-out, + .dashnav-move-timeframe, .dashnav-action-icons { display: none; } @@ -40,6 +41,11 @@ .gf-timepicker-nav-btn { max-width: 120px; } + + .dashnav-move-timeframe { + display: none; + } + .panel-in-fullscreen { .dashnav-action-icons { display: none; @@ -60,6 +66,9 @@ .dashnav-zoom-out { display: block; } + .dashnav-move-timeframe { + display: block; + } } @include media-breakpoint-up(xl) { diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 84b01692069..560f2d2dab1 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -27,7 +27,7 @@ $white: #fff; // ------------------------- $blue: #33B5E5; $blue-dark: #005f81; -$green: #669900; +$green: #609000; $red: #CC3900; $yellow: #ECBB13; $pink: #FF4444; @@ -39,6 +39,7 @@ $brand-primary: $orange; $brand-success: $green; $brand-warning: $brand-primary; $brand-danger: $red; +$brand-text-highlight: #f7941d; // Status colors // ------------------------- @@ -48,12 +49,17 @@ $critical: #ed2e18; // Scaffolding // ------------------------- -$body-bg: rgb(20,20,20); -$page-bg: $dark-2; -$body-color: $gray-4; -$text-color: $gray-4; -$text-color-strong: $white; -$text-color-weak: $gray-2; +$body-bg: rgb(20,20,20); +$page-bg: $dark-2; +$body-color: $gray-4; +$text-color: $gray-4; +$text-color-strong: $white; +$text-color-weak: $gray-2; +$text-color-faint: $gray-1; +$text-color-emphasis: $gray-5; + +$text-shadow-strong: 1px 1px 4px $black; +$text-shadow-faint: 1px 1px 4px rgb(45, 45, 45); // gradients $brand-gradient: linear-gradient(to right, rgba(255,213,0,0.7) 0%, rgba(255,68,0,0.7) 99%, rgba(255,68,0,0.7) 100%); @@ -64,6 +70,7 @@ $page-gradient: linear-gradient(60deg, transparent 70%, darken($page-bg, 4%) 98% $link-color: darken($white,11%); $link-color-disabled: darken($link-color,30%); $link-hover-color: $white; +$external-link-color: $blue; // Typography // ------------------------- @@ -95,7 +102,9 @@ $tight-form-func-bg: #333; $tight-form-func-highlight-bg: #444; $modal-background: $black; -$code-tag-bg: #444; +$code-tag-bg: $dark-5; +$code-tag-border: lighten($code-tag-bg, 2%); + // Lists $grafanaListBackground: $dark-3; @@ -124,7 +133,7 @@ $btn-primary-bg: $brand-primary; $btn-primary-bg-hl: lighten($brand-primary, 8%); $btn-secondary-bg: $blue-dark; -$btn-secondary-bg-hl: lighten($blue-dark, 3%); +$btn-secondary-bg-hl: lighten($blue-dark, 5%); $btn-success-bg: lighten($green, 3%); $btn-success-bg-hl: darken($green, 3%); @@ -235,14 +244,6 @@ $successBackground: $btn-success-bg; $infoText: $blue-dark; $infoBackground: $blue-dark; -// Tooltips and popovers -// ------------------------- -$tooltipColor: $white; -$tooltipBackground: rgb(58, 57, 57); -$tooltipArrowWidth: 5px; -$tooltipArrowColor: $tooltipBackground; -$tooltipLinkColor: $link-color; - // popover $popover-bg: $dark-4; $popover-color: $text-color; @@ -250,6 +251,16 @@ $popover-color: $text-color; $popover-help-bg: $btn-secondary-bg; $popover-help-color: $text-color; + +// Tooltips and popovers +// ------------------------- +$tooltipColor: $popover-help-color; +$tooltipBackground: $popover-help-bg; +$tooltipArrowWidth: 5px; +$tooltipArrowColor: $tooltipBackground; +$tooltipLinkColor: $link-color; +$graph-tooltip-bg: $dark-4; + // images $checkboxImageUrl: '../img/checkbox.png'; @@ -257,5 +268,3 @@ $checkboxImageUrl: '../img/checkbox.png'; $card-background: linear-gradient(135deg, #2f2f2f, #262626); $card-background-hover: linear-gradient(135deg, #343434, #262626); $card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .3); - - diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index 279e284b0be..a7b5c72af2b 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -44,6 +44,7 @@ $brand-primary: $orange; $brand-success: $green; $brand-warning: $orange; $brand-danger: $red; +$brand-text-highlight: #f7941d; // Status colors // ------------------------- @@ -54,12 +55,17 @@ $critical: #EC2128; // Scaffolding // ------------------------- -$body-bg: $white; -$page-bg: $white; -$body-color: $gray-1; -$text-color: $gray-1; -$text-color-strong: $white; -$text-color-weak: $gray-1; +$body-bg: $white; +$page-bg: $white; +$body-color: $gray-1; +$text-color: $gray-1; +$text-color-strong: $white; +$text-color-weak: $gray-1; +$text-color-faint: $gray-3; +$text-color-emphasis: $dark-5; + +$text-shadow-strong: none; +$text-shadow-faint: none; // gradients $brand-gradient: linear-gradient(to right, rgba(255,213,0,1.0) 0%, rgba(255,68,0,1.0) 99%, rgba(255,68,0,1.0) 100%); @@ -70,6 +76,7 @@ $page-gradient: linear-gradient(60deg, transparent 70%, darken($page-bg, 4%) 98% $link-color: $gray-1; $link-color-disabled: lighten($link-color, 30%); $link-hover-color: darken($link-color, 20%); +$external-link-color: $blue; // Typography // ------------------------- @@ -102,7 +109,8 @@ $tight-form-func-bg: $gray-5; $tight-form-func-highlight-bg: $gray-6; $modal-background: $body-bg; -$code-tag-bg: $dark-5; +$code-tag-bg: $gray-6; +$code-tag-border: darken($code-tag-bg, 3%); // Lists $grafanaListBackground: $gray-6; @@ -261,22 +269,22 @@ $infoText: $blue; $infoBackground: $blue-dark; $infoBorder: transparent; - -// Tooltips and popovers -// ------------------------- -$tooltipColor: $text-color; -$tooltipBackground: $gray-5; -$tooltipArrowWidth: 5px; -$tooltipArrowColor: $tooltipBackground; -$tooltipLinkColor: $text-color; - // popover -$popover-bg: $gray-5; -$popover-color: $text-color; +$popover-bg: $gray-5; +$popover-color: $text-color; $popover-help-bg: $blue-dark; $popover-help-color: $gray-6; +// Tooltips and popovers +// ------------------------- +$tooltipColor: $popover-help-color; +$tooltipBackground: $popover-help-bg; +$tooltipArrowWidth: 5px; +$tooltipArrowColor: $tooltipBackground; +$tooltipLinkColor: lighten($popover-help-color, 5%); +$graph-tooltip-bg: $gray-5; + // images $checkboxImageUrl: '../img/checkbox_white.png'; @@ -284,4 +292,3 @@ $checkboxImageUrl: '../img/checkbox_white.png'; $card-background: linear-gradient(135deg, $gray-5, $gray-6); $card-background-hover: linear-gradient(135deg, $gray-6, $gray-7); $card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .1); - diff --git a/public/sass/base/_code.scss b/public/sass/base/_code.scss index db7a8cfab18..eb085c0577c 100644 --- a/public/sass/base/_code.scss +++ b/public/sass/base/_code.scss @@ -10,29 +10,39 @@ pre { font-size: $font-size-base - 2; background-color: $code-tag-bg; color: $text-color; - border: 1px solid darken($code-tag-bg, 15%); - padding: 2px; + border: 1px solid $code-tag-border; + padding: 10px; + border-radius: 4px; } // Inline code code { - color: #d14; - background-color: #f7f7f9; - border: 1px solid #e1e1e8; + color: $text-color; + background-color: $code-tag-bg; + border: 1px solid darken($code-tag-bg, 15%); white-space: nowrap; } +code.code--small { + font-size: $font-size-xs; + padding: 5px; + margin: 0 2px; +} + +p.code--line { + line-height: 1.8; +} + // Blocks of code pre { display: block; - margin: 0 0 $line-height-base / 2; - font-size: $font-size-base - 1; // 14px to 13px + margin: 0 0 $line-height-base; line-height: $line-height-base; word-break: break-all; word-wrap: break-word; white-space: pre; white-space: pre-wrap; - background-color: #f5f5f5; + background-color: $code-tag-bg; // Make prettyprint styles more spaced out for readability &.prettyprint { @@ -49,4 +59,3 @@ pre { border: 0; } } - diff --git a/public/sass/base/_font_awesome.scss b/public/sass/base/_font_awesome.scss index 438185f3198..51d9bac987d 100644 --- a/public/sass/base/_font_awesome.scss +++ b/public/sass/base/_font_awesome.scss @@ -66,12 +66,6 @@ border: solid 0.08em #eeeeee; border-radius: .1em; } -.pull-right { - float: right; -} -.pull-left { - float: left; -} .fa.pull-left { margin-right: .3em; } diff --git a/public/sass/base/_forms.scss b/public/sass/base/_forms.scss index 4f1d0778e0b..3891adc4085 100644 --- a/public/sass/base/_forms.scss +++ b/public/sass/base/_forms.scss @@ -174,7 +174,7 @@ label.cr1 { cursor:pointer; } -input[type="checkbox"]:checked+label { +input[type="checkbox"].cr1:checked+label { background: url($checkboxImageUrl) 0px -18px no-repeat; } diff --git a/public/sass/base/_grafana_icons.scss b/public/sass/base/_grafana_icons.scss index e442cbdce6c..f7f3a0368ee 100644 --- a/public/sass/base/_grafana_icons.scss +++ b/public/sass/base/_grafana_icons.scss @@ -169,7 +169,7 @@ .icon-gf-scale:before { content: "\e906"; } -.icon-gf-litmus:before { +.icon-gf-worldping:before { content: "\e627"; } .icon-gf-grafana_wordmark:before { diff --git a/public/sass/base/_type.scss b/public/sass/base/_type.scss index e819f71b540..904dfa13b4e 100644 --- a/public/sass/base/_type.scss +++ b/public/sass/base/_type.scss @@ -114,7 +114,7 @@ hr { small, .small { - font-size: 85%; + font-size: $font-size-sm; font-weight: normal; } diff --git a/public/sass/components/_cards.scss b/public/sass/components/_cards.scss index 0d4d1ae88c9..c64fbbe8aa7 100644 --- a/public/sass/components/_cards.scss +++ b/public/sass/components/_cards.scss @@ -76,13 +76,20 @@ } .card-item-header { + margin-bottom: $spacer; +} + +.card-item-type { color: $text-color-weak; text-transform: uppercase; - margin-bottom: $spacer; font-size: $font-size-sm; font-weight: bold; } +.card-item-notice { + font-size: $font-size-sm; +} + .card-item-name { color: $headings-color; overflow: hidden; @@ -97,13 +104,39 @@ width: 100%; } +.card-item-sub-name--header { + color: $text-color-weak; + text-transform: uppercase; + margin-bottom: $spacer; + font-size: $font-size-sm; + font-weight: bold; +} + .card-list-layout-grid { + .card-item-type { + display: inline-block; + } + + .card-item-notice { + font-size: $font-size-sm; + display: inline-block; + margin-left: $spacer; + } + + .card-item-header-action { + float: right; + } + .card-item-wrapper { width: 100%; padding: 0 1.5rem 1.5rem 0rem; } + .card-item-wrapper--clickable { + cursor: pointer; + } + .card-item-figure { margin: 0 $spacer $spacer 0; height: 6rem; @@ -128,6 +161,14 @@ width: 33.333333%; } } + + &.card-list-layout-grid--max-2-col { + @include media-breakpoint-up(lg) { + .card-item-wrapper { + width: 50%; + } + } + } } .card-list-layout-list { @@ -137,6 +178,10 @@ width: 100%; } + .card-item-wrapper--clickable { + cursor: pointer; + } + .card-item { border-bottom: .2rem solid $page-bg; border-radius: 0; @@ -145,6 +190,7 @@ .card-item-header { float: right; + text-align: right; } .card-item-figure { @@ -166,4 +212,3 @@ margin-right: 0; } } - diff --git a/public/sass/components/_drop.scss b/public/sass/components/_drop.scss index 34c53557d45..570c1862ef0 100644 --- a/public/sass/components/_drop.scss +++ b/public/sass/components/_drop.scss @@ -1,4 +1,4 @@ -$popover-arrow-size: 1rem; +$popover-arrow-size: 0.7rem; $color: inherit; $backgroundColor: $btn-secondary-bg; $color: $text-color; @@ -7,6 +7,7 @@ $attachmentOffset: 0%; $easing: cubic-bezier(0, 0, 0.265, 1.00); .drop-element { + z-index: 10000; position: absolute; display: none; opacity: 0; @@ -15,11 +16,7 @@ $easing: cubic-bezier(0, 0, 0.265, 1.00); display: block; } - &.drop-open.drop-help.drop-out-of-bounds, - &.drop-open-transitionend.drop-help.drop-out-of-bounds { - display: none; - } -} + } .drop-element, .drop-element * { box-sizing: border-box; @@ -38,6 +35,12 @@ $easing: cubic-bezier(0, 0, 0.265, 1.00); } } +.drop-hide-out-of-bounds { + &.drop-open.drop-help.drop-out-of-bounds, + &.drop-open-transitionend.drop-help.drop-out-of-bounds { + display: none; + } +} @include drop-theme("help", $popover-help-bg, $popover-help-color); @include drop-theme("popover", $popover-bg, $popover-color); diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss index b94fe5fc61e..d3d5a9a07c9 100644 --- a/public/sass/components/_gf-form.scss +++ b/public/sass/components/_gf-form.scss @@ -1,5 +1,4 @@ $gf-form-margin: 0.25rem; -$gf-form-label-margin: 0.25rem; .gf-form { margin-bottom: $gf-form-margin; @@ -7,9 +6,14 @@ $gf-form-label-margin: 0.25rem; flex-direction: row; align-items: center; text-align: left; + position: relative; - .cr1 { - margin-left: 8px; + &--offset-1 { + margin-left: $spacer; + } + + &--grow { + flex-grow: 1; } } @@ -22,10 +26,6 @@ $gf-form-label-margin: 0.25rem; flex-direction: row; flex-wrap: wrap; align-content: flex-start; - - .gf-form-flex { - flex-grow: 1; - } } .gf-form-button-row { @@ -37,16 +37,23 @@ $gf-form-label-margin: 0.25rem; .gf-form-label { padding: $input-padding-y $input-padding-x; + margin-right: $gf-form-margin; line-height: $input-line-height; flex-shrink: 0; background-color: $input-label-bg; display: block; font-size: $font-size-sm; - margin-right: $gf-form-label-margin; + margin-right: $gf-form-margin; border: $input-btn-border-width solid transparent; @include border-radius($label-border-radius-sm); + + + &--grow { + flex-grow: 1; + min-height: 2.7rem; + } } .gf-form-checkbox { @@ -109,6 +116,7 @@ $gf-form-label-margin: 0.25rem; } .gf-form-select-wrapper { + margin-right: $gf-form-margin; position: relative; background-color: $input-bg; @@ -137,20 +145,112 @@ $gf-form-label-margin: 0.25rem; content: '\f0d7'; pointer-events: none; } + + &--has-help-icon { + &:after { + right: $input-padding-x*3; + } + } } -.gf-form-select-wrapper + .gf-form-select-wrapper { - margin-left: $gf-form-label-margin; +.gf-form--v-stretch { + align-items: stretch; } .gf-form-btn { padding: $input-padding-y $input-padding-x; + margin-right: $gf-form-margin; line-height: $input-line-height; + font-size: $font-size-sm; + flex-shrink: 0; flex-grow: 0; } -.query-editor-secondary-row { - margin-left: 5.2rem; - +.gf-form-switch { + margin-right: $gf-form-margin; +} + +.natural-language-input { + &input[type="number"] { + font-size: $font-size-base; + line-height: $input-line-height; + margin: -6px -5px 0 5px; + padding: $input-padding-y/2 $input-padding-x/2; + } +} + +.gf-form-dropdown-typeahead { + margin-right: $gf-form-margin; + position: relative; + background-color: $input-bg; + padding-right: $input-padding-x; + + &:after { + position: absolute; + top: 35%; + right: $input-padding-x/2; + background-color: transparent; + color: $input-color; + font: normal normal normal $font-size-sm/1 FontAwesome; + content: '\f0d7'; + pointer-events: none; + } +} + +.gf-form-query { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-content: flex-start; + align-items: flex-start; + + .gf-form, + .gf-form-filler { + margin-bottom: 2px; + } + + .gf-form-switch input, + .gf-form-switch label, + .gf-form-input, + .gf-form-select-wrapper, + .gf-form-filler, + .gf-form-label { + margin-right: 2px; + } +} + +.gf-form-query-content { + flex-grow: 1; + + &--collapsed { + overflow: hidden; + + .gf-form-label { + overflow: hidden; + text-overflow: ellipsis; + width: 100%; + white-space: nowrap; + } + } +} + +.gf-form-help-icon { + flex-grow: 0; + padding-left: $spacer; + color: $text-color-weak; + + &--right-absolute { + position: absolute; + right: $spacer; + top: 8px; + } + + &--right-normal { + float: right; + } +} + +select.gf-form-input ~ .gf-form-help-icon { + right: 10px; } diff --git a/public/sass/components/_modals.scss b/public/sass/components/_modals.scss index c71302f34f1..357840062eb 100644 --- a/public/sass/components/_modals.scss +++ b/public/sass/components/_modals.scss @@ -127,6 +127,7 @@ .share-modal-options { margin: 11px 20px 33px 20px; + display: inline-block; } .share-modal-big-icon { @@ -162,8 +163,3 @@ } } -.modal-body { - .position-center { - display: inline-block; - } -} diff --git a/public/sass/components/_navbar.scss b/public/sass/components/_navbar.scss index 1034cb59958..7ac46505f1b 100644 --- a/public/sass/components/_navbar.scss +++ b/public/sass/components/_navbar.scss @@ -131,6 +131,11 @@ font-size: 20px; line-height: 8px; } + + > img { + max-width: 27px; + max-height: 27px; + } } .sidemenu-pinned { diff --git a/public/sass/components/_old_stuff.scss b/public/sass/components/_old_stuff.scss index eae5eb558ce..bb53b657862 100644 --- a/public/sass/components/_old_stuff.scss +++ b/public/sass/components/_old_stuff.scss @@ -1,9 +1,9 @@ -div.editor-row { +.editor-row { vertical-align: top; } -div.editor-row div.section { +.section { margin-right: 20px; vertical-align: top; display: inline-block; diff --git a/public/sass/components/_panel_dashlist.scss b/public/sass/components/_panel_dashlist.scss index f00b59735c1..dfb11dfb848 100644 --- a/public/sass/components/_panel_dashlist.scss +++ b/public/sass/components/_panel_dashlist.scss @@ -1,5 +1,10 @@ -.dashlist-item { +.dashlist-section-header { + margin-bottom: $spacer; + color: $text-color-weak; +} +.dashlist-section { + margin-bottom: $spacer; } .dashlist-link { @@ -7,7 +12,6 @@ margin: 5px; padding: 7px; background-color: $tight-form-bg; - border: 1px solid $tight-form-border; .fa { float: right; padding-top: 3px; diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index ccecbc6c9a7..ca79034a0bd 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -74,6 +74,10 @@ float: left; white-space: nowrap; padding-left: 10px; + + &--right-y { + float: right; + } } .graph-legend-value { @@ -83,14 +87,22 @@ .graph-legend-table { overflow-y: scroll; - .graph-legend-series { display: table-row; + .graph-legend-series { + display: table-row; float: none; padding-left: 0; - &.pull-right { + &--right-y { float: none; + + .graph-legend-alias:after { + content: '(right-y)'; + padding: 0 5px; + color: $text-color-weak; + } } } + td, .graph-legend-alias, .graph-legend-icon, .graph-legend-value { float: none; display: table-cell; @@ -222,30 +234,35 @@ .graph-tooltip { white-space: nowrap; + font-size: $font-size-sm; + background-color: $graph-tooltip-bg; + color: $text-color; .graph-tooltip-time { text-align: center; - font-weight: $font-weight-semi-bold; position: relative; top: -3px; - } - - .tone-down { - opacity: 0.7; + padding: 0.2rem; } .graph-tooltip-list-item { display: table-row; + + &--highlight { + color: $text-color-emphasis; + font-weight: bold; + } } .graph-tooltip-series-name { display: table-cell; + padding: 0.15rem; } .graph-tooltip-value { display: table-cell; font-weight: bold; - padding-left: 10px; + padding-left: 15px; text-align: right; } } diff --git a/public/sass/components/_panel_pluginlist.scss b/public/sass/components/_panel_pluginlist.scss new file mode 100644 index 00000000000..605e1afdb6a --- /dev/null +++ b/public/sass/components/_panel_pluginlist.scss @@ -0,0 +1,75 @@ +.pluginlist-section-header { + margin-bottom: $spacer; + color: $text-color-weak; +} + +.pluginlist-section { + margin-bottom: $spacer; +} + +.pluginlist-link { + display: block; + margin: 5px; + padding: 7px; + background-color: $tight-form-bg; + + &:hover { + background-color: $tight-form-func-bg; + } +} + +.pluginlist-icon { + vertical-align: sub; + font-size: $font-size-h1; + margin-right: $spacer / 2; +} + +.pluginlist-image { + width: 20px; +} + +.pluginlist-title { + margin-right: $spacer / 3; +} + +.pluginlist-version { + font-size: $font-size-sm; + color: $text-color-weak; +} + +.pluginlist-message { + float: right; + font-size: $font-size-sm; +} + +.pluginlist-message--update { + &:hover { + border-bottom: 1px solid $text-color; + } +} + +.pluginlist-message--enable{ + color: $external-link-color; + &:hover { + border-bottom: 1px solid $external-link-color; + } +} + +.pluginlist-message--no-update { + color: $text-color-weak; +} + +.pluginlist-emphasis { + font-weight: 600; +} + +.pluginlist-none-installed { + color: $text-color-weak; + font-size: $font-size-sm; +} + +.pluginlist-inline-logo { + vertical-align: sub; + margin-right: $spacer / 3; + width: 16px; +} diff --git a/public/sass/components/_query_part.scss b/public/sass/components/_query_part.scss new file mode 100644 index 00000000000..1e2fb9622c2 --- /dev/null +++ b/public/sass/components/_query_part.scss @@ -0,0 +1,11 @@ + +.query-part { + background-color: $input-bg !important; + + &.show-function-controls { + padding-top: 5px; + min-width: 100px; + text-align: center; + } +} + diff --git a/public/sass/components/_sidemenu.scss b/public/sass/components/_sidemenu.scss index f937ca9c284..685406bb098 100644 --- a/public/sass/components/_sidemenu.scss +++ b/public/sass/components/_sidemenu.scss @@ -30,7 +30,7 @@ min-height: calc(100% - 54px); } .dashboard-container { - padding-left: $side-menu-width + 0.2rem; + padding-left: $side-menu-width + 0.5rem; } .page-container { margin-left: $side-menu-width; diff --git a/public/sass/components/_switch.scss b/public/sass/components/_switch.scss new file mode 100644 index 00000000000..4216029bab2 --- /dev/null +++ b/public/sass/components/_switch.scss @@ -0,0 +1,91 @@ +$switch-border-radius: 1rem; +$switch-width: 3.5rem; +$switch-height: 1.5rem; + +/* ============================================================ + SWITCH 3 - YES NO +============================================================ */ + +.gf-form-switch { + position: relative; + max-width: 4.5rem; + flex-grow: 1; + min-width: 4.0rem; + + input { + position: absolute; + margin-left: -9999px; + visibility: hidden; + display: none; + } + + input + label { + display: block; + position: relative; + cursor: pointer; + outline: none; + user-select: none; + width: 100%; + height: 2.65rem; + background-color: $page-bg; + } + + input + label:before, input + label:after { + @include buttonBackground($input-bg, $input-bg); + + display: block; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + color: #fff; + font-size: $font-size-sm; + text-align: center; + line-height: 2.8rem; + font-size: 150%; + } + + &:hover { + input + label:before { + @include buttonBackground($input-bg, lighten($input-bg, 5%)); + color: $text-color; + text-shadow: $text-shadow-faint; + } + + input + label:after { + @include buttonBackground($input-bg, lighten($input-bg, 5%)); + color: lighten($orange, 10%); + text-shadow: $text-shadow-strong; + } + + } + + input + label:before { + font-family: 'FontAwesome'; + content: "\f096"; // square-o + color: $text-color-faint; + transition: transform 0.4s; + backface-visibility: hidden; + text-shadow: $text-shadow-faint; + } + + input + label:after { + content: "\f046"; // check-square-o + color: $orange; + text-shadow: $text-shadow-strong; + + font-family: 'FontAwesome'; + transition: transform 0.4s; + transform: rotateY(180deg); + backface-visibility: hidden; + } + + input:checked + label:before { + transform: rotateY(180deg); + } + + input:checked + label:after { + transform: rotateY(0); + } +} diff --git a/public/sass/components/_tabbed_view.scss b/public/sass/components/_tabbed_view.scss index c72dd1d4388..6c8a42a8059 100644 --- a/public/sass/components/_tabbed_view.scss +++ b/public/sass/components/_tabbed_view.scss @@ -1,7 +1,7 @@ .tabbed-view { background-color: $page-bg; background-image: $page-gradient; - margin: -($panel-margin*2); + margin: (-$panel-margin*2) (-$panel-margin); margin-bottom: $spacer*2; padding: $spacer*3; diff --git a/public/sass/components/_timepicker.scss b/public/sass/components/_timepicker.scss index 776c69a6fac..8bfbdef4cff 100644 --- a/public/sass/components/_timepicker.scss +++ b/public/sass/components/_timepicker.scss @@ -115,3 +115,15 @@ @extend .fa; @extend .fa-chevron-left; } + +.gf-timepicker-time-control { + font-size: $font-size-sm; + a { + padding: 18px 7px 13px !important; + } +} + +.dashnav-move-timeframe { + position: relative; + top: 1px; +} diff --git a/public/sass/components/_tooltip.scss b/public/sass/components/_tooltip.scss index e70e1010bf1..f710dc68931 100644 --- a/public/sass/components/_tooltip.scss +++ b/public/sass/components/_tooltip.scss @@ -2,7 +2,6 @@ // Tooltips // -------------------------------------------------- - // Base class .tooltip { position: absolute; @@ -27,6 +26,7 @@ text-align: center; text-decoration: none; background-color: $tooltipBackground; + border-radius: 2px; } // Arrows @@ -37,6 +37,7 @@ border-color: transparent; border-style: solid; } + .tooltip { &.top .tooltip-arrow { bottom: 0; @@ -88,13 +89,6 @@ } } -.grafana-tooltip hr { - padding: 2px; - color: #c8c8c8; - margin: 0px; - border-bottom: 0px solid #c8c8c8; -} - .grafana-tip { padding-left: 5px; } diff --git a/public/sass/mixins/_drop_element.scss b/public/sass/mixins/_drop_element.scss index d08e50e63b8..290e49f4cad 100644 --- a/public/sass/mixins/_drop_element.scss +++ b/public/sass/mixins/_drop_element.scss @@ -10,7 +10,7 @@ font-family: inherit; background: $theme-bg; color: $theme-color; - padding: $spacer; + padding: 0.65rem; font-size: $font-size-sm; max-width: 20rem; diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index 2b04f0e9b29..8fb1e6bcdaa 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -96,7 +96,8 @@ .add-row-panel-hint, .dashnav-refresh-action, .dashnav-zoom-out, - .dashnav-action-icons { + .dashnav-action-icons, + .dashnav-move-timeframe { display: none; } @@ -278,11 +279,11 @@ div.flot-text { .dashboard-header { font-family: $headings-font-family; - font-size: $font-size-h2; + font-size: $font-size-h3; text-align: center; span { display: inline-block; @include brand-bottom-border(); - padding: 1.2rem .5rem .4rem .5rem; + padding: 0.5rem .5rem .2rem .5rem; } } diff --git a/public/sass/pages/_login.scss b/public/sass/pages/_login.scss index 926e8c21f6c..8f376dfae71 100644 --- a/public/sass/pages/_login.scss +++ b/public/sass/pages/_login.scss @@ -134,7 +134,7 @@ border-bottom: 1px solid $gray-1; .login-divider-text { - background-color: $dark-3; + background-color: $panel-bg; color: $gray-2; padding: 0 10px; } @@ -192,4 +192,3 @@ } } } - diff --git a/public/sass/pages/_playlist.scss b/public/sass/pages/_playlist.scss index 412fbcaef50..d0d8f04f130 100644 --- a/public/sass/pages/_playlist.scss +++ b/public/sass/pages/_playlist.scss @@ -118,7 +118,7 @@ .playlist-column-header { border-bottom: thin solid $gray-1; - padding-bottom: 10px; + padding-bottom: 3px; margin-bottom: 15px; } diff --git a/public/sass/utils/_utils.scss b/public/sass/utils/_utils.scss index efccb434d48..055fe96a213 100644 --- a/public/sass/utils/_utils.scss +++ b/public/sass/utils/_utils.scss @@ -6,6 +6,11 @@ color: $brand-primary; } +.emphasis-word { + font-weight: 500; + color: $text-color-emphasis; +} + // Close icons // -------------------------------------------------- .close { @@ -47,6 +52,7 @@ button.close { .pull-right { float: right !important; } + .pull-left { float: left !important; } diff --git a/public/sass/utils/_widths.scss b/public/sass/utils/_widths.scss index bd6fc78ef90..cf324b35c72 100644 --- a/public/sass/utils/_widths.scss +++ b/public/sass/utils/_widths.scss @@ -6,14 +6,20 @@ // widths @for $i from 1 through 30 { .width-#{$i} { - width: ($spacer * $i) - $gf-form-margin; + width: ($spacer * $i) - $gf-form-margin !important; } } @for $i from 1 through 30 { .max-width-#{$i} { - max-width: ($spacer * $i) - $gf-form-margin; + max-width: ($spacer * $i) - $gf-form-margin !important; flex-grow: 1; } } +@for $i from 1 through 30 { + .offset-width-#{$i} { + margin-left: ($spacer * $i) !important; + } +} + diff --git a/public/test/core/utils/emitter_specs.ts b/public/test/core/utils/emitter_specs.ts new file mode 100644 index 00000000000..f7076c46719 --- /dev/null +++ b/public/test/core/utils/emitter_specs.ts @@ -0,0 +1,50 @@ +import {describe, beforeEach, it, sinon, expect} from 'test/lib/common' + +import {Emitter} from 'app/core/core'; + +describe("Emitter", () => { + + describe('given 2 subscribers', () => { + + it('should notfiy subscribers', () => { + var events = new Emitter(); + var sub1Called = false; + var sub2Called = false; + + events.on('test', () => { + sub1Called = true; + }); + events.on('test', () => { + sub2Called = true; + }); + + events.emit('test', null); + + expect(sub1Called).to.be(true); + expect(sub2Called).to.be(true); + }); + + it.only('should handle errors', () => { + var events = new Emitter(); + var sub1Called = 0; + var sub2Called = 0; + + events.on('test', () => { + sub1Called++; + throw "hello"; + }); + + events.on('test', () => { + sub2Called++; + }); + + try { events.emit('test', null); } catch (_) { } + try { events.emit('test', null); } catch (_) {} + + expect(sub1Called).to.be(2); + expect(sub2Called).to.be(0); + }); + }); +}); + + diff --git a/public/test/specs/app_specs.ts b/public/test/specs/app_specs.ts new file mode 100644 index 00000000000..3c57f2d625a --- /dev/null +++ b/public/test/specs/app_specs.ts @@ -0,0 +1,14 @@ +import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'; + +import {GrafanaApp} from 'app/app'; + +describe('GrafanaApp', () => { + + var app = new GrafanaApp(); + + it('can call inits', () => { + expect(app).to.not.be(null); + }); +}); + + diff --git a/public/test/specs/dashboardSrv-specs.js b/public/test/specs/dashboardSrv-specs.js index 86f54f274fb..6fc53f09190 100644 --- a/public/test/specs/dashboardSrv-specs.js +++ b/public/test/specs/dashboardSrv-specs.js @@ -7,6 +7,10 @@ define([ var _dashboardSrv; beforeEach(module('grafana.services')); + beforeEach(module(function($provide) { + $provide.value('contextSrv', { + }); + })); beforeEach(inject(function(dashboardSrv) { _dashboardSrv = dashboardSrv; @@ -122,7 +126,10 @@ define([ { panels: [ { - type: 'graphite', legend: true, aliasYAxis: { test: 2 }, grid: { min: 1, max: 10 }, + type: 'graph', legend: true, aliasYAxis: { test: 2 }, + y_formats: ['kbyte', 'ms'], + grid: {min: 1, max: 10, rightMin: 5, rightMax: 15, leftLogBase: 1, rightLogBase: 2}, + leftYAxisLabel: 'left label', targets: [{refId: 'A'}, {}], }, { @@ -172,11 +179,6 @@ define([ expect(graph.legend.show).to.be(true); }); - it('update grid options', function() { - expect(graph.grid.leftMin).to.be(1); - expect(graph.grid.leftMax).to.be(10); - }); - it('move aliasYAxis to series override', function() { expect(graph.seriesOverrides[0].alias).to.be("test"); expect(graph.seriesOverrides[0].yaxis).to.be(2); @@ -193,8 +195,24 @@ define([ expect(table.styles[1].thresholds[1]).to.be("300"); }); + it('graph grid to yaxes options', function() { + expect(graph.yaxes[0].min).to.be(1); + expect(graph.yaxes[0].max).to.be(10); + expect(graph.yaxes[0].format).to.be('kbyte'); + expect(graph.yaxes[0].label).to.be('left label'); + expect(graph.yaxes[0].logBase).to.be(1); + expect(graph.yaxes[1].min).to.be(5); + expect(graph.yaxes[1].max).to.be(15); + expect(graph.yaxes[1].format).to.be('ms'); + expect(graph.yaxes[1].logBase).to.be(2); + + expect(graph.grid.rightMax).to.be(undefined); + expect(graph.grid.rightLogBase).to.be(undefined); + expect(graph.y_formats).to.be(undefined); + }); + it('dashboard schema version should be set to latest', function() { - expect(model.schemaVersion).to.be(11); + expect(model.schemaVersion).to.be(12); }); }); @@ -248,6 +266,8 @@ define([ rows: [{ panels: [{ type: 'graph', + grid: {}, + yaxes: [{}, {}], targets: [{ "alias": "$tag_datacenter $tag_source $col", "column": "value", diff --git a/public/test/specs/dynamicDashboardSrv-specs.js b/public/test/specs/dynamicDashboardSrv-specs.js index 0104081b768..b988203009a 100644 --- a/public/test/specs/dynamicDashboardSrv-specs.js +++ b/public/test/specs/dynamicDashboardSrv-specs.js @@ -12,6 +12,11 @@ define([ ctx.setup = function (setupFunc) { beforeEach(module('grafana.services')); + beforeEach(module(function($provide) { + $provide.value('contextSrv', { + user: { timezone: 'utc'} + }); + })); beforeEach(inject(function(dynamicDashboardSrv, dashboardSrv) { ctx.dynamicDashboardSrv = dynamicDashboardSrv; @@ -45,10 +50,10 @@ define([ value: ['se1', 'se2', 'se3'] }, options: [ - {text: 'se1', value: 'se1', selected: true}, - {text: 'se2', value: 'se2', selected: true}, - {text: 'se3', value: 'se3', selected: true}, - {text: 'se4', value: 'se4', selected: false} + {text: 'se1', value: 'se1', selected: true}, + {text: 'se2', value: 'se2', selected: true}, + {text: 'se3', value: 'se3', selected: true}, + {text: 'se4', value: 'se4', selected: false} ] }); }); @@ -93,7 +98,7 @@ define([ describe('After a second iteration and selected values reduced', function() { beforeEach(function() { ctx.dash.templating.list[0].options[1].selected = false; - + ctx.dynamicDashboardSrv.update(ctx.dash); }); diff --git a/public/test/specs/value_select_dropdown_specs.js b/public/test/specs/value_select_dropdown_specs.js index 3e6397ab50c..2bca4aaaf5c 100644 --- a/public/test/specs/value_select_dropdown_specs.js +++ b/public/test/specs/value_select_dropdown_specs.js @@ -11,7 +11,7 @@ function () { var rootScope; beforeEach(module('grafana.core')); - beforeEach(inject(function($controller, $rootScope, $q) { + beforeEach(inject(function($controller, $rootScope, $q, $httpBackend) { rootScope = $rootScope; scope = $rootScope.$new(); ctrl = $controller('ValueSelectDropdownCtrl', {$scope: scope}); @@ -19,6 +19,7 @@ function () { return $q.when(tagValuesMap[obj.tagKey]); }; ctrl.onUpdated = sinon.spy(); + $httpBackend.when('GET', /\.html$/).respond(''); })); describe("Given simple variable", function() { diff --git a/public/test/test-main.js b/public/test/test-main.js index 5cc7b25dd6c..d40955022fe 100644 --- a/public/test/test-main.js +++ b/public/test/test-main.js @@ -10,6 +10,7 @@ baseURL: '/base/', defaultJSExtensions: true, paths: { + 'eventemitter3': 'vendor/npm/eventemitter3/index.js', 'tether': 'vendor/npm/tether/dist/js/tether.js', 'tether-drop': 'vendor/npm/tether-drop/dist/js/drop.js', 'moment': 'vendor/moment.js', @@ -58,7 +59,11 @@ 'vendor/angular-mocks/angular-mocks.js': { format: 'global', deps: ['angular'], - } + }, + 'vendor/npm/eventemitter3/index.js': { + format: 'cjs', + exports: 'EventEmitter' + }, } }); diff --git a/public/vendor/angular-mocks/.bower.json b/public/vendor/angular-mocks/.bower.json index 9831478983e..03fc650628a 100644 --- a/public/vendor/angular-mocks/.bower.json +++ b/public/vendor/angular-mocks/.bower.json @@ -1,20 +1,20 @@ { "name": "angular-mocks", - "version": "1.5.1-build.4601+sha.c966876", + "version": "1.5.3", "license": "MIT", "main": "./angular-mocks.js", "ignore": [], "dependencies": { - "angular": "1.5.1-build.4601+sha.c966876" + "angular": "1.5.3" }, "homepage": "https://github.com/angular/bower-angular-mocks", - "_release": "1.5.1-build.4601+sha.c966876", + "_release": "1.5.3", "_resolution": { "type": "version", - "tag": "v1.5.1-build.4601+sha.c966876", - "commit": "ff7c5c2ac686293829786d26d844391e45c37c11" + "tag": "v1.5.3", + "commit": "319557fe710cecc11e12c772cc1abb8098d29ccb" }, "_source": "git://github.com/angular/bower-angular-mocks.git", - "_target": "~1.5.1", + "_target": "~1.5.3", "_originalSource": "angular-mocks" } \ No newline at end of file diff --git a/public/vendor/angular-mocks/angular-mocks.js b/public/vendor/angular-mocks/angular-mocks.js index ff28e1a7892..a1fc2fcf7ae 100644 --- a/public/vendor/angular-mocks/angular-mocks.js +++ b/public/vendor/angular-mocks/angular-mocks.js @@ -1,5 +1,5 @@ /** - * @license AngularJS v1.5.1-build.4601+sha.c966876 + * @license AngularJS v1.5.3 * (c) 2010-2016 Google, Inc. http://angularjs.org * License: MIT */ @@ -134,12 +134,12 @@ angular.mock.$Browser = function() { }; angular.mock.$Browser.prototype = { -/** - * @name $browser#poll - * - * @description - * run all fns in pollFns - */ + /** + * @name $browser#poll + * + * @description + * run all fns in pollFns + */ poll: function poll() { angular.forEach(this.pollFns, function(pollFn) { pollFn(); @@ -552,7 +552,7 @@ angular.mock.$IntervalProvider = function() { * This directive should go inside the anonymous function but a bug in JSHint means that it would * not be enacted early enough to prevent the warning. */ -var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; +var R_ISO8061_STR = /^(-?\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; function jsonStringToDate(string) { var match; @@ -578,7 +578,7 @@ function toInt(str) { return parseInt(str, 10); } -function padNumber(num, digits, trim) { +function padNumberInMock(num, digits, trim) { var neg = ''; if (num < 0) { neg = '-'; @@ -727,13 +727,13 @@ angular.mock.TzDate = function(offset, timestamp) { // provide this method only on browsers that already have it if (self.toISOString) { self.toISOString = function() { - return padNumber(self.origDate.getUTCFullYear(), 4) + '-' + - padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' + - padNumber(self.origDate.getUTCDate(), 2) + 'T' + - padNumber(self.origDate.getUTCHours(), 2) + ':' + - padNumber(self.origDate.getUTCMinutes(), 2) + ':' + - padNumber(self.origDate.getUTCSeconds(), 2) + '.' + - padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z'; + return padNumberInMock(self.origDate.getUTCFullYear(), 4) + '-' + + padNumberInMock(self.origDate.getUTCMonth() + 1, 2) + '-' + + padNumberInMock(self.origDate.getUTCDate(), 2) + 'T' + + padNumberInMock(self.origDate.getUTCHours(), 2) + ':' + + padNumberInMock(self.origDate.getUTCMinutes(), 2) + ':' + + padNumberInMock(self.origDate.getUTCSeconds(), 2) + '.' + + padNumberInMock(self.origDate.getUTCMilliseconds(), 3) + 'Z'; }; } @@ -1328,7 +1328,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { } // TODO(vojta): change params to: method, url, data, headers, callback - function $httpBackend(method, url, data, callback, headers, timeout, withCredentials) { + function $httpBackend(method, url, data, callback, headers, timeout, withCredentials, responseType) { + var xhr = new MockXhr(), expectation = expectations[0], wasExpected = false; @@ -1392,7 +1393,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { // if $browser specified, we do auto flush all requests ($browser ? $browser.defer : responsesPush)(wrapResponse(definition)); } else if (definition.passThrough) { - $delegate(method, url, data, callback, headers, timeout, withCredentials); + $delegate(method, url, data, callback, headers, timeout, withCredentials, responseType); } else throw new Error('No response defined !'); return; } @@ -2095,10 +2096,12 @@ angular.mock.$RAFDecorator = ['$delegate', function($delegate) { /** * */ +var originalRootElement; angular.mock.$RootElementProvider = function() { - this.$get = function() { - return angular.element('
    '); - }; + this.$get = ['$injector', function($injector) { + originalRootElement = angular.element('
    ').data('$injector', $injector); + return originalRootElement; + }]; }; /** @@ -2127,7 +2130,7 @@ angular.mock.$RootElementProvider = function() { * * myMod.controller('MyDirectiveController', ['$log', function($log) { * $log.info(this.name); - * })]; + * }]); * * * // In a test ... @@ -2137,7 +2140,7 @@ angular.mock.$RootElementProvider = function() { * var ctrl = $controller('MyDirectiveController', { /* no locals */ }, { name: 'Clark Kent' }); * expect(ctrl.name).toEqual('Clark Kent'); * expect($log.info.logs).toEqual(['Clark Kent']); - * }); + * })); * }); * * ``` @@ -2565,11 +2568,16 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) { }]; -if (window.jasmine || window.mocha) { +!(function(jasmineOrMocha) { + + if (!jasmineOrMocha) { + return; + } var currentSpec = null, + injectorState = new InjectorState(), annotatedFunctions = [], - isSpecRunning = function() { + wasInjectorCreated = function() { return !!currentSpec; }; @@ -2581,48 +2589,6 @@ if (window.jasmine || window.mocha) { return angular.mock.$$annotate.apply(this, arguments); }; - - (window.beforeEach || window.setup)(function() { - annotatedFunctions = []; - currentSpec = this; - }); - - (window.afterEach || window.teardown)(function() { - var injector = currentSpec.$injector; - - annotatedFunctions.forEach(function(fn) { - delete fn.$inject; - }); - - angular.forEach(currentSpec.$modules, function(module) { - if (module && module.$$hashKey) { - module.$$hashKey = undefined; - } - }); - - currentSpec.$injector = null; - currentSpec.$modules = null; - currentSpec.$providerInjector = null; - currentSpec = null; - - if (injector) { - injector.get('$rootElement').off(); - injector.get('$rootScope').$destroy(); - } - - // clean up jquery's fragment cache - angular.forEach(angular.element.fragments, function(val, key) { - delete angular.element.fragments[key]; - }); - - MockXhr.$$lastInstance = null; - - angular.forEach(angular.callbacks, function(val, key) { - delete angular.callbacks[key]; - }); - angular.callbacks.counter = 0; - }); - /** * @ngdoc function * @name angular.mock.module @@ -2643,9 +2609,9 @@ if (window.jasmine || window.mocha) { * {@link auto.$provide $provide}.value, the key being the string name (or token) to associate * with the value on the injector. */ - window.module = angular.mock.module = function() { + var module = window.module = angular.mock.module = function() { var moduleFns = Array.prototype.slice.call(arguments, 0); - return isSpecRunning() ? workFn() : workFn; + return wasInjectorCreated() ? workFn() : workFn; ///////////////////// function workFn() { if (currentSpec.$injector) { @@ -2654,11 +2620,11 @@ if (window.jasmine || window.mocha) { var fn, modules = currentSpec.$modules || (currentSpec.$modules = []); angular.forEach(moduleFns, function(module) { if (angular.isObject(module) && !angular.isArray(module)) { - fn = function($provide) { + fn = ['$provide', function($provide) { angular.forEach(module, function(value, key) { $provide.value(key, value); }); - }; + }]; } else { fn = module; } @@ -2672,6 +2638,165 @@ if (window.jasmine || window.mocha) { } }; + module.$$beforeAllHook = (window.before || window.beforeAll); + module.$$afterAllHook = (window.after || window.afterAll); + + // purely for testing ngMock itself + module.$$currentSpec = function(to) { + if (arguments.length === 0) return to; + currentSpec = to; + }; + + /** + * @ngdoc function + * @name angular.mock.module.sharedInjector + * @description + * + * *NOTE*: This function is declared ONLY WHEN running tests with jasmine or mocha + * + * This function ensures a single injector will be used for all tests in a given describe context. + * This contrasts with the default behaviour where a new injector is created per test case. + * + * Use sharedInjector when you want to take advantage of Jasmine's `beforeAll()`, or mocha's + * `before()` methods. Call `module.sharedInjector()` before you setup any other hooks that + * will create (i.e call `module()`) or use (i.e call `inject()`) the injector. + * + * You cannot call `sharedInjector()` from within a context already using `sharedInjector()`. + * + * ## Example + * + * Typically beforeAll is used to make many assertions about a single operation. This can + * cut down test run-time as the test setup doesn't need to be re-run, and enabling focussed + * tests each with a single assertion. + * + * ```js + * describe("Deep Thought", function() { + * + * module.sharedInjector(); + * + * beforeAll(module("UltimateQuestion")); + * + * beforeAll(inject(function(DeepThought) { + * expect(DeepThought.answer).toBeUndefined(); + * DeepThought.generateAnswer(); + * })); + * + * it("has calculated the answer correctly", inject(function(DeepThought) { + * // Because of sharedInjector, we have access to the instance of the DeepThought service + * // that was provided to the beforeAll() hook. Therefore we can test the generated answer + * expect(DeepThought.answer).toBe(42); + * })); + * + * it("has calculated the answer within the expected time", inject(function(DeepThought) { + * expect(DeepThought.runTimeMillennia).toBeLessThan(8000); + * })); + * + * it("has double checked the answer", inject(function(DeepThought) { + * expect(DeepThought.absolutelySureItIsTheRightAnswer).toBe(true); + * })); + * + * }); + * + * ``` + */ + module.sharedInjector = function() { + if (!(module.$$beforeAllHook && module.$$afterAllHook)) { + throw Error("sharedInjector() cannot be used unless your test runner defines beforeAll/afterAll"); + } + + var initialized = false; + + module.$$beforeAllHook(function() { + if (injectorState.shared) { + injectorState.sharedError = Error("sharedInjector() cannot be called inside a context that has already called sharedInjector()"); + throw injectorState.sharedError; + } + initialized = true; + currentSpec = this; + injectorState.shared = true; + }); + + module.$$afterAllHook(function() { + if (initialized) { + injectorState = new InjectorState(); + module.$$cleanup(); + } else { + injectorState.sharedError = null; + } + }); + }; + + module.$$beforeEach = function() { + if (injectorState.shared && currentSpec && currentSpec != this) { + var state = currentSpec; + currentSpec = this; + angular.forEach(["$injector","$modules","$providerInjector", "$injectorStrict"], function(k) { + currentSpec[k] = state[k]; + state[k] = null; + }); + } else { + currentSpec = this; + originalRootElement = null; + annotatedFunctions = []; + } + }; + + module.$$afterEach = function() { + if (injectorState.cleanupAfterEach()) { + module.$$cleanup(); + } + }; + + module.$$cleanup = function() { + var injector = currentSpec.$injector; + + annotatedFunctions.forEach(function(fn) { + delete fn.$inject; + }); + + angular.forEach(currentSpec.$modules, function(module) { + if (module && module.$$hashKey) { + module.$$hashKey = undefined; + } + }); + + currentSpec.$injector = null; + currentSpec.$modules = null; + currentSpec.$providerInjector = null; + currentSpec = null; + + if (injector) { + // Ensure `$rootElement` is instantiated, before checking `originalRootElement` + var $rootElement = injector.get('$rootElement'); + var rootNode = $rootElement && $rootElement[0]; + var cleanUpNodes = !originalRootElement ? [] : [originalRootElement[0]]; + if (rootNode && (!originalRootElement || rootNode !== originalRootElement[0])) { + cleanUpNodes.push(rootNode); + } + angular.element.cleanData(cleanUpNodes); + + // Ensure `$destroy()` is available, before calling it + // (a mocked `$rootScope` might not implement it (or not even be an object at all)) + var $rootScope = injector.get('$rootScope'); + if ($rootScope && $rootScope.$destroy) $rootScope.$destroy(); + } + + // clean up jquery's fragment cache + angular.forEach(angular.element.fragments, function(val, key) { + delete angular.element.fragments[key]; + }); + + MockXhr.$$lastInstance = null; + + angular.forEach(angular.callbacks, function(val, key) { + delete angular.callbacks[key]; + }); + angular.callbacks.counter = 0; + }; + + (window.beforeEach || window.setup)(module.$$beforeEach); + (window.afterEach || window.teardown)(module.$$afterEach); + /** * @ngdoc function * @name angular.mock.inject @@ -2774,14 +2899,14 @@ if (window.jasmine || window.mocha) { window.inject = angular.mock.inject = function() { var blockFns = Array.prototype.slice.call(arguments, 0); var errorForStack = new Error('Declaration Location'); - return isSpecRunning() ? workFn.call(currentSpec) : workFn; + return wasInjectorCreated() ? workFn.call(currentSpec) : workFn; ///////////////////// function workFn() { var modules = currentSpec.$modules || []; var strictDi = !!currentSpec.$injectorStrict; - modules.unshift(function($injector) { + modules.unshift(['$injector', function($injector) { currentSpec.$providerInjector = $injector; - }); + }]); modules.unshift('ngMock'); modules.unshift('ng'); var injector = currentSpec.$injector; @@ -2822,7 +2947,7 @@ if (window.jasmine || window.mocha) { angular.mock.inject.strictDi = function(value) { value = arguments.length ? !!value : true; - return isSpecRunning() ? workFn() : workFn; + return wasInjectorCreated() ? workFn() : workFn; function workFn() { if (value !== currentSpec.$injectorStrict) { @@ -2834,7 +2959,16 @@ if (window.jasmine || window.mocha) { } } }; -} + + function InjectorState() { + this.shared = false; + this.sharedError = null; + + this.cleanupAfterEach = function() { + return !this.shared || this.sharedError; + }; + } +})(window.jasmine || window.mocha); })(window, window.angular); diff --git a/public/vendor/angular-mocks/bower.json b/public/vendor/angular-mocks/bower.json index ac8b75413db..4ce65bbc809 100644 --- a/public/vendor/angular-mocks/bower.json +++ b/public/vendor/angular-mocks/bower.json @@ -1,10 +1,10 @@ { "name": "angular-mocks", - "version": "1.5.1-build.4601+sha.c966876", + "version": "1.5.3", "license": "MIT", "main": "./angular-mocks.js", "ignore": [], "dependencies": { - "angular": "1.5.1-build.4601+sha.c966876" + "angular": "1.5.3" } } diff --git a/public/vendor/angular-mocks/package.json b/public/vendor/angular-mocks/package.json index eda7287688f..42d78038650 100644 --- a/public/vendor/angular-mocks/package.json +++ b/public/vendor/angular-mocks/package.json @@ -1,6 +1,6 @@ { "name": "angular-mocks", - "version": "1.5.1-build.4601+sha.c966876", + "version": "1.5.3", "description": "AngularJS mocks for testing", "main": "angular-mocks.js", "scripts": { diff --git a/public/vendor/angular-other/angular-strap.js b/public/vendor/angular-other/angular-strap.js index 204da8a728b..d9721bda038 100644 --- a/public/vendor/angular-other/angular-strap.js +++ b/public/vendor/angular-other/angular-strap.js @@ -79,76 +79,7 @@ angular.module('$strap.directives').factory('$modal', [ return ModalFactory; } ]) -'use strict'; -angular.module('$strap.directives').directive('bsTabs', [ - '$parse', - '$compile', - '$timeout', - function ($parse, $compile, $timeout) { - var template = '
    ' + '' + '
    ' + '
    '; - return { - restrict: 'A', - require: '?ngModel', - priority: 0, - scope: true, - template: template, - replace: true, - transclude: true, - compile: function compile(tElement, tAttrs, transclude) { - return function postLink(scope, iElement, iAttrs, controller) { - var getter = $parse(iAttrs.bsTabs), setter = getter.assign, value = getter(scope); - scope.panes = []; - var $tabs = iElement.find('ul.nav-tabs'); - var $panes = iElement.find('div.tab-content'); - var activeTab = 0, id, title, active; - $timeout(function () { - $panes.find('[data-title], [data-tab]').each(function (index) { - var $this = angular.element(this); - id = 'tab-' + scope.$id + '-' + index; - title = $this.data('title') || $this.data('tab'); - active = !active && $this.hasClass('active'); - $this.attr('id', id).addClass('tab-pane'); - if (iAttrs.fade) - $this.addClass('fade'); - scope.panes.push({ - id: id, - title: title, - content: this.innerHTML, - active: active - }); - }); - if (scope.panes.length && !active) { - $panes.find('.tab-pane:first-child').addClass('active' + (iAttrs.fade ? ' in' : '')); - scope.panes[0].active = true; - } - }); - if (controller) { - iElement.on('show', function (ev) { - var $target = $(ev.target); - scope.$apply(function () { - controller.$setViewValue($target.data('index')); - }); - }); - scope.$watch(iAttrs.ngModel, function (newValue, oldValue) { - if (angular.isUndefined(newValue)) - return; - activeTab = newValue; - setTimeout(function () { - // Check if we're still on the same tab before making the switch - if(activeTab === newValue) { - var $next = $($tabs[0].querySelectorAll('li')[newValue * 1]); - if (!$next.hasClass('active')) { - $next.children('a').tab('show'); - } - } - }); - }); - } - }; - } - }; - } -]); + 'use strict'; angular.module('$strap.directives').directive('bsTooltip', [ '$parse', @@ -202,6 +133,7 @@ angular.module('$strap.directives').directive('bsTooltip', [ }; } ]); + 'use strict'; angular.module('$strap.directives').directive('bsTypeahead', [ '$parse', diff --git a/public/vendor/angular-route/.bower.json b/public/vendor/angular-route/.bower.json index 43ecb04633b..0abe0215778 100644 --- a/public/vendor/angular-route/.bower.json +++ b/public/vendor/angular-route/.bower.json @@ -1,20 +1,20 @@ { "name": "angular-route", - "version": "1.5.1-build.4601+sha.c966876", + "version": "1.5.3", "license": "MIT", "main": "./angular-route.js", "ignore": [], "dependencies": { - "angular": "1.5.1-build.4601+sha.c966876" + "angular": "1.5.3" }, "homepage": "https://github.com/angular/bower-angular-route", - "_release": "1.5.1-build.4601+sha.c966876", + "_release": "1.5.3", "_resolution": { "type": "version", - "tag": "v1.5.1-build.4601+sha.c966876", - "commit": "967fdabf084ac9f37c6b984d8893ebfebde5fc02" + "tag": "v1.5.3", + "commit": "750e4833612071d30993c8e4a547a6982eba3b84" }, "_source": "git://github.com/angular/bower-angular-route.git", - "_target": "~1.5.1", + "_target": "~1.5.3", "_originalSource": "angular-route" } \ No newline at end of file diff --git a/public/vendor/angular-route/angular-route.js b/public/vendor/angular-route/angular-route.js index cde0787fecf..3c9a4e8595e 100644 --- a/public/vendor/angular-route/angular-route.js +++ b/public/vendor/angular-route/angular-route.js @@ -1,5 +1,5 @@ /** - * @license AngularJS v1.5.1-build.4601+sha.c966876 + * @license AngularJS v1.5.3 * (c) 2010-2016 Google, Inc. http://angularjs.org * License: MIT */ @@ -22,7 +22,11 @@ */ /* global -ngRouteModule */ var ngRouteModule = angular.module('ngRoute', ['ng']). - provider('$route', $RouteProvider), + provider('$route', $RouteProvider). + // Ensure `$route` will be instantiated in time to capture the initial + // `$locationChangeSuccess` event. This is necessary in case `ngView` is + // included in an asynchronously loaded template. + run(['$route', angular.noop]), $routeMinErr = angular.$$minErr('ngRoute'); /** @@ -218,9 +222,9 @@ function $RouteProvider() { path = path .replace(/([().])/g, '\\$1') - .replace(/(\/)?:(\w+)([\?\*])?/g, function(_, slash, key, option) { - var optional = option === '?' ? option : null; - var star = option === '*' ? option : null; + .replace(/(\/)?:(\w+)(\*\?|[\?\*])?/g, function(_, slash, key, option) { + var optional = (option === '?' || option === '*?') ? '?' : null; + var star = (option === '*' || option === '*?') ? '*' : null; keys.push({ name: key, optional: !!optional }); slash = slash || ''; return '' diff --git a/public/vendor/angular-route/angular-route.min.js b/public/vendor/angular-route/angular-route.min.js index d4888631eca..5d2e84f9498 100644 --- a/public/vendor/angular-route/angular-route.min.js +++ b/public/vendor/angular-route/angular-route.min.js @@ -1,15 +1,15 @@ /* - AngularJS v1.5.1-build.4601+sha.c966876 + AngularJS v1.5.3 (c) 2010-2016 Google, Inc. http://angularjs.org License: MIT */ (function(r,d,C){'use strict';function x(s,h,g){return{restrict:"ECA",terminal:!0,priority:400,transclude:"element",link:function(a,c,b,f,y){function k(){n&&(g.cancel(n),n=null);l&&(l.$destroy(),l=null);m&&(n=g.leave(m),n.then(function(){n=null}),m=null)}function z(){var b=s.current&&s.current.locals;if(d.isDefined(b&&b.$template)){var b=a.$new(),f=s.current;m=y(b,function(b){g.enter(b,null,m||c).then(function(){!d.isDefined(u)||u&&!a.$eval(u)||h()});k()});l=f.scope=b;l.$emit("$viewContentLoaded"); l.$eval(v)}else k()}var l,m,n,u=b.autoscroll,v=b.onload||"";a.$on("$routeChangeSuccess",z);z()}}}function A(d,h,g){return{restrict:"ECA",priority:-400,link:function(a,c){var b=g.current,f=b.locals;c.html(f.$template);var y=d(c.contents());if(b.controller){f.$scope=a;var k=h(b.controller,f);b.controllerAs&&(a[b.controllerAs]=k);c.data("$ngControllerController",k);c.children().data("$ngControllerController",k)}a[b.resolveAs||"$resolve"]=f;y(a)}}}r=d.module("ngRoute",["ng"]).provider("$route",function(){function s(a, -c){return d.extend(Object.create(a),c)}function h(a,d){var b=d.caseInsensitiveMatch,f={originalPath:a,regexp:a},g=f.keys=[];a=a.replace(/([().])/g,"\\$1").replace(/(\/)?:(\w+)([\?\*])?/g,function(a,d,b,c){a="?"===c?c:null;c="*"===c?c:null;g.push({name:b,optional:!!a});d=d||"";return""+(a?"":d)+"(?:"+(a?d:"")+(c&&"(.+?)"||"([^/]+)")+(a||"")+")"+(a||"")}).replace(/([\/$\*])/g,"\\$1");f.regexp=new RegExp("^"+a+"$",b?"i":"");return f}var g={};this.when=function(a,c){var b=d.copy(c);d.isUndefined(b.reloadOnSearch)&& -(b.reloadOnSearch=!0);d.isUndefined(b.caseInsensitiveMatch)&&(b.caseInsensitiveMatch=this.caseInsensitiveMatch);g[a]=d.extend(b,a&&h(a,b));if(a){var f="/"==a[a.length-1]?a.substr(0,a.length-1):a+"/";g[f]=d.extend({redirectTo:a},h(f,b))}return this};this.caseInsensitiveMatch=!1;this.otherwise=function(a){"string"===typeof a&&(a={redirectTo:a});this.when(null,a);return this};this.$get=["$rootScope","$location","$routeParams","$q","$injector","$templateRequest","$sce",function(a,c,b,f,h,k,r){function l(b){var e= -t.current;(x=(p=n())&&e&&p.$$route===e.$$route&&d.equals(p.pathParams,e.pathParams)&&!p.reloadOnSearch&&!v)||!e&&!p||a.$broadcast("$routeChangeStart",p,e).defaultPrevented&&b&&b.preventDefault()}function m(){var w=t.current,e=p;if(x)w.params=e.params,d.copy(w.params,b),a.$broadcast("$routeUpdate",w);else if(e||w)v=!1,(t.current=e)&&e.redirectTo&&(d.isString(e.redirectTo)?c.path(u(e.redirectTo,e.params)).search(e.params).replace():c.url(e.redirectTo(e.pathParams,c.path(),c.search())).replace()),f.when(e).then(function(){if(e){var a= -d.extend({},e.resolve),b,c;d.forEach(a,function(b,e){a[e]=d.isString(b)?h.get(b):h.invoke(b,null,null,e)});d.isDefined(b=e.template)?d.isFunction(b)&&(b=b(e.params)):d.isDefined(c=e.templateUrl)&&(d.isFunction(c)&&(c=c(e.params)),d.isDefined(c)&&(e.loadedTemplateUrl=r.valueOf(c),b=k(c)));d.isDefined(b)&&(a.$template=b);return f.all(a)}}).then(function(c){e==t.current&&(e&&(e.locals=c,d.copy(e.params,b)),a.$broadcast("$routeChangeSuccess",e,w))},function(b){e==t.current&&a.$broadcast("$routeChangeError", -e,w,b)})}function n(){var a,b;d.forEach(g,function(f,g){var q;if(q=!b){var h=c.path();q=f.keys;var l={};if(f.regexp)if(h=f.regexp.exec(h)){for(var k=1,n=h.length;k + * **Note:** Angular does not make a copy of the `data` parameter before it is passed into the `transformRequest` pipeline. + * That means changes to the properties of `data` are not local to the transform function (since Javascript passes objects by reference). + * For example, when calling `$http.get(url, $scope.myObject)`, modifications to the object's properties in a transformRequest + * function will be reflected on the scope and in any templates where the object is data-bound. + * To prevent his, transform functions should have no side-effects. + * If you need to modify properties, it is recommended to make a copy of the data, or create new object to return. + *
    + * * ### Default Transformations * * The `$httpProvider` provider and `$http` service expose `defaults.transformRequest` and @@ -10614,26 +10778,35 @@ function $HttpProvider() { * * ## Caching * - * To enable caching, set the request configuration `cache` property to `true` (to use default - * cache) or to a custom cache object (built with {@link ng.$cacheFactory `$cacheFactory`}). - * When the cache is enabled, `$http` stores the response from the server in the specified - * cache. The next time the same request is made, the response is served from the cache without - * sending a request to the server. + * {@link ng.$http `$http`} responses are not cached by default. To enable caching, you must + * set the config.cache value or the default cache value to TRUE or to a cache object (created + * with {@link ng.$cacheFactory `$cacheFactory`}). If defined, the value of config.cache takes + * precedence over the default cache value. * - * Note that even if the response is served from cache, delivery of the data is asynchronous in - * the same way that real requests are. + * In order to: + * * cache all responses - set the default cache value to TRUE or to a cache object + * * cache a specific response - set config.cache value to TRUE or to a cache object * - * If there are multiple GET requests for the same URL that should be cached using the same - * cache, but the cache is not populated yet, only one request to the server will be made and - * the remaining requests will be fulfilled using the response from the first request. + * If caching is enabled, but neither the default cache nor config.cache are set to a cache object, + * then the default `$cacheFactory($http)` object is used. * - * You can change the default cache to a new object (built with - * {@link ng.$cacheFactory `$cacheFactory`}) by updating the - * {@link ng.$http#defaults `$http.defaults.cache`} property. All requests who set - * their `cache` property to `true` will now use this cache object. + * The default cache value can be set by updating the + * {@link ng.$http#defaults `$http.defaults.cache`} property or the + * {@link $httpProvider#defaults `$httpProvider.defaults.cache`} property. + * + * When caching is enabled, {@link ng.$http `$http`} stores the response from the server using + * the relevant cache object. The next time the same request is made, the response is returned + * from the cache without sending a request to the server. + * + * Take note that: + * + * * Only GET and JSONP requests are cached. + * * The cache key is the request URL including search parameters; headers are not considered. + * * Cached responses are returned asynchronously, in the same way as responses from the server. + * * If multiple identical requests are made using the same cache, which is not yet populated, + * one request will be made to the server and remaining requests will return the same response. + * * A cache-control header on the response does not affect if or how responses are cached. * - * If you set the default cache to `false` then only requests that specify their own custom - * cache object will be cached. * * ## Interceptors * @@ -10803,7 +10976,7 @@ function $HttpProvider() { * transform function or an array of such functions. The transform function takes the http * response body, headers and status and returns its transformed (typically deserialized) version. * See {@link ng.$http#overriding-the-default-transformations-per-request - * Overriding the Default TransformationjqLiks} + * Overriding the Default Transformations} * - **paramSerializer** - `{string|function(Object):string}` - A function used to * prepare the string representation of request parameters (specified as an object). * If specified as string, it is interpreted as function registered with the @@ -10811,10 +10984,9 @@ function $HttpProvider() { * by registering it as a {@link auto.$provide#service service}. * The default serializer is the {@link $httpParamSerializer $httpParamSerializer}; * alternatively, you can use the {@link $httpParamSerializerJQLike $httpParamSerializerJQLike} - * - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the - * GET request, otherwise if a cache instance built with - * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for - * caching. + * - **cache** – `{boolean|Object}` – A boolean value or object created with + * {@link ng.$cacheFactory `$cacheFactory`} to enable or disable caching of the HTTP response. + * See {@link $http#caching $http Caching} for more information. * - **timeout** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} * that should abort the request when resolved. * - **withCredentials** - `{boolean}` - whether to set the `withCredentials` flag on the @@ -13725,8 +13897,10 @@ AST.prototype = { primary = this.arrayDeclaration(); } else if (this.expect('{')) { primary = this.object(); - } else if (this.constants.hasOwnProperty(this.peek().text)) { - primary = copy(this.constants[this.consume().text]); + } else if (this.selfReferential.hasOwnProperty(this.peek().text)) { + primary = copy(this.selfReferential[this.consume().text]); + } else if (this.options.literals.hasOwnProperty(this.peek().text)) { + primary = { type: AST.Literal, value: this.options.literals[this.consume().text]}; } else if (this.peek().identifier) { primary = this.identifier(); } else if (this.peek().constant) { @@ -13878,15 +14052,7 @@ AST.prototype = { return false; }, - - /* `undefined` is not a constant, it is an identifier, - * but using it as an identifier is not supported - */ - constants: { - 'true': { type: AST.Literal, value: true }, - 'false': { type: AST.Literal, value: false }, - 'null': { type: AST.Literal, value: null }, - 'undefined': {type: AST.Literal, value: undefined }, + selfReferential: { 'this': {type: AST.ThisExpression }, '$locals': {type: AST.LocalsExpression } } @@ -14576,7 +14742,7 @@ ASTInterpreter.prototype = { forEach(ast.body, function(expression) { expressions.push(self.recurse(expression.expression)); }); - var fn = ast.body.length === 0 ? function() {} : + var fn = ast.body.length === 0 ? noop : ast.body.length === 1 ? expressions[0] : function(scope, locals) { var lastValue; @@ -14717,7 +14883,7 @@ ASTInterpreter.prototype = { return context ? {value: locals} : locals; }; case AST.NGValueParameter: - return function(scope, locals, assign, inputs) { + return function(scope, locals, assign) { return context ? {value: assign} : assign; }; } @@ -14931,7 +15097,7 @@ var Parser = function(lexer, $filter, options) { this.lexer = lexer; this.$filter = $filter; this.options = options; - this.ast = new AST(this.lexer); + this.ast = new AST(lexer, options); this.astCompiler = options.csp ? new ASTInterpreter(this.ast, $filter) : new ASTCompiler(this.ast, $filter); }; @@ -15008,16 +15174,39 @@ function getValueOf(value) { function $ParseProvider() { var cacheDefault = createMap(); var cacheExpensive = createMap(); + var literals = { + 'true': true, + 'false': false, + 'null': null, + 'undefined': undefined + }; + + /** + * @ngdoc method + * @name $parseProvider#addLiteral + * @description + * + * Configure $parse service to add literal values that will be present as literal at expressions. + * + * @param {string} literalName Token for the literal value. The literal name value must be a valid literal name. + * @param {*} literalValue Value for this literal. All literal values must be primitives or `undefined`. + * + **/ + this.addLiteral = function(literalName, literalValue) { + literals[literalName] = literalValue; + }; this.$get = ['$filter', function($filter) { var noUnsafeEval = csp().noUnsafeEval; var $parseOptions = { csp: noUnsafeEval, - expensiveChecks: false + expensiveChecks: false, + literals: copy(literals) }, $parseOptionsExpensive = { csp: noUnsafeEval, - expensiveChecks: true + expensiveChecks: true, + literals: copy(literals) }; var runningChecksEnabled = false; @@ -15266,15 +15455,15 @@ function $ParseProvider() { * [Kris Kowal's Q](https://github.com/kriskowal/q). * * $q can be used in two fashions --- one which is more similar to Kris Kowal's Q or jQuery's Deferred - * implementations, and the other which resembles ES6 promises to some degree. + * implementations, and the other which resembles ES6 (ES2015) promises to some degree. * * # $q constructor * * The streamlined ES6 style promise is essentially just using $q as a constructor which takes a `resolver` - * function as the first argument. This is similar to the native Promise implementation from ES6 Harmony, + * function as the first argument. This is similar to the native Promise implementation from ES6, * see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). * - * While the constructor-style use is supported, not all of the supporting methods from ES6 Harmony promises are + * While the constructor-style use is supported, not all of the supporting methods from ES6 promises are * available yet. * * It can be used like so: @@ -16627,7 +16816,7 @@ function $RootScopeProvider() { dirty, ttl = TTL, next, current, target = this, watchLog = [], - logIdx, logMsg, asyncTask; + logIdx, asyncTask; beginPhase('$digest'); // Check for changes to browser url that happened in sync before the call to $digest @@ -18412,6 +18601,10 @@ function $SceProvider() { function $SnifferProvider() { this.$get = ['$window', '$document', function($window, $document) { var eventSupport = {}, + // Chrome Packaged Apps are not allowed to access `history.pushState`. They can be detected by + // the presence of `chrome.app.runtime` (see https://developer.chrome.com/apps/api_index) + isChromePackagedApp = $window.chrome && $window.chrome.app && $window.chrome.app.runtime, + hasHistoryPushState = !isChromePackagedApp && $window.history && $window.history.pushState, android = toInt((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), boxee = /Boxee/i.test(($window.navigator || {}).userAgent), @@ -18456,7 +18649,7 @@ function $SnifferProvider() { // so let's not use the history API also // We are purposefully using `!(android < 4)` to cover the case when `android` is undefined // jshint -W018 - history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee), + history: !!(hasHistoryPushState && !(android < 4) && !boxee), // jshint +W018 hasEvent: function(event) { // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have @@ -18482,7 +18675,7 @@ function $SnifferProvider() { }]; } -var $compileMinErr = minErr('$compile'); +var $templateRequestMinErr = minErr('$compile'); /** * @ngdoc provider @@ -18578,7 +18771,7 @@ function $TemplateRequestProvider() { function handleError(resp) { if (!ignoreRequestError) { - throw $compileMinErr('tpload', 'Failed to load template: {0} (HTTP status: {1} {2})', + throw $templateRequestMinErr('tpload', 'Failed to load template: {0} (HTTP status: {1} {2})', tpl, resp.status, resp.statusText); } return $q.reject(resp); @@ -19500,7 +19693,7 @@ function currencyFilter($locale) { * Formats a number as text. * * If the input is null or undefined, it will just be returned. - * If the input is infinite (Infinity/-Infinity) the Infinity symbol '∞' is returned. + * If the input is infinite (Infinity or -Infinity), the Infinity symbol '∞' or '-∞' is returned, respectively. * If the input is not a number an empty string is returned. * * @@ -19636,18 +19829,37 @@ function roundNumber(parsedNumber, fractionSize, minFrac, maxFrac) { var digit = digits[roundAt]; if (roundAt > 0) { - digits.splice(roundAt); + // Drop fractional digits beyond `roundAt` + digits.splice(Math.max(parsedNumber.i, roundAt)); + + // Set non-fractional digits beyond `roundAt` to 0 + for (var j = roundAt; j < digits.length; j++) { + digits[j] = 0; + } } else { // We rounded to zero so reset the parsedNumber + fractionLen = Math.max(0, fractionLen); parsedNumber.i = 1; - digits.length = roundAt = fractionSize + 1; - for (var i=0; i < roundAt; i++) digits[i] = 0; + digits.length = Math.max(1, roundAt = fractionSize + 1); + digits[0] = 0; + for (var i = 1; i < roundAt; i++) digits[i] = 0; } - if (digit >= 5) digits[roundAt - 1]++; + if (digit >= 5) { + if (roundAt - 1 < 0) { + for (var k = 0; k > roundAt; k--) { + digits.unshift(0); + parsedNumber.i++; + } + digits.unshift(1); + parsedNumber.i++; + } else { + digits[roundAt - 1]++; + } + } // Pad out with zeros to get the required fraction length - for (; fractionLen < fractionSize; fractionLen++) digits.push(0); + for (; fractionLen < Math.max(0, fractionSize); fractionLen++) digits.push(0); // Do any carrying, e.g. a digit was rounded up to 10 @@ -19719,7 +19931,7 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { // format the integer digits with grouping separators var groups = []; - if (digits.length > pattern.lgSize) { + if (digits.length >= pattern.lgSize) { groups.unshift(digits.splice(-pattern.lgSize).join('')); } while (digits.length > pattern.gSize) { @@ -19746,11 +19958,15 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { } } -function padNumber(num, digits, trim) { +function padNumber(num, digits, trim, negWrap) { var neg = ''; - if (num < 0) { - neg = '-'; - num = -num; + if (num < 0 || (negWrap && num <= 0)) { + if (negWrap) { + num = -num + 1; + } else { + num = -num; + neg = '-'; + } } num = '' + num; while (num.length < digits) num = ZERO_CHAR + num; @@ -19761,7 +19977,7 @@ function padNumber(num, digits, trim) { } -function dateGetter(name, size, offset, trim) { +function dateGetter(name, size, offset, trim, negWrap) { offset = offset || 0; return function(date) { var value = date['get' + name](); @@ -19769,14 +19985,15 @@ function dateGetter(name, size, offset, trim) { value += offset; } if (value === 0 && offset == -12) value = 12; - return padNumber(value, size, trim); + return padNumber(value, size, trim, negWrap); }; } -function dateStrGetter(name, shortForm) { +function dateStrGetter(name, shortForm, standAlone) { return function(date, formats) { var value = date['get' + name](); - var get = uppercase(shortForm ? ('SHORT' + name) : name); + var propPrefix = (standAlone ? 'STANDALONE' : '') + (shortForm ? 'SHORT' : ''); + var get = uppercase(propPrefix + name); return formats[get][value]; }; @@ -19831,13 +20048,14 @@ function longEraGetter(date, formats) { } var DATE_FORMATS = { - yyyy: dateGetter('FullYear', 4), - yy: dateGetter('FullYear', 2, 0, true), - y: dateGetter('FullYear', 1), + yyyy: dateGetter('FullYear', 4, 0, false, true), + yy: dateGetter('FullYear', 2, 0, true, true), + y: dateGetter('FullYear', 1, 0, false, true), MMMM: dateStrGetter('Month'), MMM: dateStrGetter('Month', true), MM: dateGetter('Month', 2, 1), M: dateGetter('Month', 1, 1), + LLLL: dateStrGetter('Month', false, true), dd: dateGetter('Date', 2), d: dateGetter('Date', 1), HH: dateGetter('Hours', 2), @@ -19863,7 +20081,7 @@ var DATE_FORMATS = { GGGG: longEraGetter }; -var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/, +var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/, NUMBER_STRING = /^\-?\d+$/; /** @@ -19883,6 +20101,7 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+| * * `'MMM'`: Month in year (Jan-Dec) * * `'MM'`: Month in year, padded (01-12) * * `'M'`: Month in year (1-12) + * * `'LLLL'`: Stand-alone month in year (January-December) * * `'dd'`: Day in month, padded (01-31) * * `'d'`: Day in month (1-31) * * `'EEEE'`: Day in Week,(Sunday-Saturday) @@ -21564,8 +21783,8 @@ var ngFormDirective = formDirectiveFactory(true); ngModelMinErr: false, */ -// Regex code is obtained from SO: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231 -var ISO_DATE_REGEXP = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/; +// Regex code was initially obtained from SO prior to modification: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231 +var ISO_DATE_REGEXP = /^\d{4,}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+-][0-2]\d:[0-5]\d|Z)$/; // See valid URLs in RFC3987 (http://tools.ietf.org/html/rfc3987) // Note: We are being more lenient, because browsers are too. // 1. Scheme @@ -21581,12 +21800,18 @@ var ISO_DATE_REGEXP = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0- var URL_REGEXP = /^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+\])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i; var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i; var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/; -var DATE_REGEXP = /^(\d{4})-(\d{2})-(\d{2})$/; -var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; -var WEEK_REGEXP = /^(\d{4})-W(\d\d)$/; -var MONTH_REGEXP = /^(\d{4})-(\d\d)$/; +var DATE_REGEXP = /^(\d{4,})-(\d{2})-(\d{2})$/; +var DATETIMELOCAL_REGEXP = /^(\d{4,})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; +var WEEK_REGEXP = /^(\d{4,})-W(\d\d)$/; +var MONTH_REGEXP = /^(\d{4,})-(\d\d)$/; var TIME_REGEXP = /^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; +var PARTIAL_VALIDATION_EVENTS = 'keydown wheel mousedown'; +var PARTIAL_VALIDATION_TYPES = createMap(); +forEach('date,datetime-local,month,time,week'.split(','), function(type) { + PARTIAL_VALIDATION_TYPES[type] = true; +}); + var inputType = { /** @@ -21942,7 +22167,7 @@ var inputType = { }]);
    - +
    @@ -22663,7 +22888,7 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { if (!$sniffer.android) { var composing = false; - element.on('compositionstart', function(data) { + element.on('compositionstart', function() { composing = true; }); @@ -22673,6 +22898,8 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { }); } + var timeout; + var listener = function(ev) { if (timeout) { $browser.defer.cancel(timeout); @@ -22702,8 +22929,6 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { if ($sniffer.hasEvent('input')) { element.on('input', listener); } else { - var timeout; - var deferListener = function(ev, input, origValue) { if (!timeout) { timeout = $browser.defer(function() { @@ -22735,6 +22960,26 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { // or form autocomplete on newer browser, we need "change" event to catch it element.on('change', listener); + // Some native input types (date-family) have the ability to change validity without + // firing any input/change events. + // For these event types, when native validators are present and the browser supports the type, + // check for validity changes on various DOM events. + if (PARTIAL_VALIDATION_TYPES[type] && ctrl.$$hasNativeValidators && type === attr.type) { + element.on(PARTIAL_VALIDATION_EVENTS, function(ev) { + if (!timeout) { + var validity = this[VALIDITY_STATE_PROPERTY]; + var origBadInput = validity.badInput; + var origTypeMismatch = validity.typeMismatch; + timeout = $browser.defer(function() { + timeout = null; + if (validity.badInput !== origBadInput || validity.typeMismatch !== origTypeMismatch) { + listener(ev); + } + }); + } + }); + } + ctrl.$render = function() { // Workaround for Firefox validation #12102. var value = ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue; @@ -25061,7 +25306,7 @@ forEach( */ -var ngIfDirective = ['$animate', function($animate) { +var ngIfDirective = ['$animate', '$compile', function($animate, $compile) { return { multiElement: true, transclude: 'element', @@ -25077,7 +25322,7 @@ var ngIfDirective = ['$animate', function($animate) { if (!childScope) { $transclude(function(clone, newScope) { childScope = newScope; - clone[clone.length++] = document.createComment(' end ngIf: ' + $attr.ngIf + ' '); + clone[clone.length++] = $compile.$$createComment('end ngIf', $attr.ngIf); // Note: We only need the first/last node of the cloned nodes. // However, we need to keep the reference to the jqlite wrapper as it might be changed later // by a directive with templateUrl when its template arrives. @@ -25876,9 +26121,9 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ }; ngModelSet = function($scope, newValue) { if (isFunction(parsedNgModel($scope))) { - invokeModelSetter($scope, {$$$p: ctrl.$modelValue}); + invokeModelSetter($scope, {$$$p: newValue}); } else { - parsedNgModelAssign($scope, ctrl.$modelValue); + parsedNgModelAssign($scope, newValue); } }; } else if (!parsedNgModel.assign) { @@ -25903,7 +26148,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * the `$viewValue` are different from last time. * * Since `ng-model` does not do a deep watch, `$render()` is only invoked if the values of - * `$modelValue` and `$viewValue` are actually different from their previous value. If `$modelValue` + * `$modelValue` and `$viewValue` are actually different from their previous values. If `$modelValue` * or `$viewValue` are objects (rather than a string or number) then `$render()` will not be * invoked if you only change a property on the objects. */ @@ -26255,7 +26500,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ setValidity(name, undefined); validatorPromises.push(promise.then(function() { setValidity(name, true); - }, function(error) { + }, function() { allValid = false; setValidity(name, false); })); @@ -26729,7 +26974,7 @@ var ngModelDirective = ['$rootScope', function($rootScope) { }); } - element.on('blur', function(ev) { + element.on('blur', function() { if (modelCtrl.$touched) return; if ($rootScope.$$phase) { @@ -27412,8 +27657,8 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { var key = (optionValues === optionValuesKeys) ? index : optionValuesKeys[index]; var value = optionValues[key]; - var locals = getLocals(optionValues[key], key); - var selectValue = getTrackByValueFn(optionValues[key], locals); + var locals = getLocals(value, key); + var selectValue = getTrackByValueFn(value, locals); watchedArray.push(selectValue); // Only need to watch the displayFn if there is a specific label expression @@ -27538,14 +27783,20 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { var option = options.getOptionFromViewValue(value); if (option && !option.disabled) { + // Don't update the option when it is already selected. + // For example, the browser will select the first option by default. In that case, + // most properties are set automatically - except the `selected` attribute, which we + // set always + if (selectElement[0].value !== option.selectValue) { removeUnknownOption(); removeEmptyOption(); selectElement[0].value = option.selectValue; option.element.selected = true; - option.element.setAttribute('selected', 'selected'); } + + option.element.setAttribute('selected', 'selected'); } else { if (value === null || providedEmptyOption) { removeUnknownOption(); @@ -28390,7 +28641,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, */ -var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { +var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $animate, $compile) { var NG_REMOVED = '$$NG_REMOVED'; var ngRepeatMinErr = minErr('ngRepeat'); @@ -28425,7 +28676,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { $$tlb: true, compile: function ngRepeatCompile($element, $attr) { var expression = $attr.ngRepeat; - var ngRepeatEndComment = document.createComment(' end ngRepeat: ' + expression + ' '); + var ngRepeatEndComment = $compile.$$createComment('end ngRepeat', expression); var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/); @@ -29142,7 +29393,7 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { */ -var ngSwitchDirective = ['$animate', function($animate) { +var ngSwitchDirective = ['$animate', '$compile', function($animate, $compile) { return { require: 'ngSwitch', @@ -29183,7 +29434,7 @@ var ngSwitchDirective = ['$animate', function($animate) { selectedTransclude.transclude(function(caseElement, selectedScope) { selectedScopes.push(selectedScope); var anchor = selectedTransclude.element; - caseElement[caseElement.length++] = document.createComment(' end ngSwitchWhen: '); + caseElement[caseElement.length++] = $compile.$$createComment('end ngSwitchWhen'); var block = { clone: caseElement }; selectedElements.push(block); @@ -29477,7 +29728,7 @@ function chromeHack(optionElement) { * added `